AUTH4D-14 Discord social authentication

Discord login is a hosted browser flow and does not use Discord as an OpenID Connect issuer. Following Discord’s OAuth2 documentation, the API requests only the identify email scopes, exchanges the authorization code on the Worker, and verifies the account through the current-user API.

Start and callback

GET /t/{tenantId}/identity/discord/start?client_id={clientId}&browser_flow_id={flowId} requires the __Host-auth4_flow_{flowId} HttpOnly browser cookie issued by /oauth/authorize. The API consumes and restores the short-lived tenant-scoped authorization-flow record to validate its tenant, client, and cookie binding; caller-supplied redirect URIs, scopes, and state are ignored. The tenant must be active, the client enabled for authorization code, and the tenant’s Discord provider configuration enabled. A configured Discord application client ID and secret binding must also be available. Disabled or incomplete provider configuration fails closed.

The API redirects to https://discord.com/oauth2/authorize with response_type=code, the fixed callback URI GET /t/{tenantId}/identity/discord/callback, a cryptographically random state, and the minimal identify email scopes. State is stored only as a hash in the tenant’s SQLite-backed challenge Durable Object and is bound to the authorization flow, client, callback URI, and hash of the original browser cookie. It expires no later than the browser authorization flow.

The callback accepts exactly one of code or provider error, plus state. The state record is consumed before the browser binding, provider result, or code is processed, so it cannot be reused. Missing, unknown, expired, tenant-mismatched, and browser-mismatched state fails closed. Provider denial redirects to the hosted login with error=access_denied; other provider, timeout, and malformed-response errors use error=temporarily_unavailable. Error responses do not include provider bodies or credential details.

For success, the Worker exchanges the code at https://discord.com/api/oauth2/token using server-side HTTP Basic authentication and calls https://discord.com/api/v10/users/@me with the returned bearer token. Neither the access token nor the client secret is included in a frontend response or log. Only the stable Discord user ID is used to look up an identity, scoped by the tenant. Matching email addresses never merge accounts. A new subject creates a distinct user and Discord identity; an existing subject resolves to its existing active user.

The callback returns to /login with a one-time proof_id and proof_purpose=discord_login. The proof is stored in the tenant’s atomic challenge authority and binds tenant, client, browser flow, user, and Discord identity. The hosted login passes that proof to /oauth/authorize/continue; it never receives provider access tokens or secrets.

Provider configuration

Each tenant enables Discord through its provider_configurations row (provider='discord', enabled=1). provider_client_id may override the DISCORD_CLIENT_ID Worker binding; secret_binding selects the Worker secret binding name and defaults to DISCORD_CLIENT_SECRET. The callback URI must be registered in the Discord application. Local tests inject a deterministic HTTP adapter and do not require Discord credentials.

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