AUTH4D-312 Discord Activities
Games running as Discord Activities sign
players in without a redirect. The Activity calls the Embedded App SDK’s authorize() command,
posts the code to auth4.dev, and gets auth4 tokens plus the Discord access token it needs for the
SDK’s authenticate() command. auth4.dev exchanges the code with the project’s existing Discord
provider configuration (client ID and secret), so the Activity needs no backend of its own.
The Discord identity is matched or created exactly as hosted Discord sign-in does it
(AUTH4D-14 (repository file docs/api-spec/auth4d-14.md)): the same Discord user gets the same sub either way, and the Discord
address never becomes the email claim (AUTH4D-265 (repository file docs/api-spec/auth4d-265.md)).
The exchange is off until a project owner or admin turns it on for a browser application and names its Activity origin. Only that origin may call it.
Code exchange
POST /t/{tenantId}/identity/discord/activity
{ "client_id": "cli_…", "code": "…", "nonce": "…" }
| Field | Required | Notes |
|---|---|---|
client_id |
yes | An enabled browser application with the Activity exchange turned on |
code |
yes | From authorize(), 8 to 512 characters of A-Z a-z 0-9 . _ ~ - |
nonce |
no | Echoed as the ID token’s nonce, 16 to 512 characters of the same set; SDKs set it |
The request must carry Origin equal to the application’s Activity origin, the Discord proxy
https://{discord application id}.discordsays.com. The authorize() call must include the
identify scope. Add email to let a Discord-verified address satisfy the verified email step
(below).
auth4.dev sends the code to Discord’s token endpoint without a redirect_uri, as Discord expects for
Activity codes, then reads /users/@me.
200 returns the auth4 token set, the user and session, and the Discord token:
{
"user_id": "usr_…",
"session_id": "ses_…",
"access_token": "…",
"token_type": "Bearer",
"expires_in": 300,
"id_token": "…",
"refresh_token": "…",
"scope": "openid profile email offline_access",
"discord": {
"access_token": "…",
"token_type": "Bearer",
"expires_in": 604800,
"scope": "identify email"
}
}
scopeisopenidplus whichever ofprofile,emailandoffline_accessthe application allows.offline_access, and sorefresh_token, needs the application’s browser refresh opt-in (AUTH4D-63 (repository filedocs/api-spec/auth4d-63.md)).- The ID token carries
idp: "discord"and the request’snonce.emailandemail_verifiedappear only for an address proven to auth4.dev, as for every other sign-in. discordis for the Activity’sauthenticate()call. auth4.dev keeps no copy and drops Discord’s refresh token.- No cookie is set. The response has
Cache-Control: no-storeandAccess-Control-Allow-Originset to the Activity origin, never with credentials.
A new player runs the project’s pre_registration Flows and every sign-in runs post_login, as for
hosted Discord sign-in. A successful sign-in writes authentication.login.succeeded (metadata
provider: discord, client_type: browser) and publishes the user.created and user.signed_in
Flows events with method discord.
Errors
Errors are JSON {"error": "…"} and never include Discord’s response.
| Status | error |
When |
|---|---|---|
400 |
invalid_request |
The body is not exactly the fields above |
401 |
unauthorized_client |
Unknown or disabled application, a game or server application, or a suspended project |
403 |
unauthorized_client |
The Activity exchange is off for the application |
403 |
access_denied |
Origin is missing or is not the application’s Activity origin; or a disabled user, or a Flow denied the sign-in |
400 |
invalid_grant |
Discord refused the code (unknown, used or expired), or did not grant identify |
403 |
interaction_required |
The sign-in needs a hosted-login screen: the verified email step or a Flows terms screen |
403 |
account_suspended |
The player has an active ban (AUTH4D-310 (repository file docs/api-spec/auth4d-310.md)); the reason is never sent |
400 |
invalid_scope |
The application does not allow openid |
429 |
slow_down |
Too many exchanges for this application from one address, or for one Discord user |
503 |
temporarily_unavailable |
Discord sign-in is not configured for the project, or Discord or state storage failed |
Refusals before the origin check (400 invalid_request, 401, 403 unauthorized_client and the
origin refusal itself) carry no CORS headers.
The verified email step (AUTH4D-258 (repository file docs/api-spec/auth4d-258.md)) and Flows screens need hosted login, which an
Activity cannot show. Instead of skipping them, the exchange refuses with interaction_required.
With the default requireVerifiedEmail of when_missing, request the email scope in
authorize() so a Discord-verified address counts, or set the application to off.
Budgets: 300 exchanges per minute per application and client address (requests arrive through the Discord proxy, so players share addresses) and 10 per minute per Discord user.
CORS
The boundary answers the preflight (OPTIONS, POST with at most Content-Type) only for an
origin that is the turned-on Activity origin of an enabled browser application of the active tenant
in the path. Every other origin, including the deployment’s own and the application’s registered
allowedOrigins, gets 403 origin_denied. The exchange response is readable only by the named
application’s Activity origin.
Inside Discord, requests to other hosts are blocked by the Activity’s Content Security Policy. Add a
URL mapping for the auth origin in the Discord Developer Portal and route the SDK’s requests through
it (for example with the Embedded App SDK’s patchUrlMappings). The browser then sends the
requests from the Activity origin.
Setting
GET /management/tenants/{tenantId}/clients/{clientId}/discord-activity (client:read) returns:
{ "discord_activity": { "enabled": false, "origin": null } }
PATCH on the same path (client:write, owner or admin) takes either field or both:
{ "enabled": true, "origin": "https://123456789012345678.discordsays.com" }
origin is https:// plus the 17 to 20 digit Discord application ID plus .discordsays.com, or
null. Turning the exchange off keeps the origin. The response is the stored setting. A change
writes management.client.discord_activity.updated with status enabled or disabled.
| Status | error.code |
When |
|---|---|---|
400 |
validation_failed |
A malformed origin (details[0].field origin) or enabled that is not a boolean |
400 |
validation_failed |
enabled: true with no origin (origin), or on a game or server application (enabled) |
403 |
forbidden |
The project is unknown or belongs to someone else |
403 |
insufficient_role |
A viewer tries to change the setting |
404 |
not_found |
The application is not in this project |
409 |
project_suspended |
The project is suspended |
503 |
not_ready |
Migration 0043_discord_activity.sql has not been applied |
The setting is stored in clients.discord_activity_enabled and clients.discord_activity_origin
(migration 0043_discord_activity.sql). The exchange reads a database without them as off. Owners
and admins can also change it in the console, in the Discord Activity section of a browser
application (AUTH4D-317).
Browser SDK
Auth4BrowserClient.signInWithDiscordActivity({ code }) posts the code with a fresh nonce, checks
the ID token like a redirect sign-in, keeps the auth4 tokens in memory and resolves with
{ session, discord: { accessToken, expiresIn, scope } }. See the
Discord Activities guide.
Source: docs/api-spec/auth4d-312.md