cover

NestJS: Google OAuth2 Authentication with Passport

7 Minutes

Overview

We will create project Nestjs using authentication with library passportjs, we will use OAuth strategy for this project.

NestJs

Nestjs is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript. Nestjs core engine still using expressjs as server library, so there is same concept between expressjs and nestjs. Nestjs support module system, dependency injection, middleware, and many more.

PassportJs

Passport is authentication middleware for Node.js. Extremely flexible and modular, Passport can be unobtrusively dropped in to any Express-based web application. - passportjs

We have target in this tutorial, We will use only small library for simplicity and no database, so you can follow quickly.

Setup Nestjs

Install packages

Prerequisite:

  • Nodejs
  • Typescript Skill

Run this in your terminal

$ npm i -g @nestjs/cli
$ nest new nestjs-with-auth

We will install these library:

# Install passport js with the types
$ npm install passport
$ npm install -D @types/passport

# install passport google oauth2.0
$ npm install passport-google-oauth20
$ npm install -D @types/passport-google-oauth20

# install passport nestjs library
$ npm install @nestjs/passport

Since passport google oauth 2.0 require session to make sure all process flow direct to spesific user. We will install express-session.

$ npm install express-session
$ npm install -D @types/express-session

Setup Configuration Environment

We had to setup environtment to make our nestjs read our .env configuration

$ npm install @nestjs/config

Import ConfigModule to your app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    })
  ],
  ...
})
export class AppModule {}

Setup session nestjs

We need to add session to file main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import session from 'express-session';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(
    session({
      secret: 'WRITE-YOUR-SECRET',
      resave: false,
      saveUninitialized: false,
      cookie: {
        secure: process.env.NODE_ENV === 'production',
        maxAge: 1000 * 60 * 60 * 24 * 7,
      },
    })
  )
  await app.listen(3000);
}
bootstrap();

Put your own secret on secret properties. Usually we put inside .env file.

