NestJS + PassportJS in depth not shallow
Mohammad Jawad barati
Software Engineer | Fullstack Engineer | Microservices | TS/JS | Python | IaC | AWS | TDD | BDD | Automation | CI/CD
We all know what does PassportJS did for us, It make our life a lot easier and modular. I mean we can simply implement a complete auth module and use it almost everywhere. We can add OAuth simpler and modular.
But in outside world - I meant ExpressJS, and other libs that does not provide any form of structure and you have to take care off anything - we do need to be in a modular codebase to do so, I mean we need to build that modularity to reach this goal. But In NestJS we can do it literally right away.
NestJS devs write a wrapper around PassportJS to make it more adjustable with NestJS. We need @nestjs/passport lib to do it. In this post I use JWT as I developed many RESTful APIs.
How PassportJS works?
It is a mini framework IMO:
- It abstract the Auth for us
- Accept configurations - JSON object that we pass to it - and a callback which will be invoked at the appropriate time (see a simple example here).
- Support different strategies
- Verify callback is where you can access the decoded JWT information - Recall that usually we create a JWT on signin. - and the magic is that we do not need to do those steps by hand. Now you can simply check if userId is in your database or not.
- If user does not found or password does not match you need just to return null. It means that throw a 403 error.
What we do in NestJS to configure local strategy?
Installation packages
npm i @nestjs/passport passport passport-local
npm i -D @types/passport-local
Some notes about installation:
- No matter which strategy you wanna use in you APP, You need @nestjs/passport, and passport itself
- Instasll @types for sake of assistant while wring TS codes.
Create necessary modules
First we should define our strategy, But first let's create Auth module with the help of NestJS cli. I love it. It help me to prevent writing boilerplate codes in most cases:
nest g module modules/auth
nest g service modules/auth
- I like to put my modules inside the modules directory instead of src.
BTW we do not wanna to write spaghetti code, right? So that's why we need to create user module too:
nest g module modules/user
nest g service modules/user
- In this way we will have a good separation of concerns. Our AuthService duty is to verify password, send OTP, and our UserService duty is to retrieve user from database.
Your important task #1:
Now in the UserService class we should implement our logic to fetch a user by username, and other methods. I delegate it to you, You can do google and implement this section first. So here is your task:
- Write a repository layer inside your user module. Do not forget to make it provider. As always I am a huge fan of using NestJS cli:
nest g provider modules/user/user.repository
- You can use Prisma, or TypeORM, MicroORM, or any ORM that you like and write your users table/collection in it.
- Before diving into implementing your repo layer write its tests in the user.repository.spec.ts file and run the tests. In this way you learn to follow TDD principle - Write your test first and then run it, at first it fails obviously. But you have to make it pass by implementing the repository layer - and you will face less pain because you're developing your app incrementally and test it step by step instead of once a whole system.
- Now implement CRUD operation in your repository layer.
- It's time to update your user.service.ts file - As I said write your tests first, in its file which is user.service.spec.ts and then run unit tests - to return the user by its username, and its userId.
- Export UserService in user.module.ts and import it in the AuthModule. A quick note about this part: Every class which is marked by @Injectable is a provider and we can use them through DI :smile:.
Implement Necessary part in AuthService
Create a validateUser method in it which accepts username, and password. Here is what we need to do:
import { Injectable } from '@nestjs/common';
import { UserService } from '../users/user.service';
@Injectable()
export class AuthService {
// Note: DI take care of instantiating userService for us
? constructor(private userService: UserService) {}
// Note: change any to User type if you are using Prisma
async validateUser(username: string, password: string): Promise<any> {
? ? ? ? const user = await this.usersService.findOne(username);
? ? // Note: if you wanna use bcryptjs feel free to change this section to use bcryptjs.compare
? ? ? ? if (user && user.password === password) {
? ? // Remove everything that you do not wanna put in the req.user
? ? ? ? ? const { password, ...result } = user;
? ? ? ? ? return result;
? ? ? ? }
? ? // Note: As I wrote earlier return null is the same to throw 403 error. But we do not do it here because we wanna do it in another layer.
? ? ? ? return null;
? }
};
Now let's implement passport local strategy
We simply need to execute this command in terminal to do the magic for us:
nest g provider modules/auth/local/local.strategy --no-spec
- Note: I love to keep things clean, therefore as will explore more about this section this is a good practice to keep each strategy in its directory.
- This time I used an extra flag to tell NestJS I do not wanna have a spec file for my strategy. TBH I am not sure should we write unit tests for strategies.
With this in place we have all we need. it is necessary to create our strategies provider. TBH at the time I am writing this article IDK why :sweat_smile:. BTW my gut tells me something, I guess it is something related to how @nestjs/passport works behind the sense, I mean it seems that we are registering our strategy. But whenever I was sure I will update this article.
Open the local.strategy.ts and write this codes in it, More details in comments:
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';
// IDK why we have to make strategies provider. if you know leave a comment in the comment section.
@Injectable()
// Note: Strategy imported from passport-local. This is a common pattern for all strategies.
// We configure strategies by inheriting from PassportStrategy
export class LocalStrategy extends PassportStrategy(Strategy) {
? constructor(private authService: AuthService) {
// If any configuration will be needed we can pass them as a object while calling super. We will see it in the JWT strategy.
// In this strategy we can pass this object: {usernameField: 'email'}
// As you know the req.body with default conf will be a JSON like this: { username: 'something', password: '123' }
// But if we change super() to super({ usernameField: 'email' })
// We have to send this JSON: { email: 'e@e.com', password: '123' }
? ? super();
? }
// As always if you are using Prisma change any to User. This is why I love Prisma.
? async validate(username: string, password: string): Promise<any> {
? ? const user = await this.authService.validateUser(username, password);
? ? if (!user) {
// This is where I said we will throw 403 error. Read comments in the validateUser method in the AuthService
? ? throw new UnauthorizedException();
? ? }
? ? return user;
}
};
- Note: validate method is the same as verify function we used to specify while configuring new strategies. So validate method should be provided for almost every strategy.
- If everything goes well passport can do what it has to do, attaching returned user from AuthService to the request object.
- In validate method we can do more logic: evaluate whether the userId carried in the decoded token matches a record in our database, or matches a list of revoked tokens.
- Do not rush, This is not complete. Not yet. We need to issue a JWT for user whenever the passed credentials - username, password - were OK.
Login endpoint
Now we have our local strategy configured but we also need to define login endpoint in the AuthController:
import {
? ? Body,
? ? Controller,
? ? Post,
? ? UseGuards,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { AuthService } from './auth.service';
import { LoginDto } from './dto';
@ApiTags('authentication')
@Controller('auth')
export class AuthController {
? ? constructor(private readonly authService: AuthService) {}
// FIXME: we need a guard here
? ? @Post('login')
? ? async login(
// I like to do this, Because in this way I am telling this endpoints accept what as in req.body
? ? ? ? @Body() loginCredentials: LoginDto,
// Note: you can change any to Request exported from express if you are using ExpressJS
? ? ? ? @Request() req: any,
? ? ): Promise<{ accessToken: string }> {
// Here we could even use a serializer to serialize response.
? ? ? ? return await this.authService.login(req.user);
? ? }
}
Your important task #2:
As you see we need login method too. I will delegate this one to you again. Here is the instruction:
- Write its test in auth.service.spec.ts file. Execute unit tests
- Now the test is failing and you need to implement it. This method as you see accept one argument, user and it should returns a object which inside it is the accessToken.
- Generate accessToken with @nestjs/jwt, It is as simple as installing @nestjs/jwt and injecting it in the AuthService. Note: put userId in sub key to keep your code aligned with JWT standard.
After All it's time to implement our local guards
- Guard is responsible to authenticate or authorize incoming requests.
- Client sends us the user credentials - username, password - and now we need to run verify function and attach user property to the request. This tedious task done by guard. This is why I am bewitched by NestJS.
To create our guard we can simply use NestJS cli :joy:
?nest g guard modules/auth/local/local-auth --no-spec
- This time again I added --no-spec flag to prevent creating unit test files.
领英推è
Now open the local-auth.guard.ts and update it like this:
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') {};
And now we can update our endpoint:
@UseGuards(LocalAuthGuard)
@Post('login')
async login(
@Body() loginCredentials: LoginDto,
? ? @Request() req: any,
): Promise<{ accessToken: string }> {
return await this.authService.login(req.user);
}
That's all we need to do to configure our login endpoint. Stay tuned. In the following section I'll tell you how to configure your JWT strategy.
JWT strategy
I guess you are already familiar with JWT and what is it. But if you're not do not hesitate. I'll tell you everything you need to know:
- JWT is an open standard (RFC7519)
- A compact and self contained way to securely transmit data between parties as JSON object.
- Why is it trusted while we can take a JWT and put it in the jwt.io website and see its data? It is trusted because nobody else except the one whom has the secret or private key - In case that you wanna use RSA - can generate JWT tokens and those who has secret or public key can verify it, but we should not put sensitive data in it.
- BTW We can cut the last part - Signature - of our tokens and in this way nobody can decode our tokens too. This one is a little tricky and handy in some cases. However I do not wanna make you confused, So leave it for now. :joy:
- JWT has 3 part separated with dot. the Header, Payload, and Signature. A formal JWT looks like this: header.payload.signature
- We can use JWT to Authorize or transform data.
Now that you have a solid understanding about JWT we can go further and install what we need:
Installation packages
npm i @nestjs/jwt passport-jwt
npm i -D @types/passport-jwt
- We already have @nestjs/jwt in place. Remember the last parts in local strategy that we signed a token using @nestjs/jwt package, I am talking about "your important task #2".
Update your AuthService
For sake of having a clean module, and separate concerns we generate and verify JWT tokens in the AuthService. As you should know - If you did "your important task #2" well - you have to import JwtModule in the auth.module.ts:
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { UserModule } from '../user/user.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { LocalStrategy } from '../local/local.strategy';
@Module({
? ? imports: [
? ? ? ? JwtModule.registerAsync({
? ? ? ? ? ? imports: [ConfigModule],
? ? ? ? ? ? inject: [ConfigService],
// Here we can do this much much better. Please see this repo: https://github.com/kasir-barati/you-say/blob/main/src/packages/auth/auth.module.ts
// IMO it is much much cleaner and easier to maintain. Besides it is safer also.
? ? ? ? ? ? useFactory: async (configService: ConfigService) => ({
? ? ? ? ? ? ? ? secret: configService.get('JWT_SECRET_KEY'),
? ? ? ? ? ? ? ? signOptions: {
// I assume that JWT_EXPIRATION is a number in second
? ? ? ? ? ? ? ? ? ? expiresIn: `${configService.get('JWT_EXPIRATION')}s`,
? ? ? ? ? ? ? ? },
? ? ? ? ? ? }),
? ? ? ? }),
? ? ? ? UserModule,
? ? ? ? PassportModule,
? ? ],
? ? controllers: [AuthController],
? ? providers: [AuthService, LocalStrategy],
})
export class AuthModule {};
Now let's implement JWT strategy
What else we can do for our endpoints? Yes protecting some of them. I mean some endpoints should be only available if you were logged in. Here we ask client to provide JWT in request to access specific endpoints. So:
nest g provider modules/auth/jwt/jwt.strategy --no-spec
As always open your jwt.strategy.ts and update it. Please read comments carefully:
import { Injectable } from '@nestjs/common';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '@nestjs/passport';
import { ConfigType } from '@nestjs/config';
import authConfig from '../configs/auth.config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
? ? constructor(configService: ConfigService) {
? ? ? ? super({
? ? ? ? ? ? jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
? ? ? ? ? ? ignoreExpiration: false,
// I handle this part in this repo better that this solution. But I forced to write it in this way to keep things simpler: https://github.com/kasir-barati/you-say/blob/main/src/packages/auth/jwt/jwt.strategy.ts
? ? ? ? ? ? secretOrKey: configService.get('JWT_SECRET_KEY'),
? ? ? ? });
? ? }
// As I wrote in the local strategy section in most cases we should write validate method for each strategy.
? ? async validate(payload: any) {
// Do not be mad at me, You can put anything in you JWT payload but recall what I wrote while introducing JWT to you
? ? ? ? return { userId: payload.sub, username: payload.username };
? ? }
}
- jwtFromRequest: This option tell strategy to llok where toward jwt token in the request's headers.
- ignoreExpiration: We delegate validating passed JWT tokens of incoming requests to the passport. BTW you can ignore this conf, because false is the default value.
- secretOrKey: The secret which the token was signed with it. Keep this secret in a secure place like GitHub. Kidding, use something like HashiCorp Vault, or docker secrets. BTW I am not sure about docker secret also.
validate method in JWT strategy
What will PassportJS will do for us:
- Verify JWT's signature and decode it
- Invoke the validate method with decoded JWT token as the only passed argument.
- Returned value from validate method will attach to request object. So it can be a good decision to fetch user info from database and return it.
Side note, but important. Copied from NestJS doc: we can "looking up the userId in a list of revoked tokens, enabling us to perform token revocation". I am not sure how I should do this, BTW i guess it means that we save revoked tokens by userId but it is somehow weird and impossible to me. Please help me if you know what it means. or better provide me a link to a source code which has this token revoke in place.
Now let's implement the JWT guard
It is simple:
nest g guard modules/auth/jwt/jwt-auth --no-spec
open the jwt-auth.guard.ts and paste the following codes in it:
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {};
Now you can put this guard on each controller or endpoint that you wanna protect it.
@UseGuards(JwtAuthGuard)
- In the req.user is the returned object from the jwt.strategy.ts validate method.
You can define a @GetUser decorator to make your life easier. This decorator will extract user from req object and return it. Let me tell you how to create it, first run the following command in the terminal:
nest g decorator shared/decorators/get-user
- Now see why I love to use NestJS cli as much as it is possible
- This decorator is a general purpose decorator. That's why I put it in shared directory. We will create our general decorators in this directory
Now we need to update what is inside the get-user.decorator.ts:
import {
? ? createParamDecorator,
? ? ExecutionContext,
} from '@nestjs/common';
// Change any to User if you are using Prisma.
export const GetUser = createParamDecorator(
? ? (data: any, ctx: ExecutionContext): any => {
? ? ? ? const request = ctx.switchToHttp().getRequest();
// As you see we returned the user.
? ? ? ? return request.user;
? ? },
);
Global guard
Another note: we can define global guards by putting this object in our providers in app.module.ts:
{
? ? provide: APP_GUARD,
? ? useClass: JwtAuthGuard,
}
Now if you wanna make some endpoints public you can follow this instruction in the official doc.
I hope you enjoyed this article. Do not forgot, follow my instructions step by step.