Thought leadership from the most innovative tech companies, all in one place.

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 (12) and resources elsewhere (12) 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