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!