Skip to main content

Discord OAuth

WIP

Discord Sign-in. Useful for community apps, gated content, and game services.

Setup

  1. Open the Discord Developer Portal.
  2. Create an application.
  3. Under OAuth2 -> General, add a redirect URL: https://app.example.com/auth/discord/callback.
  4. Copy the client ID and client secret.
import { authDiscord } from '@gentleduck/auth/providers/oauth/discord'

export const auth = new AuthEngine({
  // ...
  providers: [
    authDiscord({
      clientId: process.env.DISCORD_CLIENT_ID!,
      clientSecret: process.env.DISCORD_CLIENT_SECRET!,
      redirectUri: 'https://app.example.com/auth/discord/callback',
      stateSigningSecret: process.env.OAUTH_STATE_SECRET!,
      scopes: ['identify', 'email'],
      onSignIn: async ({ profile }) => {
        let identity = await auth.identities.findByProviderSub({
          providerId: 'discord',
          sub: profile.id,
        })
        if (!identity) {
          identity = await auth.identities.create({
            email: profile.email,
            emailVerified: profile.verified,
            profile: {
              displayName: profile.global_name ?? profile.username,
              avatarUrl: profile.avatar
                ? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`
                : undefined,
              discordUsername: profile.username,
            },
          })
          await auth.identities.linkProvider(identity.id, {
            providerId: 'discord',
            sub: profile.id,
          })
        }
        return { identityId: identity.id }
      },
    }),
  ],
})

What the profile contains

Discord's /users/@me:

{
  id: '123456789012345678',           // numeric snowflake
  username: 'ada',                     // current unique handle
  global_name: 'Ada Lovelace',         // display name (may be null)
  discriminator: '0',                  // legacy; always '0' on new accounts
  email: 'ada@example.com',
  verified: true,
  avatar: 'a1b2c3...',                 // hash; build URL from cdn.discordapp.com
  banner: 'd4e5f6...',                 // banner hash
  accent_color: 12345678,
  locale: 'en-US',
}

Gating by guild (server) membership

If your app should only allow members of a specific Discord server:

authDiscord({
  // ...
  scopes: ['identify', 'email', 'guilds'],
  onSignIn: async ({ profile }) => {
    const guilds = await fetch('https://discord.com/api/users/@me/guilds', {
      headers: { Authorization: `Bearer ${profile._accessToken}` },
    }).then((r) => r.json())

    const inMyServer = guilds.some((g) => g.id === '999999999999999999')
    if (!inMyServer) {
      throw new AuthError('AUTH/PROVIDER_FAILED', {
        reason: 'not-in-guild',
      })
    }
    // ...
  },
})

For per-role checks (only allow Moderators to sign in), use the guilds.members.read scope and call /users/@me/guilds/{guild.id}/member for the user's role IDs.

Bot integrations

duck-auth's discord provider is for user sign-in (OAuth2). If you're building a bot that uses Discord's gateway, that's a separate protocol (gateway.discord.gg) - not in scope here.