ADR-0011: Authentication via OIDC SSO with Server-Side (BFF) Sessions¶
- Status: accepted
- Date: 2026-06-17
- Deciders: Nicolas Burri
- Relates to: ADR-0009 (RLS context), GAP-01
- Refined by: ADR-0028 (session held in a stateless encrypted cookie; revocation by per-request re-evaluation — supersedes the "server-side / server-revocable session" wording below); ADR-0033 (the CSRF control is SameSite=Lax + JSON-only content type, not a token filter)
Context¶
LQMS needs authentication. Forces: a regulated QMS (ISO 13485; Part 11 / Annex 11 if ever in scope) makes self-managed credentials a heavy liability; an Angular SPA plus REST API plus future MCP clients must all authenticate; RISK-002 requires each authenticated request to yield the user's authorized scope set so RLS (ADR-0009) can be applied. Stated wishes: SSO against existing identity providers (e.g. Keycloak), and protection against cookie/token theft.
Decision¶
Delegate authentication to an external OIDC provider; LQMS stores no credentials.
- OIDC only, pluggable. Authenticate against any standards-compliant OIDC provider, configured per deployment (a corporate IdP, or a bundled Keycloak for standalone/on-prem installs). LQMS stores no passwords and no second-factor secrets; MFA and password policy are the IdP's responsibility.
- Browser SPA uses a server-side session (BFF). Quarkus performs the OIDC Authorization Code flow with PKCE and holds the tokens server-side; the browser receives only an opaque, httpOnly, Secure, SameSite session cookie.
- Anti-theft posture. The cookie is an opaque, server-side-revocable session reference carrying no extractable identity/authorization claims (strictly safer than a self-contained bearer token). Plus idle + absolute session timeouts, CSRF protection for state-changing requests, and TLS-only transport. Step-up re-authentication for the highest-risk actions is deferred to electronic signatures (GAP-04).
- Authentication ≠ authorization. A successfully authenticated user with no role assignment has no access to any scope's data (deny-by-default, already enforced by RLS). Users are JIT-provisioned on first login (keyed to a stable IdP subject id) so they can later be assigned roles; provisioning grants nothing.
- Authn → authz → RLS bridge. On each authenticated request, LQMS resolves the user's
authorized scope set from role assignments, sets
lqms.authorized_scope_ids, and connects as the RLS-subject rolelqms_app(ADR-0009). This is the design that closes RISK-002. - Machine / MCP clients authenticate via OAuth2 (client credentials / service accounts) with bearer tokens validated against the OIDC provider, subject to the same authorization and RLS.
Options Considered¶
- Bearer tokens in the browser (rejected): tokens in JS-reachable storage are exposed to XSS theft and cannot be revoked before expiry; unacceptable for this data sensitivity.
- Local accounts in LQMS (rejected): credential-management burden (policy, reset, lockout, MFA) and a larger regulatory/security surface, with no corporate SSO.
Consequences¶
- The
apimodule gains OIDC integration (quarkus-oidc, web-app/BFF mode), a server-side session store, and a CSRF filter. SOUP:quarkus-oidc; Keycloak as a deployment dependency for standalone installs. - Every deployment needs an OIDC provider. Standalone/on-prem installs bundle Keycloak — an extra component to operate and include in backups.
- Closing RISK-002 at runtime (connect as
lqms_app, set the scope context per request) is the implementation follow-up to this decision, built with the request-authorization layer. - Logout performs server-side session invalidation and IdP single-logout where supported.
- External users (auditors, customer reps) must be reachable through an OIDC provider (corporate guest accounts or a deployment Keycloak realm) — there is no local-account fallback. Revisit if a real case needs it.
Requirements¶
Introduces STR-016 and the REQ-AUTH group (pending requirements baseline v1.1).