AUTH4D-17 console sign-in and management authentication
The console uses a confidential client in the immutable control-plane realm. Its issuer is fixed
to {AUTH_ORIGIN}/t/control-plane and its audience is urn:auth4:management. The API bootstraps
the client row from CONTROL_PLANE_CONSOLE_CLIENT_ID and
CONTROL_PLANE_CONSOLE_CLIENT_SECRET; a conflicting row or missing control-plane seed fails
closed. Neither value is sent to browser code.
Sign-in protocol
POST {CONSOLE_ORIGIN}/api/auth/start requires an exact same-origin Origin header. The BFF
generates OAuth state and an S256 PKCE verifier, stores them encrypted in its SQLite-backed
ConsoleSessionStore Durable Object, sets a short-lived HttpOnly flow cookie, and redirects to:
GET {AUTH_ORIGIN}/management-auth/authorize?response_type=code&client_id=cli_...&redirect_uri={CONSOLE_ORIGIN}%2Fapi%2Fauth%2Fcallback&scope=tenant%3Aread%20...&state=...&code_challenge=...&code_challenge_method=S256
The API accepts only the configured client, exact callback URI, unique management scopes, and
code_challenge_method=S256. The hosted form posts email-code requests to:
POST /management-auth/email/start
Origin: {AUTH_ORIGIN}
Cookie: __Host-auth4_control_flow=...
Content-Type: application/json
{"flow_id":"flw_...","email":"developer@example.test"}
It returns HTTP 202 with {"expires_in":600} for permitted requests. Verification uses the same
origin and cookie:
POST /management-auth/email/confirm
Origin: {AUTH_ORIGIN}
Cookie: __Host-auth4_control_flow=...
Content-Type: application/json
{"flow_id":"flw_...","email":"developer@example.test","code":"381042"}
A successful verification creates or resolves the control-plane email identity and returns
{"redirect_to":"{CONSOLE_ORIGIN}/api/auth/callback?code=...&state=..."}. The six-digit code
is stored as an HMAC in control-plane Durable Object state, expires after ten minutes, and allows
five attempts. Email-send, IP and per-flow attempt budgets use AtomicStateStore.consumeBudget
with control-plane rate-limit keys whose subjects are HMAC-pseudonymized with AUTH_PRIVACY_KEY
under a management-rate-limit domain separator (the shared createRateLimitKey helper is
customer-realm only by design).
The BFF callback consumes its encrypted pending state once, checks the returned state, and sends
the code and PKCE verifier to POST /management-auth/token through the fixed API service binding.
The token endpoint requires HTTP Basic authentication for the configured confidential client and
accepts only authorization-code and refresh-token grants. It returns an RS256 management access
token (five minutes), a rotating refresh token, and an internal session ID. The BFF stores all
token material only as AES-GCM ciphertext in ConsoleSessionStore SQLite state.
API session routes
These are called only by the BFF over the API service binding; responses are Cache-Control: no-store.
GET /management-auth/currentwithAuthorization: Bearer <management access token>returns{ "user": { "id", "email", "email_verified": true } }, or401 unauthorizedwithWWW-Authenticate: Bearer error="invalid_token".POST /management-auth/revokewith the same bearer token revokes that token’s control-plane session and returns{ "revoked": boolean }; an invalid token returns401 unauthorized.POST /management-auth/revoke-sessionrequires HTTP Basic authentication for the console client and the strict body{ "session_id": "ses_..." }. It returns{ "revoked": boolean },401 invalid_client,400 invalid_request, or503 temporarily_unavailablewhen the client row or session authority is unavailable. The BFF uses it at sign-out, so revocation does not depend on holding an unexpired access token.
BFF session routes
GET /api/auth/currentreturns only{ "user": { "id", "email", "email_verified" } }.POST /api/auth/sign-outrequires the exact consoleOrigin, revokes the authoritative control-plane session, clears the Durable Object record, and expires the HttpOnly cookie.GET /api/management/...and same-origin state-changing methods proxy only fixed management paths to the API service binding. They add a server-side bearer token; browser payloads never receive access tokens, refresh tokens, or client credentials.
The __Host-auth4_console cookie is Secure, HttpOnly, SameSite=Strict, host-only, and expires
within one day. Console access tokens remain behind the BFF. The BFF Durable Object coalesces
concurrent refreshes, rotates the stored refresh token, and enforces an eight-hour idle and
twenty-four-hour absolute session lifetime. Ciphertext is bound (AES-GCM additional data) to the
purpose and the session object name, so rows cannot be replayed into another session.
Failure semantics: an API 400/401 on refresh, or an upstream 401 on any proxied call, drops
the session and expires the cookie (replayed refresh tokens revoke the whole control-plane session).
Transient API failures return 503 temporarily_unavailable and keep the session. The proxy only
forwards Content-Type, WWW-Authenticate and X-Request-Id response headers; upstream
Set-Cookie is never relayed.
Code layout
apps/console/worker/app.ts— BFF routes;apps/console/src/worker.tsre-exports it as the Wrangler entry point.apps/console/worker/session-do.ts—ConsoleSessionCore(logic over Durable Object SQLite) and the thinConsoleSessionStoreDurable Object shell.apps/api/src/modules/management-auth/— API routes, management-actor validation and theControlPlaneSessionAuthorityDurable Object.- Tests:
tests/integration/auth4d-17.management-auth.test.ts(runpnpm test).
Management authorization
Management routes accept only a signed token with the fixed control-plane issuer, the exact
urn:auth4:management audience, token_use=management, the configured client ID, an active
control-plane email identity, and a live session in ControlPlaneSessionAuthority. Customer access
tokens and ID tokens fail validation. Tenant-specific management requests also require a current
membership row for that tenant; GET /management/tenants lists active tenants from current
memberships. Tenant creation and client-management operations remain unavailable until their
management feature ticket implements them.
Runtime wiring
The API Worker needs CONTROL_PLANE_CONSOLE_CLIENT_SECRET, AUTH_SIGNING_KEY_RING, and
AUTH_PRIVACY_KEY secrets. The console Worker needs the same confidential-client secret plus
CONSOLE_SESSION_KEY, a base64url-encoded random 32-byte key. Keep these values out of Wrangler
vars and source control. Staging and production use separate secret values and DO storage.
The feature calls Auth4Services.email.sendVerificationCode for control-plane delivery. The shared
runtime email adapter is currently fail-closed and its customer-only type does not yet accept the
reserved realm, so a deployment must wire a control-plane-compatible AUTH4D-10 email adapter before
developers can receive codes. Tests inject the local mailbox adapter. Project ownership is still
granted only by the project-creation transaction.
Source: docs/api-spec/auth4d-17.md