Ir al contenido
↑↓Navigate↵SelectescClose

0019 - Deployment-wide OIDC federated sign-in

OIDC authentication decision for console SSO

Esta página aún no está disponible en tu idioma.

  • Status: Accepted
  • Date: 2026-06-03
  • Deciders: nexus core team
  • Tags: auth, oidc, identity, console

Nexus authenticates console users with email/password sessions. Operators running a deployment for a company need their users to sign in with an existing identity provider (Google Workspace, Microsoft Entra ID) and need to control who may create an account. Per-workspace SSO with subdomain routing is a later, subscription-gated capability; the immediate need is a single deployment-wide set of providers and a registration policy.

  • Reuse the existing user, identity, session, membership, and audit primitives rather than building a second provisioning path.
  • Keep secrets and TLS aligned with 0001 - Rust microservices with axum + tonic for backend: rustls with the aws-lc-rs provider, no native-tls/openssl/ring.
  • Configuration must be deployment-wide and operator-controlled now, without precluding per-workspace connections later.
  • The browser must only ever talk to the console origin and the IdP so the session cookie lands on the console as it does for password login.
  • Hand-rolled OIDC client — minimal dependencies, but reimplements JWKS rotation, discovery, and ID-token verification, which is security-sensitive.
  • openidconnect crate, env-driven providers — a maintained library for Authorization Code + PKCE, discovery, and ID-token verification, configured from the environment.
  • Per-workspace DB-backed connections with KMS-wrapped secrets now — the eventual model, but heavier than the current need and entangled with subdomain routing and subscription tiers.

Chosen option: openidconnect crate with env-driven, deployment-wide providers.

The auth service resolves providers from the environment behind an OidcProviderRegistry. All connections share one discovery and PKCE path; a connection’s profile selects only the extra validation applied to the normalized claims. Three profiles exist: google, microsoft, and generic. Each login runs Authorization Code + PKCE: the auth service builds the authorization URL and persists single-use state (hashed, short-TTL) in oidc_login_states; on callback it exchanges the code and uses the connection’s configured claim-source order. The default is a verified ID token (signature, issuer, audience, expiry, nonce). A connection can instead request claims from the discovered UserInfo endpoint with the access token, or list both sources in fallback order. Google requires a verified email; Microsoft requires the tid claim to match the configured tenant. Optional email-domain allowlists are enforced server-side.

generic is the standards-only profile, applying no vendor-specific claim checks, so any conformant issuer is configuration rather than code. Two properties of real issuers are handled explicitly rather than assumed:

  • Email verification is a declared posture. Issuers disagree about email_verified: some assert it, some never emit it, and a self-hosted issuer emits it only when an operator maps it into the token’s attribute contract. A connection therefore declares AUTH_OIDC_<KEY>_EMAIL_VERIFICATION as require (the default, and the only behaviour for google and microsoft) or trust-issuer. trust-issuer accepts the issuer’s asserted address on the same reasoning the microsoft profile already applies to a tenant-bound address, and is accepted only on a generic connection that also states its email-domain boundary as a list or an explicit *. That allowlist is what makes the trust decision reviewable.
  • Trust anchors are operator input. The discovery client trusts only the bundled root set and never reads the system trust store, so an issuer behind a private or enterprise CA is unreachable until its chain is supplied through AUTH_OIDC_CA_BUNDLE_PATH. Supplied roots are added alongside the built-in ones so private-CA and publicly-trusted connections coexist, and an unusable path fails startup rather than degrading to the default roots.
  • Client authentication is a declared method. A connection authenticates the token exchange with AUTH_OIDC_<KEY>_CLIENT_AUTH as client-secret (the default: HTTP Basic with the shared secret) or tls (RFC 8705 tls_client_auth: the deployment-wide client certificate named by AUTH_OIDC_CLIENT_TLS_CERT_PATH/AUTH_OIDC_CLIENT_TLS_KEY_PATH is presented during the TLS handshake, and client_id travels in the request body with no shared secret). The identity is deployment-wide for the same reason the CA bundle is — it is a property of the deployment, not of a connection — and it is only ever sent to an issuer that requests a certificate. A tls connection with a configured secret, or without a configured identity, fails startup. Issuers that publish their mutual-TLS token endpoint under mtls_endpoint_aliases or serve it on a separate port state it through AUTH_OIDC_<KEY>_TOKEN_ENDPOINT, which overrides the discovered token endpoint for the exchange only.

Identity resolution reuses user_identities(issuer, subject): an existing linked identity always logs in; otherwise a verified email may link to an existing user (when AUTH_OIDC_LINK_BY_EMAIL is set) or just-in-time create a passwordless user, gated by AUTH_ALLOW_REGISTRATION (true / invite / false). New users provision through the existing Personal Workspace bootstrap. The session JWT carries amr reflecting the authentication method.

The console proxies the flow: a browser hits a console route that calls the auth service, redirects to the IdP, and the IdP returns to a console callback route that exchanges the result and sets the existing session cookies. The openidconnect crate uses its rustls-tls feature, keeping TLS off native-tls.

  • Operators configure providers and registration policy entirely from the environment; client secrets are not stored in the database for this slice.
  • Federated users and memberships flow through the same primitives as password users, so RBAC, audit, and data-plane authorization are unchanged.
  • OIDC lifecycle steps emit auth.oidc.login_started / login_succeeded / login_denied and auth.identity.linked audit events.
  • Adding an issuer is configuration, not code: a new conformant provider needs a generic connection, not a new profile. Vendor names stay out of OidcProfile.
  • An issuer that requires mutual-TLS client authentication (a common PingFederate posture) is configuration too: tls client auth plus the deployment’s certificate, key, CA bundle, and, where published, the mTLS token endpoint. private_key_jwt remains unimplemented.
  • trust-issuer moves the trust boundary from the token to the operator’s domain allowlist. It is deliberately narrow — generic only, and only with a stated boundary — so it cannot silently become the default posture.
  • Claim-source order is declared per connection. id-token preserves the nonce-bound ID-token flow. userinfo uses the access token at the discovered UserInfo endpoint for identity claims only. A comma-separated list such as userinfo,id-token tries each source from left to right. The access token is never persisted or reused as a Nexus session credential.
  • Group and role claims are not consumed. An issuer’s group membership carries no Nexus authority, so roles remain managed inside Nexus.
  • Per-workspace OIDC connections (DB + KMS-wrapped secrets), subdomain-based SSO entrypoints, SCIM, and SAML remain deferred; the registry boundary lets them be added without changing the protocol code.