toga-ai 1.0.936 → 1.0.938

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  | Doc | Summary |
4
4
  |-----|---------|
5
- | [TOGa Desk Architecture](architecture.md) | Staff-facing 1.0 (App_) support desk + field-service platform (analysts at `/desk/`); open for the request pipeline, domain-class layer, MSP data model, crons/i |
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-09-25
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
- Staff-facing 1.0 (App_) support desk + field-service platform (analysts at `/desk/`); open for the request pipeline, domain-class layer, MSP data model, crons/integrations, or system topology.
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:** the live application root is **`desk/`** — the root-level legacy top-level code is dead and must never be extended (e.g. live ticket class is `desk/includes/classes/class.ticket.php`; the root `includes/classes/class.ticket.php` is an ancient variant with random ticket numbers/stale schema). 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.
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`. The repo-root `index.php` / `ajax.php` are thin wrappers around the `desk/` versions.
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-09-16
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` is the only other writer.
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,76 @@
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
+ - dbchanges2/Core/2026-10-02b - TogaDeskSsoDomainReuseRow.sql
20
+ related:
21
+ - ./staff-login-authentication.md
22
+ - ../architecture.md
23
+ - ../../tools/features/saml-sso-auth.md
24
+ - ../../library/features/app-sso-initiation.md
25
+ - ../../../2.0/apps/saml/features/downstream-integration-contract.md
26
+ - ../../../2.0/apps/saml/features/acs-assertion-trust-gaps.md
27
+ - ../../../clients/true/profile.md
28
+ ---
29
+
30
+ 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.
31
+
32
+ ## Summary
33
+ TRUE-78064 replaces Active Directory for **TOGA staff** with SSO through `saml.togahub.com`, copying the Tools `App_Sso` pattern. Status 2026-10-02: committed on branch `TRUE-78064` (togadesk `f6f850af`, dbchanges2 `1338980` + `4b677cb`), prod Core.Domains row in place, **not deployed**. The return leg can only be fully tested in prod (prod key).
34
+
35
+ Decisions (Mark + Brandon/call center, 2026-10-02):
36
+ - Staff only. Matched to an existing `TOGaDeskSupport.people` row by email. SSO **never creates** people rows — no row = cannot log in.
37
+ - **Prod only.** Only `config.prod.ini` has `[saml]`; beta/stage/dev have no SSO.
38
+ - 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).
39
+ - 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.
40
+ - Prod host: `togadesk.togatech.com`.
41
+
42
+ ## How it works
43
+ 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`.
44
+ 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.
45
+ 3. **Consume** (`Sso::consume()`):
46
+ - nonce check: `hash_equals`, single use, 300 s max age (= gateway TTL); payload length cap 8192; `is_string` guards.
47
+ - base64 decode (a `+` arrives as a space → `strtr` back).
48
+ - 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.
49
+ - client uuid must equal `[saml] true_client_uuid`; load `Client_True.Users` email by uuid, `isActive=1` (`db_true`).
50
+ - email must match **exactly one** `db_common` AD People row (`EmailAddresses`) — the same staff test the AD login uses.
51
+ - 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.
52
+ - `session_regenerate_id` (fail if it cannot), write `people.sessionid` / `lastlogin` by id, set `userLabel`, `logSystem`, `Referrer-Policy: no-referrer`, redirect `?route=dashboard`.
53
+ - Any failure: `logSystem` the reason (never the payload), `setStatus(1200)`, redirect `?route=signin`, `exit`.
54
+ 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.
55
+ 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.
56
+ 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).
57
+
58
+ ### Core.Domains row (prod id 261)
59
+ `uuid d1e526b9-e2fb-4462-935f-ed449e8f5986` (= `[saml] domain_uuid`), `clientId 1` (True), `appId 18` (togadesk-1, confirmed in prod), `environmentId 1`, `domain https://togadesk.togatech.com/desk/`. Domain must have **no query string** — see the [gateway contract](../../../2.0/apps/saml/features/downstream-integration-contract.md).
60
+ - `2026-10-02a` (INSERT) inserted **0 rows** in prod: slot (1, 18, 1) was already taken by id 261, an unused seed row `https://devteam.togadesk.worker.agilantsolutions.com` (old uuid `6ec4d305-104f-5c6f-8589-ed4382d8a77c`). It stays as a harmless no-op.
61
+ - `2026-10-02b` UPDATEs id 261 to the uuid + domain above, guarded on old uuid + old domain + client/app/env. Run and verified on prod Core by Mark 2026-10-02. Config `domain_uuid` did not change.
62
+
63
+ ### Kill switch
64
+ Set `[saml] domain_uuid = ""` in `config.prod.ini` and deploy → button hidden, SSO off, password login unaffected.
65
+
66
+ ## Gotchas
67
+ - **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.
68
+ - **⚠ 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.
69
+ - **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.
70
+ - **⚠ `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.
71
+ - **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.
72
+ - **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.
73
+
74
+ ## Change history
75
+ - 2026-10-02 — Domains INSERT was a no-op in prod (slot held by devteam seed row 261); repointed that row via `2026-10-02b`; committed, not deployed (mhammontree)
76
+ - 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-09-16
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;` so the SSO button is the only click — the previous welcome-card "Sign in" → `/login` intermediate step was removed.
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-09-25
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. Changing `encryptWithKey()` affects every caller — it is also
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-09-16
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`. An optional `?urlParameters` is forwarded through.
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) (`Domains_clientId_IDX`, `dbchanges2/Core/HISTORIC/2024/2024-01-01.sql`) 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
 
@@ -54,14 +64,22 @@ The downstream app then base64-decodes the `saml` param, JSON-parses it, decrypt
54
64
  2. Implement/extend `_Model_<Client>_ClientAuthentication::getAuthenticatedSsoUser()` in `_underscore` (the gateway repo is not modified — see the per-client extension pattern in the architecture doc).
55
65
  3. Initiate SSO via `singleSignOnServiceUrl()`.
56
66
  4. Consume the `?saml=` handoff and decrypt the UUIDs with `API_SECRET_ACCESS_TOKEN`.
67
+ 5. Register the return domain in `Core.Domains` — **check the (clientId, appId, environmentId) slot first** (see Gotchas).
57
68
 
58
69
  ## Gotchas
59
70
 
60
71
  - **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
72
  - **Gateway holds no session** — every downstream app must build and own its own session from the decrypted handoff.
73
+ - **"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.
74
+ - **⚠ The Domains slot is often already taken.** Old seed rows `devteam.<app>.worker.agilantsolutions.com` (e.g. ids 259 sync, 260 toga, 261 togadesk) hold the (clientId, appId, environmentId) slot for many apps, and nothing uses them. A guarded `INSERT … WHERE NOT EXISTS` then silently inserts 0 rows. Query the slot first; if a dead seed row holds it, **UPDATE that row** (uuid + domain, guarded on its old uuid + domain) instead of inserting. No FK references `Domains.uuid`, so changing it is safe once no app config uses the old uuid. Example: TOGa Desk `2026-10-02b` ([staff SSO](../../../1.0/apps/togadesk/features/staff-sso-login.md)).
75
+ - **The handoff is not replay-safe** — no issue time or single-use id. See [acs trust gaps](./acs-assertion-trust-gaps.md).
62
76
 
63
77
  ## Related
64
78
 
65
79
  - [saml Architecture](../architecture.md)
66
80
  - [_underscore Architecture](../../_underscore/architecture.md)
67
81
  - [Rate SAML SSO](../../../clients/rate/features/saml-sso.md)
82
+
83
+ ## Change history
84
+ - 2026-10-02 — Gotcha: check/reuse the Domains slot (devteam seed rows) before inserting (mhammontree)
85
+ - 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)
@@ -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) — 14 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
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-09-25
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 id 261 (repointed from an unused devteam seed row) `uuid d1e526b9-e2fb-4462-935f-ed449e8f5986`, `clientId 1`, `appId 18`, env 1, `https://togadesk.togatech.com/desk/` — live in prod Core 2026-10-02. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.936",
3
+ "version": "1.0.938",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",