The open blogging platform. Say no to algorithms and paywalls.

OAuth2 in NestJS for Social Login (Google, Facebook, Twitter, etc)

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.

image

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Ā andĀ passport-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.




Continue Learning