コンテンツにスキップ

OAuth2 authorization-code flow

このコンテンツはまだ日本語訳がありません。

Status Stable · Last verified 2026-07-11 · API v10 · Source OAuth2

The authorization-code grant logs a user in with Discord and lets your app act on their behalf. The runnable oauth2-login example implements this flow (including refresh/revoke) with zero dependencies and unit tests you can run without secrets.

1. AUTHORIZE Redirect the user to Discord's consent screen with a `state` value.
2. CONSENT User approves; Discord redirects to your redirect_uri with ?code&state
3. VALIDATE Your server checks `state` matches what it issued (CSRF defense).
4. EXCHANGE Your SERVER POSTs the code to the token endpoint (with client creds)
→ access_token (+ refresh_token, expires_in, scope).
5. CALL API Use Authorization: Bearer <access_token> (e.g. GET /users/@me).
6. MAINTAIN Refresh before expiry; revoke on logout.

[!NOTE] A note on endpoint versions Discord documents the authorize endpoint without /api or a version, and the token / revoke endpoints unversioned (https://discord.com/api/oauth2/token). A versioned form also resolves. Resource endpoints such as /users/@me are v10-pinned per the atlas API-version convention.

https://discord.com/oauth2/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&scope=identify # space-separated if multiple: "identify email"
&redirect_uri=<url-encoded, EXACT match of a portal Redirect URI>
&state=<opaque CSRF token>
# &prompt=consent # optional: "consent" always shows the screen; "none" skips if already authorized
# &integration_type=1 # optional: request a user-install (1) vs guild-install (0)

Verified authorize parameters: response_type, client_id, scope, redirect_uri, state, prompt, integration_type.

Step 2–3 — Handle the redirect and validate state

Section titled “Step 2–3 — Handle the redirect and validate state”

Discord redirects to redirect_uri?code=…&state=…. Before doing anything, confirm the returned state equals the one you issued (the example binds it to an httpOnly cookie and compares in constant time). If it doesn’t match, abort — this is your CSRF defense.

Step 4 — Exchange the code for tokens (server-side)

Section titled “Step 4 — Exchange the code for tokens (server-side)”
POST https://discord.com/api/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
code=THE_CODE
redirect_uri=SAME_REDIRECT_URI

Client authentication (verified): HTTP Basic auth (client_id:client_secret) or client_id + client_secret supplied in the form body. Either way, the secret is used server-side only.

A successful response is JSON with access_token, token_type (Bearer), expires_in, refresh_token, and the granted scope. Authorization codes are single-use — never retry an exchange with the same code.

GET https://discord.com/api/v10/users/@me
Authorization: Bearer ACCESS_TOKEN

Returns the user object (id, username, global_name, avatar…). More scopes unlock more endpoints — request the minimum.

Refresh before expires_in elapses (verified params grant_type=refresh_token, refresh_token, plus client auth):

POST https://discord.com/api/oauth2/token
grant_type=refresh_token&refresh_token=…&client_id=…&client_secret=…

The response is a new token set; store the possibly-rotated refresh_token.

Revoke on logout (verified endpoint + params token, optional token_type_hint, plus client auth):

POST https://discord.com/api/oauth2/token/revoke
token=…&token_type_hint=access_token&client_id=…&client_secret=…

[!CAUTION] Verified — revocation cascades “When you revoke a token, any active access or refresh tokens associated with that authorization will be revoked, regardless of the token and token_type_hint values.” So revoking the access token also kills the refresh token (and vice versa) — revoke once to fully log the user out. Source: OAuth2.

The oauth2-login example’s tokens.mjs implements refresh + revoke with an injectable fetch and mocked-response tests.

Authorization step (redirect errors). If the user denies consent or something is wrong, Discord redirects to your redirect_uri with an error (e.g. access_denied), usually an error_description, and your state — no code. Handle it:

const err = url.searchParams.get("error");
if (err) return render(`Authorization failed: ${url.searchParams.get("error_description") ?? err}`);

Token step (HTTP errors). The token endpoint returns a non-2xx status with a JSON body on failure. Check res.ok and surface the body:

const res = await fetch(tokenUrl, { method: "POST", headers, body });
if (!res.ok) {
const detail = await res.text(); // e.g. {"error":"invalid_grant"}
throw new Error(`token exchange failed (${res.status}): ${detail}`);
}

[!TIP] Observation — standard OAuth2 error codes The token endpoint follows OAuth2 (RFC 6749) error responses. Common bodies: invalid_grant (bad/expired/reused code or refresh token), invalid_client (bad credentials), invalid_scope, invalid_request. Treat exact strings as the RFC-standard set unless the docs specify otherwise; log the full body when debugging.

  • client_secret is server-side only — never in a browser/mobile bundle. Reset it if leaked (OAuth2 → Reset Secret). See Security.
  • Always use state (CSRF) and validate it on return; bind it to the session.
  • Exact redirect_uri — mismatches are rejected; loose matching invites open redirects.
  • Least privilege scopes; HTTPS in production; store tokens server-side (create a session — don’t hand tokens to the browser).