4.2 KiB
Decisions — September 2, 2026
One item, and it retires two decided-and-built items rather than amending them.
D15 — Authentication moves to Okta OIDC; D13 and D14 are retired before deployment
- Amends: retires
D13(LDAPS simple bind againstprime.local) andD14(the CLI authenticates against the domain). Both were decided and reaffirmed August 21 2026, built across nine tasks (T10.1–T10.9), and verified against the live domain. Neither reached production. Approved by Nick Siegfried. - Surface:
server/auth.py,server/app.py(login()),html/login.html,html/login.js,html/auth-guard.js.server/ldap_auth.pydoes not carry forward — there is no LDAPS bind in the new design, not even as a fallback. - Wave: 10. The label is reused fresh: the LDAPS work that previously answered to
"wave 10" was built on
feat/ldaps-directory-auth, which is deleted rather than merged, and never appeared inIMPLEMENTATION.md's wave table. It carries no claim on the number.
What D13/D14 were, for the record
The branch carrying them is deleted, not merged, so their decision record
(docs/waves/decisions-2026-08-21.md) no longer exists on any branch. Preserved here so
the reasoning isn't lost along with it:
D13 chose a direct LDAPS simple bind to ldaps://prime.local:636 as the sign-in
mechanism: no password stored, a successful bind was the authentication, accounts were
provisioned just-in-time from the directory, and roles stayed local. D14 moved the CLI
onto the same bind, removing create-admin/create. Both were built, tested
(1284/1288 checks, Aug 24), and screenshotted at 390px and 1440px. Neither ever deployed —
wp.controls.dev still runs the pre-D13 local-password login as of this decision.
The decision
Skip LDAPS entirely. Authentication becomes an Okta OIDC authorization-code flow, replacing local passwords directly — the same full replacement D13 intended, just via Okta instead of a domain bind. No LDAPS bind exists in this design at any point.
Four things carry forward from D13 unchanged, because they were never LDAPS-specific to begin with:
- No password is stored. The app never sees a credential of any kind; Okta owns authentication entirely.
- Accounts are provisioned just-in-time. A first successful Okta sign-in with no
matching local
usersrow creates one, at the default role. The matching logic that was going to key off a directory search instead keys off an OIDC identity claim. - Roles stay local. Okta, and AD behind it, supplies identity only. This app decides
what an identity may do. Restated because it is the one rule the whole access-control
design depends on — see
BL-029and the governance discussion that followed it. - Existing accounts keep their roles on first Okta login, exactly as D13's criterion 4 read for LDAPS.
Why this, and not LDAPS first and Okta second
BL-029 (recorded on this branch as BL-027 before the renumbering forced by main's
independent use of that number) already laid out why OIDC beats the LDAPS bind on three
counts: this app never sees a password, MFA comes from Okta rather than needing to be
built, and the domain-lockout hazard that forced AUTH_MAX_ATTEMPTS down to 2 disappears,
because failed attempts land on Okta rather than on a bind this app makes. D13 was decided
before it was known the company already runs an Okta tenant. Once that was confirmed
(security's scoping reply, September 2026), shipping LDAPS first and replacing it with
Okta days later would mean building and deploying the weaker mechanism on purpose. Going
straight to Okta avoids that.
What still needs answering before this is buildable
Open from the security scoping thread, not yet closed:
- Which OIDC claim carries the AD
sAMAccountNameequivalent (preferred_username,upn, or a custom claim) — asked of security, answer pending. - The exact redirect/callback URI once the hostname situation is reconfirmed
(
https://wp.controls.dev/api/auth/okta/callbackproposed). - The
Business Technology Grouppilot in Okta, requested for initial testing, with normal Okta session/MFA behavior rather than a stricter per-app rule.