Files
Project-SDE-WP-Suite/docs/waves/wave-10.md
Matt Mabrey c74289aa0d D16: Okta admin bootstrap and break-glass posture; correct T10.4 scope
Decision, raised during T10.4 hazard review:

- Admin bootstrap: manage_users.py moves from creating an admin account to
  promoting an existing one, by username, on a row Okta's JIT provisioning
  (T10.3) already created. Rejected blind account creation — the exact
  OKTA_IDENTITY_CLAIM format is still unconfirmed by security, and a
  hand-typed username that doesn't match it produces an orphaned second
  account instead of promoting the real one. Ongoing (non-bootstrap) admin
  naming needs no new work: html/users.js's existing role dropdown already
  handles it.
- Break glass: none, by design, matching the precedent already on record
  for the abandoned LDAPS design (D13/D14) rather than assumed to carry
  over untested. If Okta is unreachable, the app is unreachable for
  everyone until Okta is restored. Rejected a toggleable emergency local
  login — it would reintroduce the stored credential D15 exists to
  eliminate.

Also corrects T10.4's scope in wave-10.md: hazard review found real call
sites of hash_password/verify_password/password_problem the original
bullet didn't name (create_user(), admin_reset_password(), users.js's
admin forms, browser_check.py/launcher_check.py fixtures), plus a
verification-gate ordering problem (smoketest.py and seed_demo.py
authenticate via POST /api/auth/login, which T10.4 removes, and both are
named explicitly in CLAUDE.md's verification section). Fixed by having
T10.4 switch both scripts to mint a session with auth.create_token()
directly, the same technique browser_check.py already uses, rather than
waiting on T10.7.

D16
2026-09-03 10:35:58 -07:00

4.6 KiB

Wave 10 — Okta OIDC authentication

Fresh wave 10. The label was previously used by the LDAPS work under D13/D14, built on feat/ldaps-directory-auth; that branch was deleted rather than merged and never appeared in IMPLEMENTATION.md's wave table, so it carries no claim on the number. See docs/waves/decisions-2026-09-02.md (D15) for why LDAPS was retired before deployment and Okta chosen instead.

Depends only on main as it stands after D15. Not sequenced behind any other wave.

Tasks

  • T10.1 — Add the Okta OIDC client. Authlib as a dependency. Config via env vars (OKTA_ISSUER, OKTA_CLIENT_ID, OKTA_CLIENT_SECRET, OKTA_REDIRECT_URI), same pattern AUTH_SECRET_KEY already uses in server/auth.py.

  • T10.2 — Login-redirect and callback routes. A route that sends the browser to Okta's authorize endpoint, and a callback route that exchanges the code for tokens and validates the ID token. Access gating is Okta's job, not this app's: only accounts assigned to the app integration in Okta can reach it at all, so there is no app-side required-group or claim check layered on top. This is a deliberate difference from D13, which had to gate on a required AD group itself because an LDAPS bind alone could not distinguish an assigned user from any other domain account.

  • T10.3 — Identity matching and JIT provisioning. Reuses D13's shape (_provision_from_directory-style matching) keyed off an OIDC claim instead of an LDAP search result. Open dependency: which claim carries the AD sAMAccountName equivalent (preferred_username, upn, or a custom claim) is asked of security and not yet answered. Build with a configurable claim name and a documented default, not a hardcoded one, so the answer can drop in without a code change.

  • T10.4 — Remove the local password path entirely. Drop password_hash (Alembic migration; plain op.drop_column, matching existing precedent for other NOT NULL columns on users — no batch_alter_table needed), remove the bcrypt-based login(), remove the username/password form. Real deletion, matching D15's "full replacement," not a toggle or a fallback. Scope corrected by D16 after hazard review turned up more call sites than the original bullet named:

    • create_user() and admin_reset_password() in server/app.py (admin console's "add user" and "reset password" routes) — rework to drop the password field entirely rather than break.
    • html/users.js's "add user" form (nu-password) and "Reset password" button — matching frontend change.
    • server/manage_users.py — reworked per D16 from account creation to promotion: create-admin/create/reset-password (password-based) are replaced by a promote-by-username command that operates on a row Okta's JIT provisioning (T10.3) already created, never a hand-typed new one. This is now the documented admin-bootstrap path — see D16.
    • tests/browser_check.py and tests/launcher_check.py — stop calling auth.hash_password() to seed fixture rows.
    • server/smoketest.py and server/seed_demo.py — currently authenticate via POST /api/auth/login. Switch to minting a session with auth.create_token() directly, the same technique browser_check.py already uses, so both scripts (named explicitly in CLAUDE.md's verification section) keep working without depending on T10.7's timing.
  • T10.5 — Frontend: login becomes a redirect, not a form. login.html/login.js change to a "Sign in with Okta" flow. Sign-out lands back on the app's own login page.

  • T10.6 — Deployment docs and env var reference. DEPLOYMENT.md, server/.env.example, server/README.md describe the Okta config in place of the LDAP config they never ended up describing (D13 never shipped, so these still describe the original local-password system today).

  • T10.7 — Test coverage without a live Okta dependency. A fake-OIDC-provider test seam, mirroring ldap_fake.py, so the suite runs with no live Okta tenant reachable.

  • T10.8 — Verification. 390px and 1440px, full suite, done-when checks per task, matching the rigor D13 was held to.

Still open

  • The OIDC claim mapping (T10.3).
  • Final confirmation of the redirect/callback URI (https://wp.controls.dev/api/auth/okta/callback proposed, pending security).
  • The Business Technology Group pilot assignment in Okta.

Closed since first written: admin bootstrap and break-glass posture, previously open questions, decided in D16 (2026-09-03) and folded into T10.4 above.