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