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 32to 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/googleas entry API user to login and redirect to google OAuth 2.0.GET /auth/google/callbackas 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!
