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"
  }
}
  • scope is openid plus whichever of profile, email and offline_access the application allows. offline_access, and so refresh_token, needs the application’s browser refresh opt-in (AUTH4D-63 (repository file docs/api-spec/auth4d-63.md)).
  • The ID token carries idp: "discord" and the request’s nonce. email and email_verified appear only for an address proven to auth4.dev, as for every other sign-in.
  • discord is for the Activity’s authenticate() call. auth4.dev keeps no copy and drops Discord’s refresh token.
  • No cookie is set. The response has Cache-Control: no-store and Access-Control-Allow-Origin set 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