AUTH4D-12 OAuth authorization code and OIDC endpoints

All tenant operations use the tenant ID from a discovery supplied tenant_id endpoint query or form value. The Worker resolves the customer tenant and client from primary-consistent D1 reads; it never derives tenant identity from the request host. A registered redirect URI is compared as the exact request string before the service redirects to it.

Discovery and keys

  • GET /t/{tenantId}/.well-known/openid-configuration
  • GET /t/{tenantId}/.well-known/jwks.json

Discovery returns the tenant issuer {AUTH_ORIGIN}/t/{tenantId}, configured authorization, token, UserInfo and revocation endpoints, the tenant JWKS URL, RS256, S256, and the code response type. It advertises authorization-code and refresh grants only; device authorization is omitted until its endpoint is implemented. The token, authorization, UserInfo, and revocation endpoint URLs include the tenant ID as a query parameter. JWKS contains only public signing-key fields.

GET /oauth/authorize accepts tenant_id, response_type=code, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method=S256, and optional nonce. Duplicate and unsupported parameters fail. The client must be enabled, allow the authorization-code grant, allow every requested scope, and have the exact redirect URI registered. Native clients cannot use this flow. Unsupported response types, including implicit, fail without issuing a code.

The API creates a ten-minute tenant-scoped browser flow, binds it to a random HttpOnly, Secure, SameSite=Lax cookie, and redirects to the hosted login page with a non-secret flow_id. Login providers continue through:

POST /oauth/authorize/continue
Origin: {AUTH_ORIGIN}
Content-Type: application/json
Cookie: __Host-auth4_flow_{flowId}=...

{"tenant_id":"ten_…","flow_id":"flw_…","decision":"approve",
 "proof_id":"prf_…","proof_purpose":"discord_login"}

The continuation consumes the flow and identity proof once. It accepts only email-verification or Discord-login proofs tied to the same tenant, client, browser flow and active user. An existing browser session may supply the identity instead. decision=deny returns access_denied; approval persists the explicit consent scopes and returns the authorization code and original state to the registered redirect URI. Consent and one-time proof values are never accepted as caller-supplied identity claims.

Authorization codes are random, tenant and client bound, expire after 60 seconds, and are stored only as hashes in the atomic state authority. The code is consumed before PKCE verification, so a wrong verifier cannot be retried. Token exchange requires the same exact redirect URI and an RFC 7636 verifier whose S256 digest matches the stored challenge.

Token, UserInfo and revocation

POST /oauth/token accepts URL-encoded authorization-code or refresh-token grants. Public clients identify themselves with client_id; confidential clients use client_secret_basic, and the stored secret hash is checked before the grant is processed. Body-based client secrets are rejected. The device-code grant is delegated to the device authorization feature (AUTH4D-16). Password and all other grants return unsupported_grant_type. Refresh scope overrides are rejected because the session service issues the original granted scope set.

Authorization-code exchange delegates session creation, five-minute RS256 access and ID token issuance, browser-cookie handling, and optional rotating refresh tokens to the session service. The ID token uses the tenant issuer and client audience and carries the authorization request nonce.

GET /oauth/userinfo requires a verified Bearer access token. It returns sub and, only for the email scope, a tenant identity’s verified email claims. Access-token and ID-token validators are separate. POST /oauth/revoke accepts URL-encoded token revocation and returns success for unknown, expired, or already revoked tokens as required by RFC 7009.

POST /oauth/logout is the hosted-login logout extension. It accepts JSON containing the tenant, client, optional signed id_token_hint, and an optional exact registered post-logout redirect URI and state. The request must have the configured auth origin. A matching browser session is revoked and its cookie is cleared; an invalid ID token hint or unregistered redirect is rejected.

Runtime binding

The OAuth feature consumes the session library through a separate SESSION_STATE SQLite Durable Object binding and loads its RS256 signing ring from the AUTH_SIGNING_KEY_RING Worker secret. The current frozen API composition does not yet declare or register SESSION_STATE; the session composition owner must wire SessionStateStore before enabling the token, UserInfo, revocation, and logout paths in a deployed Worker. Missing session authority or signing material fails closed.

Source: docs/api-spec/auth4d-12.md