Wave 10: task breakdown for the Okta OIDC build

This commit is contained in:
2026-09-02 17:38:35 -07:00
parent 31c548318b
commit 044862acba

55
docs/waves/wave-10.md Normal file
View File

@@ -0,0 +1,55 @@
# 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`, remove the
bcrypt-based `login()`, remove the username/password form. Real deletion, matching
`D15`'s "full replacement," not a toggle or a fallback.
- **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.