Phase 4: real OIDC human login (enterprise/internal/loginhandler)

Closes the other major named gap from this phase: until now, there was
no way for a human to actually log in -- only /alerting's RoleService
credential could be minted. GET /auth/oidc/login and GET
/auth/oidc/callback drive the real coreos/go-oidc flow already wired in
enterprise/internal/oidc: CSRF state in a short-lived cookie, code
exchange, ID token verification, upserting a users row, resolving
tenant/role from exactly one tenant_memberships row (refusing outright
on zero or more than one, rather than guessing), and issuing a real
session cookie.

Unlike everything else built this phase, this one is genuinely verified
end to end: the tests spin up coreos/go-oidc's own oidctest fake IdP,
which signs real RS256 ID tokens, and drive the full login->callback->
session-cookie round trip through actual signature verification -- no
live database or Docker needed, so nothing here is asserted without
having actually been run in this session. Also fixes a real bug caught
while wiring this into enterprise-auth's main.go: assigning a nil
*oidc.Provider to the handler's interface field would have produced a
non-nil interface wrapping a nil pointer (Go's classic typed-nil trap),
silently breaking the "OIDC not configured" no-op path -- New() now
takes the concrete pointer type and checks it before ever converting to
the interface, with a regression test pinning the fix down.

Still missing: SAML's equivalent (ACS endpoint), a tenant-picker UI for
multi-membership identities, and any admin UI to actually create a
tenant_memberships row (today that's manual SQL, documented in the
runbook's new bootstrap walkthrough).
This commit is contained in:
2026-08-13 23:00:35 -07:00
parent 1d57e697b1
commit 1fab02abd5
10 changed files with 752 additions and 50 deletions
+18 -11
View File
@@ -152,17 +152,24 @@ access partway through the phase, so only the audit-logging guarantees
were actually confirmed against a live database; the rest is untested
beyond "compiles, and skips cleanly when no live database is
configured" (see `/docs/phase-4-runbook.md`'s verification-status
section). Two things still keep this phase from being done: SSO login
(OIDC/SAML protocol wiring exists, no
HTTP login handler calls it), and Tantivy/free-text queries have no
per-tenant index routing at all (`enterprise-api` closes the ClickHouse
half of tenant isolation, not the Tantivy half) — plus a deployment gap
worth naming explicitly: nothing yet forces or even flags whether a
given deployment is actually running the isolated binary
(`enterprise-api`) versus the plain single-tenant one (`api`); both
still exist and nothing currently prevents mixing them up. Full
accounting: `/docs/security/threat-model.md`; step-by-step verification
procedure (not yet run against a live cluster in this environment):
section). Human OIDC login is now built too
(`enterprise/internal/loginhandler`: `GET /auth/oidc/login` +
`GET /auth/oidc/callback`, issuing a real session cookie after resolving
tenant/role from `tenant_memberships`) — genuinely verified, unlike the
ClickHouse pieces, via a real fake IdP that signs and verifies actual
RS256 tokens (`loginhandler_test.go`, all passing), though never tried
against a real external IdP or through a running `enterprise-auth`
container. Two things still keep this phase from being done: SAML login
(protocol wiring exists, no ACS handler calls it, following OIDC's now
-built pattern), and Tantivy/free-text queries have no per-tenant index
routing at all (`enterprise-api` closes the ClickHouse half of tenant
isolation, not the Tantivy half) — plus a deployment gap worth naming
explicitly: nothing yet forces or even flags whether a given deployment
is actually running the isolated binary (`enterprise-api`) versus the
plain single-tenant one (`api`); both still exist and nothing currently
prevents mixing them up. Full accounting:
`/docs/security/threat-model.md`; step-by-step verification procedure
(not yet run against a live cluster in this environment):
`/docs/phase-4-runbook.md`. The rest of this section describes the exit
bar this phase is aiming at, not a completed state.