OAuth2 examples for NestJS are amazingly scarce. There's an open issue since 2018 asking for them, but the replies (1, 2) and resources elsewhere (1, 2) only provide a partial/incomplete overview.
Here I'll show a full-stack authentication flow, including authenticated requests after the social token has been acquired, optionally for GraphQL as well. You can check a working example in the nestjs-starter repo.
Solution overview:
- 1, Implement Google auth using
@nestjs/passport
andpassport-google-auth
(other providers are very similar). - 2, Once redirected back to the app, issue a JWT token, so the app can manage the user's session.
- 3, Protect REST and GraphQL endpoints with a JWT strategy.
Step 1
Credits go here for the Google Oauth strategy implementation and showing how to create an OAuth app in Google with screenshots. The code should look like this:
google-oauth.controller.ts
import { Controller, Get, Req, Res, UseGuards } from "@nestjs/common";
import { Request, Response } from "express";
import { GoogleOauthGuard } from "./google-oauth.guard";
@Controller("auth/google")
export class GoogleOauthController {
constructor(private jwtAuthService: JwtAuthService) {}
@Get()
@UseGuards(GoogleOauthGuard)
async googleAuth(@Req() _req) {
// Guard redirects
}
@Get("redirect")
@UseGuards(GoogleOauthGuard)
async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {
// For now, we'll just show the user object
return req.user;
}
}
google-oauth.guard.ts
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class GoogleOauthGuard extends AuthGuard("google") {}
google-oauth.module.ts
import { Module } from "@nestjs/common";
import { GoogleOauthController } from "./google-oauth.controller";
import { GoogleOauthStrategy } from "./google-oauth.strategy";
@Module({
imports: [],
controllers: [GoogleOauthController],
providers: [GoogleOauthStrategy],
})
export class GoogleOauthModule {}
google-oauth.strategy.ts
import { PassportStrategy } from "@nestjs/passport";
import { Profile, Strategy } from "passport-google-oauth20";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class GoogleOauthStrategy extends PassportStrategy(Strategy, "google") {
constructor(
configService: ConfigService,
private readonly usersService: UsersService
) {
super({
// Put config in `.env`
clientID: configService.get<string>("OAUTH_GOOGLE_ID"),
clientSecret: configService.get<string>("OAUTH_GOOGLE_SECRET"),
callbackURL: configService.get<string>("OAUTH_GOOGLE_REDIRECT_URL"),
scope: ["email", "profile"],
});
}
async validate(
_accessToken: string,
_refreshToken: string,
profile: Profile
) {
const { id, name, emails } = profile;
// Here a custom User object is returned. In the the repo I'm using a UsersService with repository pattern, learn more here: https://docs.nestjs.com/techniques/database
return {
provider: "google",
providerId: id,
name: name.givenName,
username: emails[0].value,
};
}
}
You can now use @UseGuards(GoogleOauthGuard)
. You'll notice that every protected route redirects to Google Auth. We'll solve this next.
Step 2
Now that the app knows the user we can handle the user's session within the app. We'll issue a JWT token and use the corresponding Guard to protect authenticated routes. This is explained in the official docs.
We need to transmit this token between the server and the client. A safe and easy-to-use choice is via a SameSite HttpOnly Cookie. The code should look like this:
jwt-auth.guard.ts
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") {}
jwt-auth.module.ts
import { Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { JwtAuthService } from "./jwt-auth.service";
import { JwtAuthStrategy } from "./jwt-auth.strategy";
@Module({
imports: [
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => {
return {
secret: configService.get<string>("JWT_SECRET"),
signOptions: {
expiresIn: configService.get<string>("JWT_EXPIRES_IN"),
},
};
},
inject: [ConfigService],
}),
],
providers: [JwtAuthStrategy, JwtAuthService],
exports: [JwtModule, JwtAuthService],
})
export class JwtAuthModule {}
jwt-auth.service.ts
import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
import { JwtPayload } from "./jwt-auth.strategy";
@Injectable()
export class JwtAuthService {
constructor(private jwtService: JwtService) {}
login(user) {
const payload: JwtPayload = { username: user.username, sub: user.id };
return {
accessToken: this.jwtService.sign(payload),
};
}
}
jwt-auth.strategy.ts
import { ExtractJwt, Strategy } from "passport-jwt";
import { PassportStrategy } from "@nestjs/passport";
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
export type JwtPayload = { sub: number; username: string };
@Injectable()
export class JwtAuthStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
const extractJwtFromCookie = (req) => {
let token = null;
if (req && req.cookies) {
token = req.cookies["jwt"];
}
return token;
};
super({
jwtFromRequest: extractJwtFromCookie,
ignoreExpiration: false,
secretOrKey: configService.get<string>("JWT_SECRET"),
});
}
async validate(payload: JwtPayload) {
return { id: payload.sub, username: payload.username };
}
}
Modify the OAuth controller to issue the JWT token:
google-oauth.controller.ts
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { Request, Response } from 'express';
import { GoogleOauthGuard } from './google-oauth.guard';
import { JwtAuthService } from '../jwt/jwt-auth.service';
@Controller('auth/google')
export class GoogleOauthController {
constructor(private jwtAuthService: JwtAuthService) {}
@Get()
@UseGuards(GoogleOauthGuard)
async googleAuth(@Req() _req) {
// Guard redirects
}
@Get('redirect')
@UseGuards(GoogleOauthGuard)
async googleAuthRedirect(@Req() req: Request, @Res() res: Response) {
const { accessToken } = this.jwtAuthService.login(req.user);
res.cookie('jwt, accessToken, {
httpOnly: true,
sameSite: 'lax',
});
return req.user;
}
}
google-oauth.module.ts
import { Module } from "@nestjs/common";
import { JwtAuthModule } from "../jwt/jwt-auth.module";
import { GoogleOauthController } from "./google-oauth.controller";
import { GoogleOauthStrategy } from "./google-oauth.strategy";
@Module({
imports: [JwtAuthModule],
controllers: [GoogleOauthController],
providers: [GoogleOauthStrategy],
})
export class GoogleOauthModule {}
Step 3
We can now use @UseGuards(JwtAuthGuard)
to protect authenticated routes. GrapthQL as shown in the docs works out of the box!
Finally, we have end-to-end social auth with NestJS. You'll find many more cool features in the repo (https://github.com/thisismydesign/nestjs-starter). I've also written about an MVC setup combining Next.js and NestJS, and Automagically Typed GraphQL Queries and Results with Apollo.