Skip to content

ADR 004 — Delegated authentication via OIDC (resource server + confidential client) ​

Status ​

Accepted

Context ​

The registry is multi-tenant and needs authentication. Building a local auth system — password store, hashing, resets, MFA, brute-force protection, session security — is a large, security-sensitive surface with no product value here.

Decision ​

Delegate authentication to an external OIDC provider and keep no local password store. The provider issues JWTs; the backend plays two OAuth2 roles:

  • Resource server — every request's JWT is validated against the provider's JWKS. Only four endpoints are public (login URL, logout URL, token, token refresh); everything else requires a valid JWT. CORS is restricted to a configured allowlist (external.cors.urls).
  • Confidential client — for the login/refresh endpoints, the backend brokers the authorization-code and refresh-token exchanges server-side, so the client secret never reaches the browser. The issued tokens are handed to the browser as HttpOnly cookies, and a stateless HMAC-derived CSRF token guards the state-changing calls that ride on them — see Security.

On successful JWT validation, a custom converter maps the token to the local user: it looks the user up by OIDC subject, refuses blocked and anonymized accounts, syncs changed profile fields from the token, and auto-provisions (JIT) a new local user on first login, guarding against a duplicate email.

The provider is configured generically (JWKS / authorization / token / end-session / revocation URIs, client id, secret). The identity adapter package is idp — the provider is provider-agnostic — local development runs Authentik.

Why delegate, and why server-side brokering ​

  • Not worth building or owning. An IdP does credential storage, MFA, and session security better than this codebase would, and removes the highest-risk area from it.
  • Keep the secret off the browser. Brokering the code/refresh exchange in the backend (confidential client) keeps the client secret server-side — a better posture than a public SPA client holding it, which is the main reason a public PKCE client was not used.

Consequences ​

Positive ​

  • No credential storage to secure. Hashing, resets, MFA, and session security are the provider's problem.
  • Single sign-on across the platform; zero-touch onboarding via JIT provisioning.
  • The client secret stays server-side.
  • Authorization stays local — the provider proves who, the backend decides what (ADR 005).

Negative ​

  • Hard dependency on the IdP. If it is down, nobody can log in — an accepted single point of failure, bounded on the backend side by connect/response timeouts on every outbound call so a stalled provider fails fast instead of hanging the request.
  • JIT provisioning needs care — the converter must not create a second account for an existing email, or provision a blocked/anonymized identity.
  • Profile data is a copy synced at login, so it can lag the provider between logins.

Why not build local authentication ​

Full control and no runtime dependency on an external provider — but it re-implements a large security-critical surface and gives up SSO. The maintenance and risk cost outweighs the independence.