AUTH4D-15 guest authentication and account upgrades

Guest sessions are scoped to an active customer tenant and an enabled browser or native client. Creation is limited by tenant plus client/IP pair and by tenant/IP. Guest access tokens use the client as their audience, contain no management audience or management scopes, and are returned only with the session response. Browser clients may also receive the usual HttpOnly session cookie; native clients should persist refresh credentials only through an explicit secure-storage adapter.

Create a guest session

POST /t/{tenantId}/guest
Content-Type: application/json

{"client_id":"cli_…","browser_flow_id":"flw_…"}

The client must be enabled on an active tenant. The response includes the stable guest user_id, a short-lived upgrade_proof for the server-created linking intent, and the scoped session token set:

{
  "user_id": "usr_…",
  "upgrade_proof": { "proof_id": "prf_…", "expires_in": 300 },
  "access_token": "…",
  "token_type": "Bearer",
  "expires_in": 300,
  "refresh_token": "…",
  "id_token": "…",
  "scope": "openid profile offline_access",
  "session_id": "ses_…"
}

refresh_token and id_token are present only when permitted by the client scopes. offline_access is never added if the client did not allow it. The endpoint also creates an HttpOnly browser-flow cookie when one was not supplied, so the email-code and Discord flows can bind their proofs to the same flow. Client-supplied user IDs are never used to establish a session.

Create or renew a linking intent

POST /t/{tenantId}/guest/linking-intent
Authorization: Bearer {active-guest-access-token}
Content-Type: application/json

{"client_id":"cli_…","browser_flow_id":"flw_…"}

The API verifies the access-token signature and active Durable Object session, then confirms that the subject is an active guest for this tenant. The linking intent binds that subject, session, client, tenant, browser flow, and flow-cookie hash. A cookie is added if the request starts a fresh browser flow. A live intent for the same flow cannot be replaced.

Complete an upgrade

Obtain a new proof from the ordinary tenant email-verification or Discord callback using the linking intent’s client and browser flow. Then submit its proof_id:

POST /t/{tenantId}/guest/upgrade
Authorization: Bearer {same-active-guest-access-token}
Cookie: __Host-auth4_flow_{flowId}=...
Content-Type: application/json

{"client_id":"cli_…","browser_flow_id":"flw_…","proof_id":"prf_…"}

The API consumes the linking intent and provider proof once, and verifies that both are fresh and match the same tenant, client, browser flow, and active guest session. Email proofs must reference a verified email identity; Discord proofs must reference a Discord identity validated through Discord’s user API. The API attaches the credential through the identity service without changing the guest subject, then creates a replacement session, revokes the previous session, and records a guest.upgraded audit event. The response returns the same user_id, linked identity metadata, replacement token set, and replacement session_id.

The email-code and Discord flows resolve every proven credential to a tenant user, creating a provisional user when the credential is new. The upgrade moves that credential to the guest only when the provisional user and its identity were both created after the linking intent, the user owns no other identity, and it has never had a session, membership or consent record. The move and the provisional user’s deactivation run in one D1 transaction, so concurrent upgrades attach a credential at most once.

If another user already owns the credential, the API returns 409 identity_in_use; callers must sign in to that account explicitly. No user or game-data merge is attempted. Missing, expired, replayed, flow-mismatched, cross-tenant, or cross-session proof attempts fail closed. Guest access tokens remain customer tokens and never authorize management APIs.

Session and deployment requirements

Guest session issuance requires the session Durable Object binding and signing-key configuration. The guest feature fails closed if authoritative session state is unavailable. One-time intents and proofs use tenant-scoped SQLite-backed challenge state; creation and upgrade abuse decisions use tenant-scoped atomic Durable Object rate limits with HMAC-pseudonymized IP/user subjects.

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