dorfteich/docs/architecture/adr/0021-external-authentication.md
Claude Opus 5 fd07f716f6
All checks were successful
CI / Lint, typecheck, test (pull_request) Successful in 4m42s
CI / Build container images (pull_request) Successful in 1m11s
CI / Auth e2e pack (pull_request) Successful in 7m47s
CI / Import/export fidelity gate (pull_request) Successful in 55s
CD / Build and push images (push) Successful in 18s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m16s
CD / Promote to Int (push) Successful in 11s
CI / Lint, typecheck, test (push) Successful in 4m50s
CI / Build container images (push) Has been skipped
CI / Auth e2e pack (push) Successful in 7m38s
CI / Import/export fidelity gate (push) Successful in 56s
docs: VS-NfD readiness planning (ist-aufnahme, plan, ADRs 0019-0026, issue drafts)
Add docs/vs-nfd/: the analysis brief, the as-is assessment (42 findings,
all verified against the code), the prioritized action plan rev. 2 with
issue references written back to every checkbox, the two-stage issue/ADR
brief, and the full reviewed draft used to create the forge state.

Add eight proposed ADRs 0019-0026 covering the VS-NfD architecture
decisions: no security base functions (par. 52 VSA anchor), HKDF token
key separation, external authentication, page classification, read-access
audit trail (variant A), reproducible offline deployment, plugin trust
model, and backup target restriction.

Forge state created alongside this commit: 11 labels, milestones M24-M31,
issues #188-#236 (docs-only change, no code touched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168Ph5uBmHm8X28CSVpbpnJ
2026-07-30 01:48:05 +02:00

3.3 KiB

ADR 0021: External authentication via OIDC; local passwords optional

  • Status: proposed
  • Date: 2026-07-29

Context

ADR 0007 established sessions and identities with OIDC in mind: UserIdentity.provider is documented as "password" today and "oidc:<issuer>" later, with @@unique([provider, subject]) already in place. No OIDC code exists — the readiness is structural only.

Authentication is a security base function under §52 VSA (ADR 0019), so it belongs to the operator's platform. An authority environment additionally brings its own account lifecycle: joiners, movers and leavers are managed in the IdP, and a second account store inside the application would drift from it.

Some environments terminate authentication at the perimeter instead and expect the application to trust a header or a client certificate.

Decision

  1. OIDC Authorization Code with PKCE is the primary path, configured by discovery, validated against JWKS. Keycloak is the reference IdP we verify against; nothing in the implementation is Keycloak-specific.
  2. Identities use the existing slot: provider = "oidc:<issuer>", subject from the token. Linking an OIDC identity to an existing local user follows an explicit, documented rule — never silently by e-mail address, which would be an account-takeover path.
  3. Local authentication is switchable off in full, via auth.local.enabled = false. "In full" means every credential-issuing flow: password login, self-service signup, password reset, verification-as-login, and the token flows (PAT, feed tokens). A half-closed local path makes the operating concept untrue, which is worse than not closing it.
  4. Proxy header and mTLS are a supported alternative path, off by default. When enabled they require an allowlist of trusted peers; a request carrying the header from an untrusted peer is rejected and audited. The trust boundary is stated explicitly in the security documentation.
  5. Claims map onto the existing permission model declaratively, and mapped grants are written through the same service path as manual ones so the permission cache stays correct. The application gains no second authorization model.
  6. No MFA, no password policy engine of our own (ADR 0019). Both are the IdP's.

Consequences

  • Bootstrapping needs a documented answer: the first-run wizard creates a local admin, so either it stays exempt with a stated compensating control, or setup itself runs against the IdP. The choice is recorded in #216.
  • Whether the switch is deploy-level or runtime matters: a runtime setting can be flipped back by a compromised Site-Admin. If it stays runtime, that residual risk goes into #231.
  • Existing password hashes remain in the database after the switch. Their deletion is out of scope and, being Argon2id, they are not a confidentiality problem — but the fact is documented.
  • Session handling is unchanged: OIDC produces a session through the same service, so there is exactly one session mechanism (see #190 for its bounds).
  • SAML and LDAP stay out. OIDC plus proxy/mTLS covers the environments we target; adding SAML would be a new decision.

Implementing issues

#214 (OIDC + PKCE), #215 (proxy header / mTLS), #216 (auth.local.enabled), #217 (claim mapping). Depends on #188 for the vetted JWT implementation.