Nest Authbeta

Custom OAuth Provider

Add Discord, Microsoft, Slack — anything not in the built-in list.

A copy-paste starting point. See Custom OAuth / SSO Provider for the full contract and the client call.

// app/auth/discord.provider.ts
import { BaseAuthProvider, AuthProviderUser } from '@ackplus/nest-auth';
import { UnauthorizedException } from '@nestjs/common';
 
interface DiscordOptions {
  clientId: string;
  clientSecret: string;
  redirectUri: string;
}
 
export class DiscordAuthProvider extends BaseAuthProvider {
  providerName = 'discord'; // NOT `name`
  skipMfa = true;
 
  constructor(private readonly opts: DiscordOptions) {
    super(); // repos are injected by the provider registry
  }
 
  getRequiredFields(): string[] {
    return ['token']; // or ['code']
  }
 
  async validate(credentials: { code?: string; token?: string }): Promise<AuthProviderUser | null> {
    let accessToken = credentials.token;
 
    // If the client sent a code instead, exchange it
    if (!accessToken && credentials.code) {
      const tokenResp = await fetch('https://discord.com/api/oauth2/token', {
        method: 'POST',
        body: new URLSearchParams({
          client_id: this.opts.clientId,
          client_secret: this.opts.clientSecret,
          code: credentials.code,
          grant_type: 'authorization_code',
          redirect_uri: this.opts.redirectUri,
        }),
      }).then((r) => r.json());
      accessToken = tokenResp.access_token;
    }
 
    if (!accessToken) throw new UnauthorizedException('Missing Discord credential');
 
    const me = await fetch('https://discord.com/api/users/@me', {
      headers: { Authorization: `Bearer ${accessToken}` },
    }).then((r) => r.json());
 
    if (!me?.id) throw new UnauthorizedException('Discord token rejected');
 
    return {
      userId: me.id, // the PROVIDER's user id — the field is `userId`, not `providerId`
      email: me.email,
      emailVerified: me.verified === true,
      metadata: { username: me.username, avatarUrl: me.avatar },
    };
  }
}

Register it — a plain instance with forRoot; the repositories are injected for you:

NestAuthModule.forRoot({
  customAuthProviders: [
    new DiscordAuthProvider({
      clientId: process.env.DISCORD_CLIENT_ID!,
      clientSecret: process.env.DISCORD_CLIENT_SECRET!,
      redirectUri: process.env.DISCORD_REDIRECT_URI!,
    }),
  ],
});

Log in — a first-time user needs createUserIfNotExists: true (the client SDK's socialLogin() sets it):

await client.socialLogin('discord', accessToken);
// or: POST /auth/login { providerName: 'discord', credentials: { code }, createUserIfNotExists: true }

On this page