How to Integrate Google OAuth 2.0 in a NestJS Application


Introduction
Google OAuth is a protocol that allows applications to gain access to user data on Google services, such as their email address, first name, without requiring the user to share their password. It is a very simple authentication method that's widely used across various applications.
In this guide, I'll show you how to integrate Google OAuth 2.0 into your NestJS application. To achieve this, I'll use Passport, an authentication library.
Passport is an authentication library for Node.js. Passport has over 500 strategies that implement various authentication mechanisms, from Google OAuth to Facebook, GitHub, and so on.
The Google OAuth Workflow
User clicks “Sign in with Google” on our website, which triggers an endpoint in the backend that automatically redirects the user to the Google sign-in screen.
The user signs in with their Google account. If successful, Google redirects the user to a different endpoint in our backend where we receive the user information and extract the necessary ones, such as email, first name and last name.
Then, we check if the user record exists in our database. If it doesn’t, we save the user’s details to the database (according to the user schema we already defined) and return the user record. Also, if he does (that is, his record exists in the database), we simply return the user record.
From the user record returned, we issue a signed JWT with their ID from the database and store this JWT in a cookie (that will be used to make subsequent requests to protected routes) and redirect the authenticated user to the dashboard.
Prerequisites
To get the most out of this guide, you need to have a basic understanding of Node.js, TypeScript, and the NestJS framework. You will also need credentials such as Google Client ID and Google Client Secret to make the Google OAuth 2.0 process work. Here’s a simple and easy-to-follow guide on how to get those credentials if you don’t already have them.
Note: When you reach the screen shown in the image below, add http://localhost:3000 under “Authorized JavaScript origins” and http://localhost:3000/auth/google/callback under “Authorized redirect URIs“
http://localhost:3001/api/v1/auth/google/callback is the URL Google will redirect a user to after they sign in (we’ll work on this soon), and we used localhost because we are in development mode.
Scaffolding a New NestJS Application
I believe if you are here, you are not a complete beginner to NestJS, but here are the commands to scaffold a new NestJS application: nest new project-name
. The command requires that you have the Nest CLI already installed. If you don’t already have it installed, you need to run npm i -g @nestjs/cli
if you’re using npm, by the way.
Installing the Necessary Dependencies
Next, we need to install the following dependencies. Again, if you are using npm, run:
npm i @nestjs/config @nestjs/passport passport passport-google-oauth20 passport-jwt cookie-parser
npm i -D @types/passport-google-oauth20 @types/passport-jwt
These commands will install the required dependencies and their types. Here's an overview of each dependency:
@nestjs/config
allows access to environment variables (e.g., Google Client ID).passport
and@nestjs/passport
integrate Passport into our NestJS app.passport-google-oauth20
handles the Google OAuth 2.0 authentication strategy.passport-jwt
is used for handling JWT authentication.cookie-parser
helps parse cookies from incoming requests.@types/passport-google-oauth20
and@types/passport-jwt
provide TypeScript type definitions for Passport strategies.
Setting up the Google Strategy
We now have all that we need to integrate Google OAuth 2.0 into our NestJS app. In your project, create a module called ‘auth’ using the NestJS CLI with the command nest g module auth
Then, run another command nest g controller auth
and then nest g service auth
to create a controller and a service file for the auth module. Once you have these 2 files created, create a folder called ‘strategies’ inside the auth module (or folder).
Inside the newly created ‘strategies’ folder, create a file called google.strategy.ts
that will house the logic for the Google OAuth. Once created, add the following code to the file:
import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { UserService } from '../../user/user.service';
@Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
constructor(
private readonly userService: UserService,
private readonly configService: ConfigService,
) {
const clientID = configService.get<string>('CLIENT_ID');
const clientSecret = configService.get<string>('CLIENT_SECRET');
const callbackURL = configService.get<string>('CALLBACK_URL');
super({
clientID,
clientSecret,
callbackURL,
scope: ['email', 'profile'],
passReqToCallback: true,
});
}
async validate(
req: any,
accessToken: string,
refreshToken: string,
profile: any
): Promise<any> {
const { name, emails, photos } = profile;
const email = emails[0]?.value;
let user = await this.userService.findUser({ email });
if (!user) {
user = await this.userService.createUser({
email,
firstName: name?.givenName,
lastName: name?.familyName,
avatarUrl: photos[0]?.value,
});
}
return user;
}
}
Here, we created a GoogleStrategy class that extends PassportStrategy. The GoogleStrategy class takes 2 parameters: the Google OAuth 2.0 Strategy imported from passport-google-oauth20
and an identifier, ‘google.’
Then, in the constructor function, I called super() and passed the following options:
clientID: the client ID provided by Google.
clientSecret: the client secret provided by Google.
callbackURL: the redirect URL configured in the Google Cloud Console.
scopes: an array with the information we want to obtain from the user; the most common ones are profile and email.
passReqToCallback: with this option enabled, req will be passed as the first argument to the verify callback (this was usually optional in the previous versions, but if you don't pass it in the new version, you get an error).
We then created a method called validate(). It ensures that after Google authenticates a user, their profile information is extracted, and we either find an existing user record in the database or create a new one.
The validate() takes 4 parameters; req
, accessToken
, refreshToken
, profile
and returns the user so that Passport can complete its tasks (e.g., creating the user
property on the Request
object), and the request handling pipeline can continue.
userService
, which encapsulates user operations such as findUser
and createUser
, so you have to create a user module and a user service in your project.Creating the Auth Routes
Close the google.strategy.ts
file for now and open the auth.controller.ts
file. Then, add the code below to the file.
import {
Controller,
Get,
HttpStatus,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService,
) {}
@Get('google')
@UseGuards(AuthGuard('google'))
async googleAuth(@Req() req) {}
}
Here, I defined a /auth/google
route which uses an AuthGuard
provided by the @nestjs/passport
module. Then, I passed the identifier (‘google’) of the Google strategy we created earlier to the guard. With this, when you visit the route, you will be automatically redirected to the Google sign-in page.
The next part is to handle what happens after Google authenticates a user and redirects them to the redirect URL we configured in the Google Cloud Console. To implement this, update your auth.controller.ts
file as shown below:
import {
Controller,
Get,
HttpStatus,
Req,
Res,
UseGuards,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthGuard } from '@nestjs/passport';
import { ConfigService } from '@nestjs/config';
@Controller('auth')
export class AuthController {
constructor(
private readonly authService: AuthService,
private readonly configService: ConfigService,
) {}
@Get('google')
@UseGuards(AuthGuard('google'))
async googleAuth(@Req() req) {}
@Get('google/callback')
@UseGuards(AuthGuard('google'))
async googleAuthRedirect(@Req() req, @Res() res) {
const expiresIn = this.configService.get<string>('JWT_EXPIRES_IN');
const token = await this.authService.login(req.user);
res.cookie('access_token', token, {
httpOnly: true,
sameSite: 'strict',
secure: `${this.configService.get<string>('NODE_ENV')}` === 'production',
maxAge: 24 * 60 * 1000,
});
return res.redirect(this.configService.get<string>('FRONTEND_URL')/dashboard);
}
}
I defined a 2nd route /auth/google/callback
(the redirect URL we specified in the Google Cloud Console). In this method, we can access the user object in the Request with req.user
that I passed to the login
method in the authService
. The method returns a signed JWT with the user’s ID. Then, I returned a cookie with the token and redirected the user to the dashboard.
The token stored in the cookie can then be used for subsequent requests to the protected routes on the server.
Here’s what the auth.service.ts
looks like:
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserDocument } from '../user/schemas/user.schema';
@Injectable()
export class AuthService {
constructor(private readonly jwtService: JwtService) {}
async login(user: UserDocument): string {
return this.jwtService.sign({ sub: user._id });
}
}
Setting up JWT Strategy
The job is almost done. Now, users can successfully sign in with Google and get a JWT. What’s left is to create a JWT strategy to extract and validate the tokens received in the cookies when the user wants to access protected routes on the server. To do this, create a file named jwt.strategy.ts
file in the strategies folder and add the code below to it:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-jwt';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: (req) => {
if (!req || !req.cookies) return null;
return req.cookies.access_token;
},
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
passReqToCallback: true,
});
}
async validate(req: any, payload: any) {
return { userId: payload.sub };
}
}
Here, I created a new strategy called jwt and passed the following options to super():
jwtFromRequest: a function that receives the request object as a parameter and checks if the access_token cookie exists; if it does, it returns it, else it returns null.
ignoreExpiration: a Boolean option telling Passport to consider the token expiration date or not.
secretOrKey: the JWT secret key from our environment variables, to validate the token's signature.
Then, I created a validate(). Passport will call this method if the token is valid, if it is, the decoded userId is returned and attached to the user object on the Request object.
With this in place, open your auth.module.ts
file and ensure it looks like this:
import { Module } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { GoogleStrategy } from './strategies/google.strategy';
import { UserModule } from '../user/user.module';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtStrategy } from './strategies/jwt.strategy';
@Module({
imports: [
UserModule,
JwtModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: configService.get<string>('JWT_EXPIRES_IN') },
}),
}),
],
controllers: [AuthController],
providers: [AuthService, GoogleStrategy, JwtStrategy],
})
export class AuthModule {}
With our JwtStrategy set up, we can now protect any route. For example, we can create a /profile
route to get user’s profile as thus:
import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('users')
export class UserController {
@Get()
@UseGuards(AuthGuard('jwt'))
getProfile(@Req() req) {
return `This is your profile. Your ID is ${req.user.userId}`;
}
}
Conclusion
In this guide, we explored how to easily integrate Google OAuth 2.0 into a NestJS application using Passport.js. We went through setting up the Google strategy, issuing JWTs, and creating a custom JWT strategy to secure protected routes. Now, your NestJS application can authenticate users through Google and protect sensitive routes with JWT-based authentication. Feel free to go through the docs to better understand how to integrate other strategies provided by Passport into your application.
Subscribe to my newsletter
Read articles from Abdul-Hafiz Aderemi directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
