In the previous article, we understand the steps to generate the JWT token and store it in the cookie. Now here we will understand steps to protect API and also about refresh token.
Install Random Token And MomentJS NPM Package:
Wrapping Up:
Install passport-jwt NPM Package:
We have to create a new jwt passport strategy to validate the jwt token, so we need to install the below packages.
Command To Install passport-jwt Packages:
npm install --save passport-jwt
npm install --save-dev @types/passport-jwt
Install And Setup Cookie Parser:
To read the cookie in the nestjs application we have to install the below plugin.
src/main.ts:
Command To Install Cookie Parser:
$ npm i cookie-parser
$ npm i -D @types/cookie-parser
Now configure the cookie parser 'main.ts'src/main.ts:
Create JWT Passport Strategy:
So to apply authentication to API's we have to validate our jwt token, so to do that we need to create a new jwt passport strategy.
src/users/jwt.strategy.ts:
import { Injectable, UnauthorizedException } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { Request } from "express"; import { ExtractJwt, Strategy } from "passport-jwt"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy,'jwt') { constructor(){ super({ ignoreExpiration: false, secretOrKey:"My random secret key never let others", jwtFromRequest:ExtractJwt.fromExtractors([(request:Request) => { let data = request?.cookies["auth-cookie"]; if(!data){ return null; } return data.token }]) }); } async validate(payload:any){ if(payload === null){ throw new UnauthorizedException(); } return payload; } }
- (Line: 7) Created our 'JwtStrategy'. We know that to create a strategy class must inherit the 'PassportStartegy' method that loads from '@nestjs/passport'. In the 'PassportStrategy' here we passed 2 params like 'Strategy' that loads from 'passport-jwt' library and another parameter is the name of the strategy(eg: 'jwt').
- (Line: 10) Set 'ignoreExpiration' to false, which means validating the expiration time of the jwt token.
- (Line: 11) The 'secretOrKey' value is used for decrypting the jwt token. The value must match with the value using in the 'JwtModule' in 'UserModule'.
- (Line: 12-18) Fetching the jwt token from the auth cookie.
- (Line: 22-27) The 'validate' method gets invoked automatically, here it receives the user information as a payload from the jwt token.
src/users/user.modulet.ts:
import { LocalStrategy } from './local.strategy'; // code hidden for display purpose @Module({ providers: [JwtStrategy], }) export class UsersModule {}
Create A Secure Endpoint:
Now let's create a sample secured endpoint to test our jwt authentication.
src/users/users.controller.cs:
@Get('fav-movies') @UseGuards(AuthGuard('jwt')) async movies(@Req() req){ return ["Avatar", "Avengers"]; }
- Here we enable jwt authentication by invoking the jwt strategy for validating the token.
Refresh Token Flow:
- Refresh Token is a random string key that will be created along with the JWT access token and return to the valid client on successful logging in.
- Now for all subsequent requests will use the access token, but the access token is a short-lived token whereas the refresh token lives more time than the access token.
- On the expiration of the access token, the user instead of authenticating himself again passing his user name and password, the user can send the refresh token.
- The server on receiving a refresh token first validates against the storage(database, cache, etc).
- For a valid refresh token server will create a new access token and refresh token(like when authenticate using user name and password) return it to the user.
Install Random Token And MomentJS NPM Package:
Now to we will use a randomly generated string value as the refresh token. So to generate the random string we need the blow plugin to be installed.
Command To Install Random Token:
npm install rand-token --save
To deal with date and time more effectively we will use the 'momentjs'.
Command To Install MomentJS:
npm install moment --save
Generate Refresh Token And Its Expiration:
Now at the time of user login along with the jwt token, we also need to generate the refresh token.
src/users/users.service.ts:
import * as randomToken from 'rand-token'; import * as moment from 'moment'; public async getRefreshToken(userId: number): Promise<string> { const userDataToUpdate = { refreshToken: randomToken.generate(16), refreshTokenExp: moment().day(1).format('YYYY/MM/DD'), }; await this.user.update(userId, userDataToUpdate); return userDataToUpdate.refreshToken; }
- Here we generate the 16 character length random string which we used as a refresh token. Defined the refresh token expiration for 1 day. Save the token and expiration to the database and finally returning the refresh token value as a result.
src/users/users.controller.cs:
Refresh Token Validation Strategy:
Using 'passport-jwt' library let's create a one more strategy for validating the refresh token.
src/users/refresh.strategy.ts:
import { BadRequestException, Injectable, UnauthorizedException } from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { Request } from "express"; import { ExtractJwt, Strategy } from "passport-jwt"; import { UsersService } from "./users.service"; @Injectable() export class RefreshStrategy extends PassportStrategy(Strategy, 'refresh') { constructor(private userService:UsersService){ super({ ignoreExpiration: true, passReqToCallback:true, secretOrKey:"My random secret key never let others", jwtFromRequest:ExtractJwt.fromExtractors([(request:Request) => { let data = request?.cookies["auth-cookie"]; if(!data){ return null; } return data.token }]) }) } async validate(req:Request, payload:any){ if(!payload){ throw new BadRequestException('invalid jwt token'); } let data = req?.cookies["auth-cookie"]; if(!data?.refreshToken){ throw new BadRequestException('invalid refresh token'); } let user = await this.userService.validRefreshToken(payload.email, data.refreshToken); if(!user){ throw new BadRequestException('token expired'); } return user; } }
- (Line: 8) Defined the our strategy name as 'refresh'.
- (Line: 11) The 'ignoreEpriation' set to 'true' because we know that our jwt access token is expired and we don't want our strategy to validate the jwt token expiration. The reason behind reading jwt token is to get the user email , so that we can check the refresh token of the user along with the email in the database.
- (Line: 12) The 'passReqToCallback' set to 'true' means we want to inject the request object into 'validate' method as first parameter. The jwt strategy by default injects only decrypted user information.
- (Line: 24-38) Fetching lates user information from the database by querying with user email and refreshtoken and its expiration date as well.
src/users/refresh.strategy.ts:
import { RefreshStrategy } from './refresh.strategy'; @Module({ providers: [RefreshStrategy], }) export class UsersModule {}
Create Refresh Token Endpoint:
Now let's create our refresh token endpoint which is almost similar to our login endpoint, but only difference refresh token endpoint invokes the 'RefreshStrategy'.
src/users/users.controller.ts:
@Get('refresh-tokens') @UseGuards(AuthGuard('refresh')) async regenerateTokens( @Req() req, @Res({ passthrough: true }) res: Response, ) { const token = await this.userService.getJwtToken(req.user as CurrentUser); const refreshToken = await this.userService.getRefreshToken( req.user.userId, ); const secretData = { token, refreshToken, }; res.cookie('auth-cookie', secretData, { httpOnly: true }); return {msg:'success'}; }Now we can invoke the refresh token to generate the new auth-cookie.
So that's all about the strategy to protecting an API and generating refresh token.
Video Session:
Support Me!
Buy Me A Coffee
PayPal Me
Wrapping Up:
Hopefully, I think this article delivered some useful information on generating refresh token and save it to HttpOnly Jwt Auth Cookie in the NestJS application. I love to have your feedback, suggestions, and better techniques in the comment section below.
Thanks!
ReplyDeleteGreat article. Thank you.
ReplyDeleteGreat
ReplyDeletehttp://localhost:3000/users/fav-movies
{
"statusCode": 400,
"message": "Bad Request"
}