toga-ai 1.0.936 → 1.0.937
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/1.0/apps/togadesk/INDEX.md +2 -1
- package/knowledge/1.0/apps/togadesk/architecture.md +12 -4
- package/knowledge/1.0/apps/togadesk/features/staff-login-authentication.md +17 -3
- package/knowledge/1.0/apps/togadesk/features/staff-sso-login.md +72 -0
- package/knowledge/1.0/apps/tools/features/saml-sso-auth.md +8 -3
- package/knowledge/2.0/apps/saml/features/acs-assertion-trust-gaps.md +16 -3
- package/knowledge/2.0/apps/saml/features/downstream-integration-contract.md +18 -3
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/true/profile.md +6 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Summary |
|
|
4
4
|
|-----|---------|
|
|
5
|
-
| [TOGa Desk Architecture](architecture.md) |
|
|
5
|
+
| [TOGa Desk Architecture](architecture.md) | 1.0 (App_) support desk + field-service platform — staff at `/desk/`, public/client portal at `/`; open for the request pipeline, domain-class layer, MSP data m |
|
|
6
6
|
| [Email-to-Ticket Intake (crons/tickets.php)](features/email-to-ticket-intake.md) | TOGa Desk email→ticket intake via IMAP poller (`crons/tickets.php`) + postfix pipe (`crons/pipe.php`), both into `Ticket::emailToTicket()`; open for routing/thr |
|
|
7
7
|
| [Field-Service Dispatch (central / repair orders)](features/field-service-dispatch.md) | TOGa Desk field-service dispatch (the **central** domain): repair-order lifecycle, technician scheduling, onsite/depot, parts, and UPS shipment tracking; open f |
|
|
8
8
|
| [Managed Service Order Create Flow (New MSO modal → tickets/add)](features/managed-service-order-create.md) | TOGa Desk Managed Service Order (MSO) create flow: New MSO modal → `tickets/add` builds a `ManagedServiceOrder` + assets + a `RepairOrder` per asset; open for t |
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| [REST API (RPC-over-POST) & API-Key Auth](features/rest-api.md) | TOGa Desk programmatic API at `desk/api/` — an RPC-over-POST dispatcher (not verb/URL REST) with API-key auth; open for the request shape, status codes, auth fl |
|
|
12
12
|
| [SMB Contract Editing & the clientMspId Corruption Trap](features/smb-contract-editing.md) | TOGa Desk SMB contracts modal (`/desk/?route=toga/smbcontracts`) and the `clientMspId` corruption trap that made MSP portal users see "You have no assigned clie |
|
|
13
13
|
| [Staff Login Authentication (signIn) — AD vs. local password](features/staff-login-authentication.md) | TOGa Desk staff login via `signIn()` — AD branch (live) vs. |
|
|
14
|
+
| [Staff SSO Login (Sso class via saml.togahub.com)](features/staff-sso-login.md) | TOGa Desk staff "Sign in with SSO" via saml.togahub.com (TRUE-78064, `Sso` class, Tools pattern); open before touching SSO login, the `?saml=` return, the `[sam |
|
|
14
15
|
| [Ticket "Assign To" Lists (who can be assigned a ticket)](features/ticket-assign-to-lists.md) | How the ticket "Assign To" pickers are built (new/edit/merge modals, ticket page) and the hardcoded client exclusion list (`ASSIGN_TO_EXCLUDED_CLIENTS`, IM Hous |
|
|
15
16
|
| [Ticket Lifecycle (class.ticket.php)](features/ticket-lifecycle.md) | TOGa Desk ticket creation + reply handling via `Ticket` (class.ticket.php): the four creation/reply doors, `deriveCustomerId()`, and the `addReply()` status swi |
|
|
16
17
|
| [Reverting a TOGa Desk _production Deploy (revert-forward)](workflows/production-revert-forward.md) | How to cleanly roll back one ticket's changes from the togadesk `_production` branch via `git revert -m 1` of its two-parent merge, verified with `git patch-id` |
|
|
@@ -6,9 +6,11 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-02
|
|
10
10
|
owners: ["mhammontree", "jcardinal"]
|
|
11
11
|
files:
|
|
12
|
+
- index.php
|
|
13
|
+
- includes/loader.php
|
|
12
14
|
- desk/index.php
|
|
13
15
|
- desk/includes/loader.php
|
|
14
16
|
- desk/config.php
|
|
@@ -33,17 +35,20 @@ related:
|
|
|
33
35
|
- 1.0/apps/togadesk/features/email-to-ticket-intake.md
|
|
34
36
|
- 1.0/apps/togadesk/features/notifications.md
|
|
35
37
|
- 1.0/apps/togadesk/workflows/standalone-test-scripts.md
|
|
38
|
+
- 1.0/apps/togadesk/features/staff-sso-login.md
|
|
36
39
|
---
|
|
37
40
|
|
|
38
|
-
|
|
41
|
+
1.0 (App_) support desk + field-service platform — staff at `/desk/`, public/client portal at `/`; open for the request pipeline, domain-class layer, MSP data model, crons/integrations, or system topology.
|
|
39
42
|
|
|
40
43
|
## Summary
|
|
41
44
|
TOGa Desk is the staff-facing support desk **and field-service platform** (analysts work at `/desk/`). Framework 1.0 (`App_`) app bootstrapped from the `library` core (`_.php`). Beyond ticketing it covers asset/license/inventory management, repair-order dispatch (depot + onsite technicians), a knowledge base, host/service monitoring, RBAC, a REST-ish API, and IMAP email intake. Primary database `db_togadesk` (schema `TOGaDeskSupport`); also `db_common` and per-client databases.
|
|
42
45
|
|
|
43
|
-
**Critical rules:**
|
|
46
|
+
**Critical rules:** there are **two live trees** — `desk/` (staff app) and the repo root (`index.php` → `includes/loader.php`, the public/client portal served at `/`; docroot = repo root). Do not assume root code is dead; but root `includes/classes/` holds stale variants (e.g. root `class.ticket.php`) — live domain classes are in `desk/includes/classes/`. Per-client host restriction (`getRestrictedClient()`) is an **entry/UI gate only** — record-level tenancy comes from the session `clientid`, not the host. TOGa Desk **shares the `TOGaDeskSupport` database with TOGa View** — a change to ticket data semantics affects both apps. `ENVIRONMENT` defaults to `worker` when unset → CLI scripts read the wrong config file.
|
|
44
47
|
|
|
45
48
|
## Entry points & request pipeline
|
|
46
|
-
- **Web:** `desk/index.php` → `desk/includes/loader.php`.
|
|
49
|
+
- **Web, staff:** `desk/index.php` → `desk/includes/loader.php`.
|
|
50
|
+
- **Web, public/client portal:** repo-root `index.php` → root `includes/loader.php`, served at `/` (docroot = repo root). Clients (incl. custom-URL hosts) and staff sign in here; staff are then redirected to `/desk/`. The root loader requires `desk/includes/functions.php`, so `signIn()` etc. are the desk versions. Root page templates live in `template/pages/` (e.g. the login card in `template/pages/dashboard.php`).
|
|
51
|
+
- **Staff SSO** (TRUE-78064, prod only): `?route=signin&sso=1` and the gateway's `?saml=` return route through `general.php` → `Sso`. See [staff SSO login](features/staff-sso-login.md).
|
|
47
52
|
- **Bootstrap order** (`loader.php`): local `functions.php` → `config.php` → register the two custom autoloaders `vendorClassAutoload` (`desk/vendor/classes/`) and `appClassAutoload` (`desk/includes/classes/`) → Composer autoload → `require _.php` + `App_Framework_TOGaDesk::initialize()` (starts session with `HttpOnly`/`Secure`, inits Sentry, XSS-protects `$_REQUEST`/`$_GET`/`$_POST`) → `App_Page::validateCrossSiteRequestForgery()` → Medoo `$database` instantiated from config → resolve logged-in user `$liu` via `people.sessionid` → unserialize the user's role `perms`.
|
|
48
53
|
- **`$route`** comes from `$_GET['route']` (defaults to `dashboard`) and is sanitized against path traversal.
|
|
49
54
|
- **Sequential controller pipeline**, each stage gated on a query param:
|
|
@@ -136,12 +141,15 @@ Invariants:
|
|
|
136
141
|
- `ENVIRONMENT` defaults to `worker` when unset → CLI scripts read the wrong config file.
|
|
137
142
|
- Heavy **hardcoded client-ID branching** in `central` dispatch and `tickets/add` (Walmart, WJE, Compass/ODP, AIG, RUMCSI) — verify the client arm before changing shared logic.
|
|
138
143
|
- **Two `class.ticket.php` files** — only `desk/includes/classes/` is live; the root variant is dead (random ticket numbers, stale schema).
|
|
144
|
+
- **Autoloader path differs local vs prod.** `appClassAutoload()` (`desk/includes/functions.php` ~L418) appends `/desk` only when `$scriptpath` does NOT end in `desk`. A local checkout at `.../togadesk` ends in `desk`, so the root tree loads **root** `includes/classes` locally but `desk/includes/classes` in prod (`/var/app/current`). Root pages needing a desk class should `require_once` it explicitly (see `template/pages/dashboard.php` for `Sso`). Not fixed.
|
|
145
|
+
- **⚠ `config.*.ini` may be web-readable.** Docroot is the repo root and there is no `FilesMatch` deny for `*.ini`; `config.prod.ini` is committed with plaintext DB/SMTP creds (and, with SSO, Core + Client_True creds). Use read-only DB users / EB env vars. Not fixed — see [staff SSO login](features/staff-sso-login.md).
|
|
139
146
|
- The API has **no pagination** — `get` returns full result sets; Medoo `filters` pass straight to the WHERE clause.
|
|
140
147
|
- The prod O365 OAuth secret used by `crons/tickets_prod.php` has a **hard expiry (~2026-12-20)**.
|
|
141
148
|
- **Cross-tenant login hole in the per-client host restriction (pre-existing, NOT fixed).** The password-login access condition in `desk/includes/functions.php` (~L627-631) is a chain of ORs; two clauses grant access regardless of which client's subdomain is in use, letting an IM Houston or Fordham user sign in on another client's branded host. Post-login filtering keys off the session `clientid` so it is not an immediate record leak, but it defeats the restriction and contradicts the comment directly above it. Deserves its own ticket.
|
|
142
149
|
- **Sessions are host-scoped, so a domain change logs everyone out.** `ebs/setup_phpini.php` sets `session.save_handler=rediscluster` but no `session.cookie_domain`, and there is no `session_set_cookie_params()` anywhere. Combined with `people.sessionid` being a **single column**, logging in on a second domain invalidates the session on the first.
|
|
143
150
|
|
|
144
151
|
## Related
|
|
152
|
+
- [Staff SSO login](features/staff-sso-login.md)
|
|
145
153
|
- [TOGa View architecture](../togaview/architecture.md)
|
|
146
154
|
- [Ticket lifecycle](features/ticket-lifecycle.md)
|
|
147
155
|
- [REST API](features/rest-api.md)
|
|
@@ -6,13 +6,14 @@ project: TOGa Desk
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-02
|
|
10
10
|
owners: ["mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- desk/includes/functions.php
|
|
13
13
|
- desk/includes/controllers/general.php
|
|
14
14
|
- desk/includes/controllers/actions/user/loginAsAnother.php
|
|
15
15
|
related:
|
|
16
|
+
- 1.0/apps/togadesk/features/staff-sso-login.md
|
|
16
17
|
- 1.0/apps/togadesk/features/per-client-host-restriction.md
|
|
17
18
|
- 1.0/apps/togaview/features/login-flows.md
|
|
18
19
|
- 1.0/apps/togadesk/architecture.md
|
|
@@ -24,6 +25,8 @@ TOGa Desk staff login via `signIn()` — AD branch (live) vs. the armed email-on
|
|
|
24
25
|
|
|
25
26
|
TOGa Desk staff login runs through `signIn()` in `desk/includes/functions.php` (~L458), reached from `desk/includes/controllers/general.php:8`. It has **two mutually exclusive branches** selected by the feature flag `App_Feature::ACTIVE_DIRECTORY_LOGIN`. The AD branch is the live production path; the non-AD `else` branch is an **armed landmine** whose password check is commented out (see Gotchas).
|
|
26
27
|
|
|
28
|
+
Staff **SSO** (TRUE-78064, prod only) is a separate path that does not go through `signIn()` — see [staff SSO login](staff-sso-login.md). The password form below stays for clients, staff fallback, and the dev bypass.
|
|
29
|
+
|
|
27
30
|
`signIn()` is also the **only** place `people.lastlogin` is written — which makes that column useless for measuring TOGa View portal usage.
|
|
28
31
|
|
|
29
32
|
## How it works
|
|
@@ -32,7 +35,8 @@ TOGa Desk staff login runs through `signIn()` in `desk/includes/functions.php` (
|
|
|
32
35
|
2. `signIn()` branches on `App_Feature::ACTIVE_DIRECTORY_LOGIN`. **Verified in production (2026-08-21):** `Vision.FeatureFlags` id 7, `framework = 'App_Framework_TOGaDesk'`, `isActive = 1`, created 2020-08-10 — so the AD branch has been the live path for five years.
|
|
33
36
|
3. **AD branch.** Attempts an Active Directory check against Agilant's directory (`db_common`). Emails outside Agilant's AD — every client user — fail that check and fall through to the **local credential check** at ~L611-620: `people.email LIKE … AND people.password LIKE sha1($password) AND people.adPeopleId IS NULL`.
|
|
34
37
|
4. **Client-restriction gate (~L631).** After a successful local check, access is gated by an OR-chain keyed on `getRestrictedClient()`. Client constants live in the same file (~L1488-1531), e.g. `CLIENT_RUMCSI = 146`, `CLIENT_DEPARTMENT_RUMCSI = 231`. The per-client branded host is resolved by `getRestrictedClient()` (~L1558) — see [per-client hostname restriction](per-client-host-restriction.md).
|
|
35
|
-
5. On success `signIn()` writes `people.lastlogin` (`desk/includes/functions.php:588, 635, 672, 689`). `desk/includes/controllers/actions/user/loginAsAnother.php:17-18`
|
|
38
|
+
5. On success `signIn()` writes `people.lastlogin` (`desk/includes/functions.php:588, 635, 672, 689`). `desk/includes/controllers/actions/user/loginAsAnother.php:17-18` and `Sso::consume()` are the only other writers.
|
|
39
|
+
6. **Dev bypass.** `devLoginBypassAllowed()` (~L468) skips the AD password check (~L550, ~L685) when in dev **or** test mode.
|
|
36
40
|
|
|
37
41
|
### Data model
|
|
38
42
|
`TOGaDeskSupport.people` — `email` (UNIQUE), `password` (bare SHA-1), `adPeopleId`, `ldap_user`, `clientid`, `roleid`, `type`, `approved`, `isActive`, `lastlogin`. `Vision.FeatureFlags` — the `ACTIVE_DIRECTORY_LOGIN` row (id 7).
|
|
@@ -47,12 +51,22 @@ It is **not reachable in production** — `ACTIVE_DIRECTORY_LOGIN` has been on s
|
|
|
47
51
|
### `people.lastlogin` is written by togadesk ONLY — never by togaview
|
|
48
52
|
TOGa View's `?hash=` handoff block and `mvc/login/post.php` populate the session and never touch `lastlogin`. So:
|
|
49
53
|
- A NULL / stale `lastlogin` does **not** mean the account is unused — a portal user may log in daily and never move the column.
|
|
50
|
-
- `lastlogin` measures **TOGa Desk staff** activity, full stop. Any query that uses it to size portal adoption is wrong. (This invalidated a mid-session conclusion during TRUE-81051; an easy analysis trap to fall into twice.)
|
|
54
|
+
- `lastlogin` measures **TOGa Desk staff** activity (password or SSO), full stop. Any query that uses it to size portal adoption is wrong. (This invalidated a mid-session conclusion during TRUE-81051; an easy analysis trap to fall into twice.)
|
|
51
55
|
|
|
52
56
|
### Client-user logins are local-password logins even with AD on
|
|
53
57
|
The AD branch's name suggests client users authenticate against a directory. They do not — they land on the local SHA-1 check. The AD path serves Agilant staff (`clientId = 2`) only.
|
|
54
58
|
|
|
59
|
+
### Dev login bypass is on in every `test_mode=1` environment
|
|
60
|
+
`devLoginBypassAllowed()` = dev OR test mode, so alpha/beta/stage/demo/hotfix all accept a staff login without the AD password. Pre-existing. Confirm those hosts are not public.
|
|
61
|
+
|
|
62
|
+
### Password login still bypasses Entra MFA
|
|
63
|
+
With SSO live, staff can still sign in with AD/local password on the main host and skip MFA. Possible later step: reject password login for AD-linked staff in prod. Not done.
|
|
64
|
+
|
|
55
65
|
## Related
|
|
66
|
+
- [Staff SSO login](staff-sso-login.md)
|
|
56
67
|
- [Per-client hostname restriction](per-client-host-restriction.md)
|
|
57
68
|
- [TOGa View login flows](../../togaview/features/login-flows.md)
|
|
58
69
|
- [TOGa Desk architecture](../architecture.md)
|
|
70
|
+
|
|
71
|
+
## Change history
|
|
72
|
+
- 2026-10-02 — Linked the new staff SSO path (TRUE-78064); recorded dev-bypass-in-test-mode and MFA-bypass gotchas (mhammontree)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Staff SSO Login (Sso class via saml.togahub.com)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: togadesk
|
|
5
|
+
project: TOGa Desk
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: draft
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: ["mhammontree"]
|
|
11
|
+
files:
|
|
12
|
+
- desk/includes/classes/class.sso.php
|
|
13
|
+
- desk/includes/loader.php
|
|
14
|
+
- desk/includes/controllers/general.php
|
|
15
|
+
- desk/template/signin.php
|
|
16
|
+
- template/pages/dashboard.php
|
|
17
|
+
- config.prod.ini
|
|
18
|
+
- dbchanges2/Core/2026-10-02a - TogaDeskSsoDomain.sql
|
|
19
|
+
related:
|
|
20
|
+
- ./staff-login-authentication.md
|
|
21
|
+
- ../architecture.md
|
|
22
|
+
- ../../tools/features/saml-sso-auth.md
|
|
23
|
+
- ../../library/features/app-sso-initiation.md
|
|
24
|
+
- ../../../2.0/apps/saml/features/downstream-integration-contract.md
|
|
25
|
+
- ../../../2.0/apps/saml/features/acs-assertion-trust-gaps.md
|
|
26
|
+
- ../../../clients/true/profile.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
TOGa Desk staff "Sign in with SSO" via saml.togahub.com (TRUE-78064, `Sso` class, Tools pattern); open before touching SSO login, the `?saml=` return, the `[saml]` config, or the TogaDesk Core.Domains row.
|
|
30
|
+
|
|
31
|
+
## Summary
|
|
32
|
+
TRUE-78064 replaces Active Directory for **TOGA staff** with SSO through `saml.togahub.com`, copying the Tools `App_Sso` pattern. Status 2026-10-02: written + locally tested, **not committed or deployed**; the return leg can only be fully tested in prod (needs the prod Core.Domains row + prod key).
|
|
33
|
+
|
|
34
|
+
Decisions (Mark + Brandon/call center, 2026-10-02):
|
|
35
|
+
- Staff only. Matched to an existing `TOGaDeskSupport.people` row by email. SSO **never creates** people rows — no row = cannot log in.
|
|
36
|
+
- **Prod only.** Only `config.prod.ini` has `[saml]`; beta/stage/dev have no SSO.
|
|
37
|
+
- No SSO-only page and no auto-redirect: a **"Sign in with SSO" button** on the normal login. TogaDesk cannot detect a Microsoft session itself; staff already signed in to Microsoft pass straight through after one click (same as Tools).
|
|
38
|
+
- Clients have **no** SSO — only custom-URL (host-based) portals. Their login does not change. Password form stays for clients, staff fallback, and the dev bypass.
|
|
39
|
+
- Prod host: `togadesk.togatech.com`.
|
|
40
|
+
|
|
41
|
+
## How it works
|
|
42
|
+
1. **Start:** button → `/desk/?route=signin&sso=1` → `general.php` → `Sso::initiate()`: stores `{nonce,time}` in `$_SESSION`, calls `App_Sso::initiate()` with `[saml]` config + the current `API_SECRET_ACCESS_TOKEN` read from 2.0 `Core.Parameters` over `db_toga2core`; nonce goes in `urlParameters`.
|
|
43
|
+
2. **Return:** gateway redirects to `<Core.Domains.domain>?saml=<payload>&urlParameters=<nonce>`. `loader.php` sets `$route = "sso"` when `?saml=` is present, `route` is empty, and `Sso::isAvailable()`; route `sso` skips `isSignedIn()` and the `$liu` lookup.
|
|
44
|
+
3. **Consume** (`Sso::consume()`):
|
|
45
|
+
- nonce check: `hash_equals`, single use, 300 s max age (= gateway TTL); payload length cap 8192; `is_string` guards.
|
|
46
|
+
- base64 decode (a `+` arrives as a space → `strtr` back).
|
|
47
|
+
- decrypt client + user with current → previous key. Check IV length first: `App_String::decryptWithKey` warns on short input, and a 1.0 warning ends the request.
|
|
48
|
+
- client uuid must equal `[saml] true_client_uuid`; load `Client_True.Users` email by uuid, `isActive=1` (`db_true`).
|
|
49
|
+
- email must match **exactly one** `db_common` AD People row (`EmailAddresses`) — the same staff test the AD login uses.
|
|
50
|
+
- find `TOGaDeskSupport.people` by any AD alias OR `adPeopleId`, `isActive=1`, `clientid` NULL or not Office Depot; prefer `@togatech.com`, then lowest id. Set `adPeopleId` only if no other row holds it.
|
|
51
|
+
- `session_regenerate_id` (fail if it cannot), write `people.sessionid` / `lastlogin` by id, set `userLabel`, `logSystem`, `Referrer-Policy: no-referrer`, redirect `?route=dashboard`.
|
|
52
|
+
- Any failure: `logSystem` the reason (never the payload), `setStatus(1200)`, redirect `?route=signin`, `exit`.
|
|
53
|
+
4. **Safe to send headers:** unlike Tools, `App_FrameworkIndex_TOGaDesk::render()` emits nothing, so `general.php` runs before any output. No `headers_sent()` workaround needed.
|
|
54
|
+
5. **`Sso::isAvailable()`** = every `[saml]` key set (`idp_single_sign_on_service_url`, `idp_entity_identifier`, `assertion_consumer_service_url`, `domain_uuid`, `true_client_uuid`, `return_url`), `[database_true]` + `[database_toga2core]` hostnames set, and `HTTP_HOST` == host of `[saml] return_url`. It does **not** call `getRestrictedClient()` (the root portal also calls it). So client custom-URL hosts never show the button.
|
|
55
|
+
6. **Buttons** on both the root portal login card (`template/pages/dashboard.php` — the page staff land on at `/`) and `/desk/?route=signin` (where failures return).
|
|
56
|
+
|
|
57
|
+
### Core.Domains row (`dbchanges2/Core/2026-10-02a - TogaDeskSsoDomain.sql`)
|
|
58
|
+
`uuid d1e526b9-e2fb-4462-935f-ed449e8f5986` (= `[saml] domain_uuid`), `clientId 1` (True), `appId 18` (togadesk-1, from a historic seed — **confirm in prod before running**), `environmentId 1`, `domain https://togadesk.togatech.com/desk/`. Unqualified `Domains`, guarded `WHERE NOT EXISTS` on uuid OR (clientId, appId, environmentId). Domain must have **no query string** — see the [gateway contract](../../../2.0/apps/saml/features/downstream-integration-contract.md).
|
|
59
|
+
|
|
60
|
+
### Kill switch
|
|
61
|
+
Set `[saml] domain_uuid = ""` in `config.prod.ini` and deploy → button hidden, SSO off, password login unaffected.
|
|
62
|
+
|
|
63
|
+
## Gotchas
|
|
64
|
+
- **Root-tree class loading differs local vs prod.** The root portal page needs `Sso`, but the autoloader quirk (see [architecture](../architecture.md)) loads root `includes/classes` locally. `template/pages/dashboard.php` does `if (!class_exists('Sso')) require_once __DIR__ . '/../../desk/includes/classes/class.sso.php';` — keep it.
|
|
65
|
+
- **⚠ SSO inherits the gateway's forged-login hole.** `/acs` does not verify the IdP signature, so anyone can forge a login for any staff email. Same exposure as Tools. See [acs trust gaps](../../../2.0/apps/saml/features/acs-assertion-trust-gaps.md). Not fixed; Mark to raise with Jeff/Rohan.
|
|
66
|
+
- **Handoff replay.** The payload has no issue time or single-use id. The nonce blocks login CSRF, but a leaked `?saml=` URL can be replayed by someone who starts their own SSO inside the window. App fix: a used-payload table; gateway fix: nonce + iat + MAC inside the payload. Not fixed.
|
|
67
|
+
- **⚠ `config.prod.ini` may be web-readable.** Docroot is the repo root and there is no `FilesMatch` deny for `*.ini`. The committed file holds plaintext DB/SMTP creds and now also `[database_true]` and `[database_toga2core]` — the latter can read the master SSO key for every app. Use read-only DB users / EB env vars. Not fixed.
|
|
68
|
+
- **SSO does not force MFA.** Staff can still use AD/local password on the main host and skip Entra MFA. Possible later step: reject password login for AD-linked staff in prod.
|
|
69
|
+
- **Local testing ends at the gateway.** Gateway "Invalid SAMLResponse (Code = 2)" locally = local `Core_2.Parameters` key differs from prod. Expected; not a code bug.
|
|
70
|
+
|
|
71
|
+
## Change history
|
|
72
|
+
- 2026-10-02 — Created: staff SSO button + `Sso` class, prod-only, staff matched by email, no auto-create (TRUE-78064, not yet deployed) (mhammontree)
|
|
@@ -6,8 +6,8 @@ project: Tools
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal, ajean]
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: [jcardinal, ajean, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- tools/_/app/auth.php
|
|
13
13
|
- tools/mvc/sso/initiate/get.php
|
|
@@ -24,6 +24,7 @@ related:
|
|
|
24
24
|
- ../../library/features/app-sso-initiation.md
|
|
25
25
|
- ../../../2.0/apps/saml/features/downstream-integration-contract.md
|
|
26
26
|
- ../../../clients/true/features/users-personas-data-model.md
|
|
27
|
+
- ../../togadesk/features/staff-sso-login.md
|
|
27
28
|
---
|
|
28
29
|
|
|
29
30
|
Tools `App_Auth`: consumes the 2.0 SAML gateway `?saml=` handoff, establishes its own session, and gates pages by Client_True persona; open for the SSO legs, the `Core.Parameters` runtime key sourcing, or the headers-already-sent/persona-snapshot gotchas.
|
|
@@ -67,7 +68,7 @@ The shared secret is **rotated regularly**, so any static `config.*.ini [saml]`
|
|
|
67
68
|
`mvc/login/post.php` permits **email-only** login **only** when config `[internal] dev_mode` is truthy **and** `App_Registry::inDevMode()` — both must hold. When used it writes a `SECURITY` line to `error_log`.
|
|
68
69
|
|
|
69
70
|
### One-step sign-in
|
|
70
|
-
Home route `/` (`mvc/get.php`), when not logged in, does `App_MVC::routeTo('/login'); return
|
|
71
|
+
Home route `/` (`mvc/get.php`), when not logged in, does `App_MVC::routeTo('/login'); return;`. `/login` (`mvc/login/get.php`) renders only a **"Sign in with SSO" button** (+ the dev bypass form in dev). Tools does **not** auto-detect an existing Microsoft session — the user clicks once; if already signed in to Microsoft they pass straight through. Verified on `origin/_production` 2026-10-02. TOGa Desk copies this model — see [TOGa Desk staff SSO](../../togadesk/features/staff-sso-login.md).
|
|
71
72
|
|
|
72
73
|
### Crypto format (`App_String::encryptWithKey` / `decryptWithKey`)
|
|
73
74
|
`library/app/string.php:1075-1094`, AES-256-CBC: `encryptWithKey` returns `base64( iv . openssl_encrypt(..., flag 0) )` — i.e. a base64 envelope whose body is the raw IV followed by openssl's own base64 ciphertext; `decryptWithKey` reverses it. **Interoperable byte-for-byte with the 2.0 `_String::decryptWithKey()`** that the SAML gateway uses to produce the handoff token. The 2026-06-30 login failure decoded to the exact expected iv+base64 shape, proving the issue was a stale **key**, not a cipher/format mismatch.
|
|
@@ -95,3 +96,7 @@ The platform's rotating key/value parameter store, on the **2.0 PROD Core DB** (
|
|
|
95
96
|
- [App_Sso initiation (library)](../../library/features/app-sso-initiation.md)
|
|
96
97
|
- [SAML downstream integration contract (2.0)](../../../2.0/apps/saml/features/downstream-integration-contract.md)
|
|
97
98
|
- [True users/personas data model](../../../clients/true/features/users-personas-data-model.md)
|
|
99
|
+
- [TOGa Desk staff SSO (same pattern)](../../togadesk/features/staff-sso-login.md)
|
|
100
|
+
|
|
101
|
+
## Change history
|
|
102
|
+
- 2026-10-02 — Clarified sign-in is one click on an SSO button; no auto-detect of a Microsoft session (mhammontree)
|
|
@@ -6,8 +6,8 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["rgirish"]
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: ["rgirish", "mhammontree"]
|
|
11
11
|
files:
|
|
12
12
|
- saml/Controller/Index.php
|
|
13
13
|
- _underscore/String.php
|
|
@@ -49,6 +49,11 @@ Git history: the block arrived in commit `4bcee38` "Final changes for Compass SA
|
|
|
49
49
|
**email only**, with no issuer check. So any IdP the gateway already trusts (Compass, Prudential,
|
|
50
50
|
Office Depot) can assert a `@togatech.com` email and be handed the internal user.
|
|
51
51
|
|
|
52
|
+
**Affected consumers:** every app that trusts a True login from the gateway — Tools and TOGa Desk
|
|
53
|
+
staff SSO ([Tools](../../../1.0/apps/tools/features/saml-sso-auth.md),
|
|
54
|
+
[TOGa Desk](../../../1.0/apps/togadesk/features/staff-sso-login.md)). A forged assertion for any
|
|
55
|
+
staff email signs that person in to both.
|
|
56
|
+
|
|
52
57
|
The per-client IdP verification certs already exist in source as base64 blobs (Compass, Office
|
|
53
58
|
Depot, Prudential, RUMCSI, Agilant) — they are what a re-enabled verifier would load. Verification
|
|
54
59
|
is per-client; each IdP's signing cert must match its own client.
|
|
@@ -63,7 +68,14 @@ stop replay on its own. There is also **no nonce and no single-use marking** —
|
|
|
63
68
|
presented any number of times.
|
|
64
69
|
|
|
65
70
|
**Fix direction:** AES-256-GCM, or an HMAC verified with `hash_equals()` **before** decrypting,
|
|
66
|
-
plus a server-side single-use nonce.
|
|
71
|
+
plus a server-side single-use nonce.
|
|
72
|
+
|
|
73
|
+
### Gap 3 — the `?saml=` handoff is replayable
|
|
74
|
+
|
|
75
|
+
The handoff payload carries no issue time and no single-use id. TOGa Desk's session nonce blocks
|
|
76
|
+
login CSRF, but a leaked `?saml=` URL can be replayed by someone who starts their own SSO inside
|
|
77
|
+
the window. App-side fix: a used-payload table. Gateway-side fix: nonce + iat + MAC inside the
|
|
78
|
+
payload. Changing `encryptWithKey()` affects every caller — it is also
|
|
67
79
|
the `?saml=` handoff and the api2 encrypted-user-uuid auth, so this is not a local change.
|
|
68
80
|
|
|
69
81
|
## Gotchas
|
|
@@ -77,4 +89,5 @@ the `?saml=` handoff and the api2 encrypted-user-uuid auth, so this is not a loc
|
|
|
77
89
|
citing this doc as sign-off.
|
|
78
90
|
|
|
79
91
|
## Change history
|
|
92
|
+
- 2026-10-02 — Added Tools + TOGa Desk as affected consumers; recorded handoff replay (Gap 3) (mhammontree)
|
|
80
93
|
- 2026-09-25 — Created; recorded that `/acs` verifies no IdP signature and no Audience/Destination/InResponseTo/NotBefore/NotOnOrAfter, and that RelayState uses unauthenticated AES-256-CBC with an attacker-supplied IV (rgirish)
|
|
@@ -6,8 +6,8 @@ project: SAML SSO Gateway
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-10-02
|
|
10
|
+
owners: [jcardinal, mhammontree]
|
|
11
11
|
files:
|
|
12
12
|
- saml/Controller/Index.php
|
|
13
13
|
- _underscore/Model/Core/ClientAuthentication.php
|
|
@@ -16,6 +16,9 @@ related:
|
|
|
16
16
|
- ../architecture.md
|
|
17
17
|
- ../../_underscore/architecture.md
|
|
18
18
|
- ../../../clients/rate/features/saml-sso.md
|
|
19
|
+
- ./acs-assertion-trust-gaps.md
|
|
20
|
+
- ../../../1.0/apps/tools/features/saml-sso-auth.md
|
|
21
|
+
- ../../../1.0/apps/togadesk/features/staff-sso-login.md
|
|
19
22
|
---
|
|
20
23
|
|
|
21
24
|
Integration recipe for a downstream TOGa app to authenticate users via the SAML gateway (`saml.togahub.com`); open when wiring a new SSO-consuming app.
|
|
@@ -45,7 +48,14 @@ where the JSON is:
|
|
|
45
48
|
"payload": { "client": "encryptWithKey(client.uuid)", "user": "encryptWithKey(user.uuid)" }
|
|
46
49
|
}
|
|
47
50
|
```
|
|
48
|
-
all encrypted under the Core `API_SECRET_ACCESS_TOKEN`.
|
|
51
|
+
all encrypted under the Core `API_SECRET_ACCESS_TOKEN`. `urlParameters` from RelayState is forwarded back.
|
|
52
|
+
|
|
53
|
+
Exact build (`saml/Controller/Index.php` ~L459): `$domain->domain . '?saml=' . $authPayload . '&urlParameters=' . …`. So:
|
|
54
|
+
- The `Core.Domains.domain` value **must have no query string** — the gateway appends `?saml=` blindly.
|
|
55
|
+
- The payload is base64 but **not urlencoded** — a `+` arrives as a space; the consumer must `strtr(' ', '+')` before decoding.
|
|
56
|
+
- `Core.Domains` has UNIQUE(clientId, appId, environmentId) and a unique `domain`, so an app gets **one return URL per client + app + environment**.
|
|
57
|
+
|
|
58
|
+
Known 1.0 consumers (all True staff): [Tools](../../../1.0/apps/tools/features/saml-sso-auth.md), [TOGa Desk](../../../1.0/apps/togadesk/features/staff-sso-login.md).
|
|
49
59
|
|
|
50
60
|
The downstream app then base64-decodes the `saml` param, JSON-parses it, decrypts the client and user UUIDs with `_String::decryptWithKey()`, loads the user, and **establishes its own session**. The gateway holds no session — session ownership is entirely the downstream app's.
|
|
51
61
|
|
|
@@ -59,9 +69,14 @@ The downstream app then base64-decodes the `saml` param, JSON-parses it, decrypt
|
|
|
59
69
|
|
|
60
70
|
- **Lazy-transaction write-drop.** `_Database::register()` auto-starts a lazy transaction (since Apr 2026). Any `/acs` write path (e.g. user auto-creation) must call `_Database::transactionCommit()` before exit or the writes are silently rolled back.
|
|
61
71
|
- **Gateway holds no session** — every downstream app must build and own its own session from the decrypted handoff.
|
|
72
|
+
- **"Invalid SAMLResponse (Code = 2)"** from the gateway = RelayState could not be decrypted with Core `API_SECRET_ACCESS_TOKEN` / `_PREVIOUS`. Seen when testing from local: local `Core_2.Parameters` key ≠ prod key. A full return-leg test needs the prod `Core.Domains` row + a deploy.
|
|
73
|
+
- **The handoff is not replay-safe** — no issue time or single-use id. See [acs trust gaps](./acs-assertion-trust-gaps.md).
|
|
62
74
|
|
|
63
75
|
## Related
|
|
64
76
|
|
|
65
77
|
- [saml Architecture](../architecture.md)
|
|
66
78
|
- [_underscore Architecture](../../_underscore/architecture.md)
|
|
67
79
|
- [Rate SAML SSO](../../../clients/rate/features/saml-sso.md)
|
|
80
|
+
|
|
81
|
+
## Change history
|
|
82
|
+
- 2026-10-02 — Documented exact return-URL build (no query string in domain, unencoded base64), one domain per client+app+env, Code 2 meaning, TOGa Desk as consumer (mhammontree)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -8,7 +8,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
8
8
|
- **worker** (Worker) — 42 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
11
|
-
- **togadesk** (TOGa Desk) —
|
|
11
|
+
- **togadesk** (TOGa Desk) — 15 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
12
12
|
- **togaview** (TOGa View) — 8 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
|
|
13
13
|
- **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
|
|
14
14
|
- **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
|
|
@@ -11,17 +11,19 @@ apps:
|
|
|
11
11
|
- togatech
|
|
12
12
|
- worker
|
|
13
13
|
- library
|
|
14
|
+
- togadesk
|
|
14
15
|
project: _Underscore
|
|
15
16
|
client: true
|
|
16
17
|
type: profile
|
|
17
18
|
status: active
|
|
18
|
-
updated: 2026-
|
|
19
|
-
owners: [jcardinal, kyalamarthi, tcox, rgirish]
|
|
19
|
+
updated: 2026-10-02
|
|
20
|
+
owners: [jcardinal, kyalamarthi, tcox, rgirish, mhammontree]
|
|
20
21
|
files: []
|
|
21
22
|
related:
|
|
22
23
|
- clients/true/features/users-personas-data-model.md
|
|
23
24
|
- ../../2.0/apps/saml/features/gateway-environments-and-redirect-safety.md
|
|
24
25
|
- ../../2.0/apps/_underscore/features/core-domains-app-host-registry.md
|
|
26
|
+
- ../../1.0/apps/togadesk/features/staff-sso-login.md
|
|
25
27
|
---
|
|
26
28
|
|
|
27
29
|
"True" — TOGa's internal/staff tenant (`Client_True`, identifier `True`, default email SSO mapper); open for its internal apps (Tools, sprint dashboard, togatech site) and its Talos-pricing internal-tenant role.
|
|
@@ -40,6 +42,8 @@ The public **TOGA Technology website** (repo `togatech`, standalone React SPA, a
|
|
|
40
42
|
|
|
41
43
|
The **Tools** app (1.0; repo `tools`) authenticates True users via **SSO** through `saml.togahub.com` and reads the `Client_True` DB **read-only** to gate its internal tooling by staff persona (see `1.0/apps/tools/`). Its gateway return domain is registered in `Core.Domains` (`uuid 2927bc15-e347-4358-a430-fb28f9446d27`, `clientId 1`, `appId 30`, env 1, `https://tools.togatech.com/sso`).
|
|
42
44
|
|
|
45
|
+
**TOGa Desk** (1.0; repo `togadesk`) staff sign in via the same True SSO (TRUE-78064, prod only, not deployed as of 2026-10-02): True user email → one AD People row → existing `TOGaDeskSupport.people` row; no auto-create. Domain row `uuid d1e526b9-e2fb-4462-935f-ed449e8f5986`, `clientId 1`, `appId 18`, env 1, `https://togadesk.togatech.com/desk/`. See [TOGa Desk staff SSO](../../1.0/apps/togadesk/features/staff-sso-login.md).
|
|
46
|
+
|
|
43
47
|
## ⚠ "Base" maps to Super User in Client_True
|
|
44
48
|
|
|
45
49
|
In `Client_True` the role **named `Base`** maps to **`coreRoleId` 3 = Super User**. Every other
|
package/package.json
CHANGED