# 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.