This guide continues from Add blacklist and adds the oauth feature.
The oauth feature lets users sign in with OAuth providers such as Google and GitHub. Like the other sign-in methods, OAuth sign-in uses the same user model and JWT flow.
This guide uses email as the account identifier. If the email already exists, the user signs in. If it does not exist, a new user is created first.
By the end of this guide, your application will support:
POST /auth/oauth/googlePOST /auth/oauth/github
Prerequisites
Section titled “Prerequisites”- A project completed from Add blacklist
- OAuth client IDs and client secrets from Google and GitHub
Add the OAuth feature
Section titled “Add the OAuth feature”Add the feature
Run the CLI command:
brkpt auth add oauth --driver google,githubThe CLI adds a new features/oauth/ folder with the selected driver files.
If a driver needs extra dependencies, follow the dependency note shown by the CLI. For example, the Google driver uses google-auth-library.
Configure provider credentials
Add the provider client IDs and client secrets to your existing .env file:
GOOGLE_CLIENT_ID="your-google-client-id"GOOGLE_CLIENT_SECRET="your-google-client-secret"GITHUB_CLIENT_ID="your-github-client-id"GITHUB_CLIENT_SECRET="your-github-client-secret"Configure BrkptAuthModule
Add the oauth options to BrkptAuthModule.forRootAsync:
BrkptAuthModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (config: ConfigService) => ({ jwt: { // ... }, oauth: { google: { clientId: config.getOrThrow('GOOGLE_CLIENT_ID'), clientSecret: config.getOrThrow('GOOGLE_CLIENT_SECRET'), }, github: { clientId: config.getOrThrow('GITHUB_CLIENT_ID'), clientSecret: config.getOrThrow('GITHUB_CLIENT_SECRET'), }, }, }),}),Define and validate the DTO
The oauth feature generates an empty DTO by default. The request body is passed to the driver selected by the :provider route parameter.
For this guide, /auth/oauth/google uses GoogleOAuthDriver, whose verify method reads idToken. /auth/oauth/github uses GithubOAuthDriver, whose verify method reads code.
Add those fields to the DTO:
import { IsOptional, IsString } from 'class-validator';
export class OAuthDto { @IsOptional() @IsString() idToken?: string;
@IsOptional() @IsString() code?: string;}The field names must match what the selected driver reads in its verify method. Because Google and GitHub share this DTO, both fields are optional here; the selected driver decides which one is required.
Define the user profile type
Different providers return different user payloads. Add UserProfile to adapters/types.ts so the adapter can normalize those payloads before finding or creating a user:
export interface UserProfile { name: string; email: string;}Implement OAuthAdapter
The OAuth adapter turns provider data into UserProfile, then finds or creates a user.
Create oauth.adapter.ts and implement the Prisma-backed OAuth adapter:
Directorysrc/
Directorybrkpt-auth/
Directoryadapters/
- oauth.adapter.ts
import { BadRequestException, Injectable } from '@nestjs/common';import { TokenPayload } from 'google-auth-library';
import { User } from '../../../generated/prisma/client';import { PrismaService } from '../../prisma/prisma.service';import { OAuthPort } from '../features/oauth/oauth.port';import { UserProfile } from './types';
interface GoogleUser extends TokenPayload { name: string; email: string;}
interface GitHubUser { id: number; login: string; name: string | null; email: string | null;}
@Injectable()export class OAuthAdapter implements OAuthPort<User, UserProfile> { constructor(private readonly prisma: PrismaService) {}
mapRawToProfile(provider: string, raw: unknown): UserProfile | undefined { switch (provider) { case 'google': { const r = raw as GoogleUser; if (!r.email) { throw new BadRequestException( 'Google profile does not include an email address', ); } return { name: r.name, email: r.email }; } case 'github': { const r = raw as GitHubUser; if (!r.email) { throw new BadRequestException( 'GitHub profile does not include an email address', ); } return { name: r.name ?? r.login, email: r.email ?? '' }; } } }
async findOrCreateUserByProfile( profile: UserProfile, ): Promise<{ user: User; created: boolean }> { const existing = await this.prisma.user.findUnique({ where: { email: profile.email }, }); if (existing) { return { user: existing, created: false }; }
const user = await this.prisma.user.create({ data: { name: profile.name, email: profile.email, password: '', }, });
return { user, created: true }; }
extractUserIdFromUser(user: User): number { return user.id; }}This guide continues the same account model from the previous guides: email identifies the user. The OAuth adapter uses that email to sign in an existing user or create a new one. You can change this matching logic for your own application.
Register the feature
Update features.ts and pass OAuthAdapter and the driver classes to oauthFeature:
import { BlacklistAdapter } from './adapters/blacklist.adapter';import { CoreAdapter } from './adapters/core.adapter';import { CredentialsAdapter } from './adapters/credentials.adapter';import { OAuthAdapter } from './adapters/oauth.adapter';import { SessionAdapter } from './adapters/session.adapter';import { FeatureConfig } from './common/interfaces';import { blacklistFeature } from './features/blacklist/blacklist.feature';import { coreFeature } from './features/core/core.feature';import { credentialsFeature } from './features/credentials/credentials.feature';import { GoogleOAuthDriver } from './features/oauth/drivers/google.driver';import { GithubOAuthDriver } from './features/oauth/drivers/github.driver';import { oauthFeature } from './features/oauth/oauth.feature';import { sessionFeature } from './features/session/session.feature';
export const features: FeatureConfig[] = [ coreFeature(CoreAdapter), blacklistFeature(BlacklistAdapter), credentialsFeature(CredentialsAdapter), sessionFeature(SessionAdapter), oauthFeature(OAuthAdapter, GoogleOAuthDriver, GithubOAuthDriver),];Only drivers passed to oauthFeature are enabled. You can keep a driver file in the project without enabling it here.
Run and verify
Section titled “Run and verify”Start the application:
pnpm start:devnpm run start:devyarn start:devThe oauth feature adds one provider-based endpoint:
| Method | Path | Description |
|---|---|---|
POST |
/auth/oauth/:provider |
Sign in with an OAuth provider |
The :provider value must match an enabled driver, such as google or github.
Get a Google idToken from your frontend or a temporary test flow, then send it to the server:
POST /auth/oauth/googleContent-Type: application/json
{ "idToken": "<google-id-token>"}GitHub
Get a GitHub authorization code from your frontend or a temporary test flow, then send it to the server:
POST /auth/oauth/githubContent-Type: application/json
{ "code": "<github-code>"}If the email already exists, the request signs in that user. If it does not exist, a new user is created first. Both cases return the same token result as the other sign-in methods.