...
    // Recommended method to use env to store session_secret
    session({
      secret: process.env.SESSION_SECRET,
      resave: false,
...

Protip: Use openssl rand -hex 32 to generate random string as secret key

Setup Auth Service

Create module, service and controller auth, we will store inside it.

$ nest g mo auth
$ nest g s auth
$ nest g co auth

All file auth will store at src/auth/auth.*.ts.

We will start from auth.service.ts to write some method to use later. First, we write method findUserByEmail, this method will search user into our database but for our example it only use if-else. DO NOT USE THIS IN PRODUCTION. You should change the logic to search to your database for security concern.

We also add interface User. The method will return null if no user found.

import { BadRequestException, Injectable } from '@nestjs/common';

export interface User {
  id: number;
  email: string;
  name: string;
}

@Injectable()
export class AuthService {
  async findUserByEmail(email:string): Promise<User|null>{
    // add logic to find user by email
    if(email === '[email protected]'){
      return {
        id: 1,
        email: '[email protected]',
        name: 'Rio chandra'
      }
    }
    return null
  }

And then, add method callbackOAuthGoogle. This method will be used at middleware google auth 2.0, we fetch the user and find user into our system using method findUserByEmail.

export class AuthService {
  ...
  async callbackOAuthGoogle(props: { accessToken: string, refreshToken: string, profile: any }) {
    const user = await this.findUserByEmail(props.profile.emails[0].value)
    if (!user) {
      throw new BadRequestException('User not found')
    }
    return user;
  }
}

Lastly, add new method login to return user information.

export class AuthService {
  ...
  async login(user: User) {
    const payload = {
      sub: user.id,
      email: user.email,
    };

    return payload
  }
}

You can improve this login method to generate token, but we will figure it out later.

Setup Google Auth strategy

First, you should add new OAuth at google console. Follow tutorial from google how to add new credentials. Store all configuration at your .env file.

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback

Setup GOOGLE_CALLBACK_URL to our path of auth callback, we will figure it out later.

Create new file at src/auth/strategy/GoogleOAuthStrategy.ts, and fill this configuration strategy. We will use @nestjs/passport to create injectable strategy as middleware passport later. We also need to store configuration google OAuth on .env file.

import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { Strategy } from "passport-google-oauth20";
import { AuthService } from "../auth.service";

@Injectable()
export class GoogleOauthStrategy extends PassportStrategy(Strategy, "google") {
  constructor(
    private readonly authService: AuthService,
  ) {
    super({
      clientID: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      callbackURL: process.env.GOOGLE_CALLBACK_URL,
      scope: ["email", "profile"],
      state: true,
    });
  }

  async validate(accessToken: string, refreshToken: string, profile: any) {
    return this.authService.callbackOAuthGoogle({
      profile,
      accessToken,
      refreshToken,
    });
  }
}

Key concept of passport is configuration of library and validation. validate method call our service callbackOAuthGoogle, If validation return falsy, it will throw error as unauthenticated. Configuration are setted at constructor based of our library passport-google-oauth20.

If you want to know other configuration of library passport-google-oauth20, read their repository.

Next step is register our strategy to auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { GoogleOauthStrategy } from './strategy/GoogleOAuthStrategy';

@Module({
  providers: [AuthService, GoogleOauthStrategy],
  controllers: [AuthController]
})
export class AuthModule {}

Setup API Authentication

We need to add new API to file auth.controller.ts.

  • GET /auth/google as entry API user to login and redirect to google OAuth 2.0.
  • GET /auth/google/callback as callback after user success login at google OAuth.
import { Controller, Get, Request, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(
    private readonly authservice: AuthService,
  ){}

  @UseGuards(AuthGuard('google'))
  @Get('google')
  async googleAuth() {
    return 'Google Auth';
  }

  @UseGuards(AuthGuard('google'))
  @Get('google/callback')
  async googleAuthCallback(@Request() req) {
    return this.authservice.login(req.user);
  }
}

Both path use AuthGuard to use middleware google as our strategy authentication, it will automatically generate url Google OAuth and validate it into our callback API.

At this point, you can use API localhost:3000/auth/google to login using Google Account

JWT token

We have been create authentication to validate user using google OAuth, next step is setup JWT Token.

Install package

Install package @nestjs/jwt

$ npm install @nestjs/jwt

Setup nestjs jwt to app.module.ts

Import @nestjs/jwt to app.module.ts and add configuration JWT_SECRET to your .env file.

We set global: true to make it accessable at auth.service.ts

import { JwtModule } from "@nestjs/jwt";

@Module({
  imports: [
    ...
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      global: true,
    }),
  ...
})
export class AppModule { }

Update your env to add JWT_SECRET

# env file
JWT_SECRET=------YOUR-SECRET-JWT-----

Update auth.service.ts

Update auth service method login to generate token for us.

Import JWT Service through constructor of AuthService

import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthService {
  constructor(
    private readonly jwtService: JwtService,
  ) { }
}

Update our login method to generate token using jwt service

export class AuthService {
  ....
  async login(user: User) {
    const payload = {
      sub: user.id,
      email: user.email,
    };
    const token = this.jwtService.sign(payload);
    return {
      access_token: token,
    };
  }
  ...
}

By doing so, we get our access token through API /auth/google and it will redirect automatically to /auth/google/callback by returning access token for us.

My Case

When I write this blog, this is my package.json configuration in case breaking update of the library so you should use this exact version to follow this article.

{ 
  "dependencies": {
    "@nestjs/common": "^10.0.0",
    "@nestjs/config": "^3.2.3",
    "@nestjs/core": "^10.0.0",
    "@nestjs/jwt": "^10.2.0",
    "@nestjs/passport": "^10.0.3",
    "@nestjs/platform-express": "^10.0.0",
    "express-session": "^1.18.0",
    "passport": "^0.7.0",
    "passport-google-oauth20": "^2.0.0",
    "reflect-metadata": "^0.2.0",
    "rxjs": "^7.8.1"
  },
  "devDependencies": {
    "@nestjs/cli": "^10.0.0",
    "@nestjs/schematics": "^10.0.0",
    "@nestjs/testing": "^10.0.0",
    "@types/express": "^4.17.17",
    "@types/express-session": "^1.18.0",
    "@types/jest": "^29.5.2",
    "@types/node": "^20.3.1",
    "@types/passport": "^1.0.16",
    "@types/passport-google-oauth20": "^2.0.16",
    "@types/supertest": "^6.0.0",
    "@typescript-eslint/eslint-plugin": "^7.0.0",
    "@typescript-eslint/parser": "^7.0.0",
    "eslint": "^8.42.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-prettier": "^5.0.0",
    "jest": "^29.5.0",
    "prettier": "^3.0.0",
    "source-map-support": "^0.5.21",
    "supertest": "^7.0.0",
    "ts-jest": "^29.1.0",
    "ts-loader": "^9.4.3",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "^4.2.0",
    "typescript": "^5.1.3"
  },

}

Conclusion

Setting up authentication in NestJS using Passport.js is straightforward, thanks to the hard work of the Passport.js contributors. NestJS offers seamless integration with Passport.js right out of the box.

If you want improve authentication by using authorization, I suggest you to read Authorization Tutorial By Nestjs for complete authentication flow.

If you found this article helpful, feel free to share it and stay tuned for my next post. Thank you!

comments powered by Disqus