OIDC Sign-In Flow

GET /api/v1/auth/oidc/{providerID}/start
GET /api/v1/auth/oidc/{providerID}/callback

Rate limit: 30 requests per minute per IP (burst 10).

Server-side OIDC Authorization Code flow with mandatory PKCE (S256). start stores state/nonce/PKCE-verifier in a signed, HttpOnly, SameSite=Lax cookie (osprey_oidc_state, 10 min, typ-bound JWT that is not accepted as a session token) and issues a 302 to the IdP. callback validates state and nonce, exchanges the code (via an SSRF-guarded HTTP client unless the provider sets allow_private_network), verifies the id_token, JIT-provisions or links the user, resolves the role from the provider's group mapping (highest matched role wins; no match → default_role; empty default → refused), issues the normal session cookies and redirects to /.

The redirect URI is always derived from the auth.external_url system setting — never from request headers.

Failure contract: the callback never renders errors; it redirects to /?auth_error=<code> with a machine code from this closed set: sso_failed, sso_cancelled, invalid_state, invalid_nonce, provider_disabled, provider_error, access_denied, account_disabled, link_required, internal_error. No PII ever rides the redirect.

JIT provisioning: first login creates the user (auth_source = provider type, no password). Username = email claim when present, else the OIDC sub; collisions get a #<provider> suffix. An existing account whose username equals the email claim is linked only when auth.link_by_email is true (default false) — otherwise the login fails with link_required. On every login the role is re-resolved from the IdP groups (the IdP is source of truth for SSO users); the identity metadata (groups, email, last_login_at) is refreshed.