# API-owned login and sessions The optional [Run serving gateway](../../../../docs/decisions/0069-project-private-run-services.md) issues a host-only Run handle through a nonce-bound bootstrap. Every request and open connection revalidates its parent session, Party and AUP through `AuthoriseBrowserFence`. The handle contains no parent login token. See the [activation guide](../../../../gitops/manifests/pharos-site-runtime/serving/README.md). The API implements the [authentication protocols](../../../../contracts/authentication/README.md) behind `API_AUTHENTICATION_ENABLED=true`. The retained web requires this enrolment. The NextAuth [bridge endpoints](../party/README.md) have been retired. Enabling API authentication also closes the isolated development password's browser session issuer; the separately enrolled CLI retains its own technical scope. ## Login and authority The configured OIDC provider validates signature, exact issuer, audience, authorised party, nonce, authentication time and any UserInfo subject. Signed UserInfo additionally binds its own issuer and client audience. Login attempts persist state, browser binding, the exact callback, PKCE material and a fixed expiry before redirecting. A claimed code exchange is never repeated. Interrupted exchanges require fresh login; accepted evidence and registration retries reuse their original command, correlation and signed bytes. New logins first request online authentication, then request the enrolled `offline_access` consent using the resulting browser SSO session. Keycloak 26.1 and later removes an online session when its first exchange requests offline access; completing the online exchange first preserves SSO for GitLab and Chat. Both exchanges validate the complete enrolled identity profile. The consent identity must match the first issuer, subject, stable person ID and Site username. Each leg has distinct state, nonce and PKCE material, with the same callback, browser binding and five-minute attempt expiry. Normal provider SSO supplies the second authorisation without another credential entry; provider consent remains available. Version 3 login attempts checkpoint the verified online identity binding and second-leg challenges before returning the consent redirect. An atomic private Valkey alias resolves the second state to the same attempt. A lost redirect can recover its saved challenges; uncertain exchanges require a fresh login. Cancellation through either state fences both legs. Session issuance and Party registration follow the second verified exchange. Online refresh tokens are discarded, and provider grants remain absent from saved attempts. The existing best-effort offline-grant hand-off follows session creation. Historical version 2 attempts complete using their original one-leg workflow, without rewriting their saved bytes or signed evidence. Session records remain version 2. Old application versions reject new attempt versions; a rollback requires affected in-flight browsers to start a fresh login. Beto's v3 ingress resolves the canonical Party using the exact issuer and stable `voperson_id`. A changed subject requires owner recovery and consumes the login attempt. The API stores a versioned v2 session with Party identity, authentication generation, fixed expiry and an opaque session reference. Its token is derived with a dedicated persistent key and stored by hash. The session contains no provider credential. Version 1 records remain unchanged and convey no authority in API mode. The provider request explicitly includes `openid`, `profile`, `email`, `site_username`, `voperson_id`, `voperson_external_affiliation`, `schac_home_organization` and `entitlements`. Both the ID token and UserInfo profile are validated before merging. Present UserInfo fields override ID-token fields; absent fields retain their token value. Email and its verification flag stay together. Conflicting stable person identifiers reject authentication. `site_username` comes only from UserInfo. Access-token claims supply no identity attributes. Signed v3 evidence carries given and family names, preferred username, home organisation, affiliations and entitlements, alongside the existing display name and verified email. Collections retain absence, explicit emptiness, order and values. Stable person and Site identifiers are separate identity material and do not enter compatibility profile updates. Assertions are limited to 16 KiB in total. Historical v2 checkpoints retain their original JSON, signed bytes and protocol on retry; new production logins always use v3. The callback redirects through a token-free same-origin landing document before entering the application, preserving the Strict session cookie after an external identity-provider redirect. The short-lived Lax callback cookie only identifies the login attempt. Logout cancels any bound attempt, revokes local authority and records credential cleanup and realtime disconnection together. Browser sign-out then redirects through the discovered provider end-session endpoint using the exact registered browser-origin return URL. The UI explains that the provider may ask for confirmation; users must complete that step to end their AAI session. Provider ID tokens remain absent from session storage. Operational logout failures redirect to a fixed same-origin retry page and preserve the browser cookies for retry. The local-only endpoint remains available to retained server callers. A callback replay cannot recreate a revoked session. Every protected operation checks current Party eligibility and generation. The experimental [MCP authentication edge](../../../../contracts/mcp/README.md) adds separately enrolled operation assertions. It shares the current Party and AUP checks and retains the generation captured at OAuth login. Limited sessions allow status, explicit resume, policy presentation/acceptance, logout and their own Party-targeted readiness notifications. Canonical browser operations require current AUP; provider readiness gates the retained operations which use those providers. Normal retained operations require current GitLab-backed AUP and the verified binding and credential readiness they use. Policy reads have their own service credential and never call session validation. ## Compatibility work Session issuance durably schedules the retained compatibility worker. The API dispatches closed account and credential operations with stable identities. Existing account work is found by Party/provider scope. A new authentication generation can update pending work using its current revision. Provider progress is read by the server worker; the browser performs no polling. After each preparation attempt the API worker checks AUP and provider readiness, revalidates session/Party authority and records changes in its owned Valkey state. The observation and exact typed notification bytes commit atomically. A separate relay retries the persisted bytes with the same event identity until acknowledged. Its managed loop drains on shutdown and uses a five-second HTTP timeout; slow publication leaves session expiry and credential cleanup free to progress. Missing publication bodies are removed from the pending index so later messages can drain. The existing preparation queue remains pending while AUP is required/unavailable or provider work is pending/unavailable; terminal provider attention can settle independently once AUP is current. Session expiry/revocation ends observation. Observations are per session; notification revisions are per canonical Party. Notifications contain only that Party ID and invalidate the receiving browser's own session query. They never transfer another session's readiness or grant access. Queries continue to check the owners directly. Additive `v2:readiness:*` keys preserve existing session records and fences; older workers ignore these keys. Pending publications remain available when the new relay resumes. The API connect/refresh proxy admits a valid limited session to its own Party channel, whose registered payload is readiness only. Current AUP additionally permits the private principal channel and current owner-authorised Project audiences, including their absolute authority expiry. Audience changes expire the connection so it reconnects under current authority. The web connects while pending, reads status after every connection and on a readiness notification, and offers an explicit status retry. This covers changes before connection, duplicate delivery and reconnect gaps. A notification is recorded when a preparation retry observes the changed readiness. Retries are at most four seconds apart for about the first three minutes, then a minute apart, so a slow account preparation reaches a waiting page within seconds of completing. Canonical Projects can load as soon as AUP is current, while provider-specific routes retain their gates. The session gateway credential permits only this typed Party notification and session disconnection; it cannot publish business projections. The retained GitLab writer verifies exact federated identity and records a signed ownership fact through Party Governance's private binding ingress. Current verified bindings protect both AUP reads and credential access. Tokens remain in retained private storage and are fenced by the API session before issuance and exposure. The worker reconciles uncertain provider effects and preserves cleanup work across failures. Its [runtime guide](../../../../compat/themelio/services/web/src/lib/api-authentication/README.md) describes the separate process and provider credentials. The isolated Waldur identity adaptor names the account after the verified OIDC subject. It checks the enrolled source provenance before Identity Bridge can update an existing username, reads the result back exactly and returns the UUID for the worker's signed provider-binding hand-off. A separate invitation adaptor uses current Party contact and the established Waldur binding. The general worker and API receive no Waldur staff credential. Without a configured Waldur installation its account work reports `attention_required / proof_unavailable`. GitLab and AUP can progress independently; Waldur-dependent routes wait for the established binding (`provider_pending`). The retired web-side Waldur account and invitation writers are removed. ## Enrolment and cutover Configure these values directly or through their `_FILE` forms: | Setting | Purpose | | --- | --- | | `API_OIDC_ISSUER`, `API_OIDC_CLIENT_ID`, `API_OIDC_CLIENT_SECRET` | Fixed OIDC client registration. | | `API_OIDC_CALLBACK_URLS` | JSON array of exact enrolled public callbacks, each ending in `/auth/oidc/callback`. Each callback origin must also be registered as a `/` post-logout return URI. | | `API_OIDC_AUTH_TIME_REQUIRED` | Defaults to `true`, which keeps OIDC Core's `auth_time` requirement. `false` enrols a provider that omits the claim even under `max_age`, whose logins then prove only that the token was issued now. | | `API_AUTHENTICATION_SIGNING_KEY`, `API_AUTHENTICATION_SIGNING_KEY_ID` | PKCS8 Ed25519 API attester; Beto receives its public key. | | `API_SESSION_DERIVATION_KEY` | Persistent, dedicated 32-byte key encoded as unpadded base64url. | | `THEMELIO_AUTH_ADAPTOR_KEY` | Private legacy session validation/revocation. | | `THEMELIO_COMPAT_FENCE_KEY`, `THEMELIO_COMPAT_EVIDENCE_KEY` | Separate worker fence/evidence reads. | | `THEMELIO_COMPAT_BINDING_READER_KEY`, `THEMELIO_COMPAT_BINDING_WRITER_KEY` | Separate canonical binding read/write relays. | | `THEMELIO_AUTH_POLICY_KEY`, `THEMELIO_COMPAT_DISPATCHER_KEY` | Independent policy read and durable work dispatch. | | `THEMELIO_COMPATIBILITY_URL` | Private HTTPS origin serving compatibility routes. | | `API_COMPATIBILITY_SCOPES` | JSON array declaring one GitLab and one Waldur `{provider,instance,account_scope}`. | Enrol the API attester, provider-binding attester and API provider-read scopes in [Beto](../../../beto/contexts/party_governance/adaptors/messaging/authentication/README.md). All service credentials have distinct authority. The cutover routes browser `/auth/` endpoints to the API on both the retained Themelio origin and the Pharos web origin. The API selects the callback by exact login `Origin`; the selected value stays fixed for the authorisation request and for the code exchange. The existing `pharos` host's `/auth/` route belongs to Keycloak and remains its issuer route. Exclude `/internal/*` from public ingress; compatibility calls use their separately configured private hostname and HTTPS. These routing and TLS requirements need deployment qualification before enabling the new mode. The dev box enrols `https://themelio.localtest.me/auth/oidc/callback` and `https://platform.localtest.me/auth/oidc/callback`. Each host receives its own host-only callback and session cookies while sharing the API session authority. Older Themelio browser hosts redirect to the canonical Themelio origin; a login started there cannot carry its host-only callback cookie across the redirect. The registry entries do not enable API mode: same-origin API routes and the separate private compatibility ingress are composed by the deployment described below. This branch selects API mode for dev-box qualification. Distributed sites use the environment policy in the deployment guide to enrol both public origins and provider scopes together; the default remains legacy until explicitly selected. `API_AUTHENTICATION_RETURN_PATHS` is an explicit comma-separated path allowlist, defaulting to `/`. Isolated HTTP tests additionally require `APP_ENV=local` and `API_AUTHENTICATION_ALLOW_HTTP=true`. The [deployment guide](../../../../gitops/manifests/pharos/api-auth/README.md) describes the coordinated GitOps selection, separate worker, private TLS and credential recovery. Deploy the API authentication edge and retained web together after provider and browser qualification. The retained web requires API-owned authentication and has no NextAuth mode or Party bridge. Existing NextAuth sessions need a new login; there is no v1 import. Credential outages or rejected logins never select a different authentication mode. ## Verification `pnpm verify` includes OIDC fake-provider, application, strict boundary and protocol tests. `python3 scripts/api_native_sessions.py` uses durable Valkey for callback claims, replay, limited sessions, both HTTP auth families, expiry and logout fencing. `python3 scripts/project_broker_integration.py` qualifies retained v2 and complete v3 registration, Site bindings and contact-event delivery with real PostgreSQL/RabbitMQ. `python3 scripts/themelio_party_integration.py` qualifies retained compatibility receipts, leases, provider-effect recovery and terminal credential fences. `python3 scripts/authentication_browser.py` checks the production completion document across separate HTTPS sites in Chromium and Firefox, with JavaScript disabled. The identity-provider page and callback are fixtures in this lane. `pnpm test:authentication:e2e` qualifies the assembled production processes and Chromium signup/resume/AUP journey with real durable stores and fixture OIDC/GitLab upstreams. The deployment guide lists its prerequisites and private TLS lane. `scripts/platform_authentication_live_browser.mjs` qualifies host-local AAI login, Project listing and logout on the Pharos web. Live Keycloak/GitLab/Waldur, full Themelio UAT and coordinated cluster cutover remain separate qualification lanes.