Authentication

Discord OAuth2 Authentication: Complete Implementation Guide

Rank.top Team
August 2025

A production‑ready walkthrough for implementing "Login with Discord": app setup, building the authorization URL, securely exchanging codes for tokens, fetching user data, integrating with NextAuth, and avoiding common errors. This post focuses on the Authorization Code flow for server‑rendered web apps.

Overview: Login vs Add‑to‑Server

Login with Discord (User OAuth2)

  • Scopes like identify, email, guilds.
  • Users authenticate and your app fetches their profile.
  • Best for dashboards, portals, and account linking.

Add to Server (Bot Authorization)

  • Scopes bot and often applications.commands.
  • Requires a permissions integer; optional guild_id & disable_guild_select.
  • Separate from user login. Only include if onboarding a bot.

Developer Portal Setup

  1. Open the Discord Developer Portal and create an application.
  2. Under OAuth2 → Redirects, add your callback URL exactly (dev and prod). Example: http://localhost:3000/api/auth/callback/discord or your domain.
  3. Copy your Client ID and generate a Client Secret. Keep the secret server‑side only.

Build the Authorization URL

For user login, redirect to Discord's authorization endpoint with the required parameters.

const DISCORD_AUTHORIZE = 'https://discord.com/api/oauth2/authorize';
const clientId = process.env.DISCORD_CLIENT_ID!;
const redirectUri = 'https://your.app/api/auth/callback/discord';
const scopes = ['identify', 'email'];

// Always include a CSRF state value you can verify on callback
function buildAuthorizeUrl(state: string) {
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code',
scope: scopes.join(' '),
state,
});
return `${DISCORD_AUTHORIZE}?${params.toString()}`;
}

Scopes: identify for basic profile, email if you need the user's email, guilds to list servers they are in.

Exchange Code for Tokens

After callback, exchange the code for an access token at Discord's token endpoint.

const DISCORD_TOKEN = 'https://discord.com/api/oauth2/token';

export async function exchangeCodeForToken(code: string) {
const body = new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID!,
client_secret: process.env.DISCORD_CLIENT_SECRET!,
grant_type: 'authorization_code',
code,
redirect_uri: 'https://your.app/api/auth/callback/discord',
});

const res = await fetch(DISCORD_TOKEN, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
if (!res.ok) throw new Error('Token exchange failed');
return res.json() as Promise<{ access_token: string; refresh_token: string; token_type: string; expires_in: number; scope: string; }>;
}

Keep the client secret server‑side. Do not perform token exchange in the browser for web apps.

Fetch the Current User

Use the access token to call /users/@me and optionally /users/@me/guilds if you requested guilds.

export async function fetchDiscordUser(accessToken: string) {
const res = await fetch('https://discord.com/api/users/@me', {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) throw new Error('Failed to fetch user');
return res.json();
}

Refresh & Revoke Tokens

Refresh

To refresh, POST to the token endpoint with the refresh token.

export async function refreshToken(refresh_token: string) {
const body = new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID!,
client_secret: process.env.DISCORD_CLIENT_SECRET!,
grant_type: 'refresh_token',
refresh_token,
});
const res = await fetch('https://discord.com/api/oauth2/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
if (!res.ok) throw new Error('Refresh failed');
return res.json();
}

Revoke

To revoke, POST to the revoke endpoint with the token you want to invalidate.

export async function revokeToken(token: string) {
const body = new URLSearchParams({
client_id: process.env.DISCORD_CLIENT_ID!,
client_secret: process.env.DISCORD_CLIENT_SECRET!,
token,
});
const res = await fetch('https://discord.com/api/oauth2/token/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
if (!res.ok) throw new Error('Revoke failed');
}

Note: Some libraries handle refresh and revoke for you. For public SPA/native apps, prefer a backend for token exchange/storage.

NextAuth Integration (Next.js)

If you are on Next.js, next-auth has a Discord provider that wires up the flow and sessions.

import NextAuth from 'next-auth';
import DiscordProvider from 'next-auth/providers/discord';

export const authOptions = {
providers: [
DiscordProvider({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
authorization: { params: { scope: 'identify email guilds' } },
}),
],
};

export default NextAuth(authOptions);

Sign‑in path defaults to /api/auth/signin; callback defaults to /api/auth/callback/discord. Align your Redirect in the Developer Portal accordingly.

Common Errors & Fixes

Authorization Phase

  • invalid_redirect_uri: Redirect must match exactly, including protocol, hostname, path, and trailing slash.
  • state mismatch: Generate a CSRF state, store it (cookie/session), and verify on callback.

Token/User Phase

  • invalid_grant: The code is expired or already used; ensure single use and correct redirect URI.
  • 401 Unauthorized: Missing/invalid Bearer token; check token type and header.

Security Notes

  • Keep client secrets off the client; exchange tokens on the server.
  • Request minimal scopes; explain why you need each.
  • Handle 429s per Discord Rate Limits.

Authenticate users, then grow faster

List your bot on Rank.top for discovery, analytics, and passive vote revenue. When you need more reach, add tasteful ads.

References