oauth2

Discord OAuth2: Add Login to Your Website

Rank.top Team4 min read

Follow the authorization-code flow, validate state, and keep Discord tokens on the server.

Separate login from bot installation

Discord OAuth2 lets someone authorize your website to access specific account information. A bot invite is a different use of OAuth2. Start by deciding what the website needs after login.

ScopeUseDo you need it for basic login?
identifyRead the signed-in user’s basic profileUsually
emailRead the email address when availableOnly if the product needs it
guildsList the user’s serversFor a server-picker or related feature
bot / applications.commandsInstall a bot or commandsSeparate from basic account login

The examples below illustrate a server-side authorization-code flow. For a framework auth library, use its Discord provider and session handling rather than combining half of this flow with half of another one.

The login flow in five steps

  1. Create a login request

    Generate a random state value and save it in the user’s server-side session with a short expiry.
  2. Send the user to Discord

    Include your client ID, registered redirect URI, requested scopes, and state.
  3. Validate the callback

    Handle denial, require a code, and compare state with the session value before exchanging anything. Reject missing, expired, or mismatched state and consume it once.
  4. Exchange the code on the server

    Send the code to Discord’s token endpoint with your client credentials and the same redirect URI.
  5. Create your own session

    Fetch the user profile, link it to your account record by Discord ID, and issue a secure session cookie.

Register the exact callback address in the Developer Portal. The protocol, host, port, and path matter. See Discord’s OAuth2 reference for the endpoints and supported scopes.

Build the authorization URL

This Node.js fragment assumes session is your framework’s server-side session. Save it before redirecting the browser. Use HTTPS in production.

Server-side login route · illustrative fragment
import { randomBytes } from "node:crypto";

const state = randomBytes(32).toString("hex");
session.discordOAuth = {
state,
expiresAt: Date.now() + 10 * 60 * 1000,
};
await session.save();

const params = new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID!,
redirect_uri: process.env.DISCORD_REDIRECT_URI!,
response_type: "code",
scope: "identify",
state,
});

const authorizationUrl =
"https://discord.com/oauth2/authorize?" + params.toString();
// Redirect the browser to authorizationUrl.

Validate required environment variables when the application starts. The non-null assertions here keep the fragment short; they are not runtime validation.

Token exchange · only after callback state validation
const response = await fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID!,
client_secret: process.env.DISCORD_CLIENT_SECRET!,
grant_type: "authorization_code",
code,
redirect_uri: process.env.DISCORD_REDIRECT_URI!,
}),
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) throw new Error("Discord login could not be completed");
const tokens = await response.json();
// Keep tokens server-side. Do not return this object to the browser.

Protect the session after login

  • Use Secure, HttpOnly cookies with an appropriate SameSite policy.
  • Store only the account information and tokens the feature needs.
  • Protect stored refresh tokens and update them when Discord returns a replacement.
  • Recheck server-management permissions before changing bot settings.
  • Never log authorization codes, access tokens, refresh tokens, or client secrets.

login is not authorization

Knowing who someone is does not grant access to every server.

Verify that the signed-in user can manage the selected server before reading or updating its private configuration. Do this on the backend, even if the dashboard hides the controls.

Make failures understandable

A redirect mismatch usually means the configured callback addresses differ. An invalid grant can mean an expired or reused code. A state mismatch should end the attempt and let the user start again.

Handle token expiry and revoked access without looping through failed requests. If continued Discord API access is unnecessary, avoid retaining its tokens after creating your own session.

keep reading

More from the blog

All articles