How to implement refresh tokens JWT in NestJS Part-1

How to implement refresh tokens JWT in NestJS

Hello everyone, I’m back with another article about Nestjs, this time I want to present a brief tutorial on how to implement refresh tokens in Nestjs using Jwt and TypeORM first of all let’s first understand what are the advantages of implementing this strategy in our application.

Why do we need to refresh the token?

Many times the user who is logged in to our application stops interacting for certain seconds, as he starts to interact after this idle period by accessing the API routes that are restricted, the token he sends to our API in the header of his request is validated and given as expired, this is a bad scenario for our users’ usability, it was thinking about this problem that I decided to write this post and show you how we can implement a refresh token so that the logged-in user sends his expired token. Using the refresh token strategy can solve the problem presented since if a login is successful we will create two separate JWT tokens one will be the token valid for 15 minutes and the other will be a refresh token valid for more than a week.

How does refresh token work?

We need to save both tokens in localStorage even though we only use the accessToken to authorize the user to access private routes, when its expiration time is complete we will need to update this last token, we will create a route in our endpoint called /refresh to receive a new token, so it won’t be possible for the user to have to authenticate again.

Problems with vulnerability

We need to consider as a vulnerability the data leakage with the discovery of refresh token by cybercriminals because this is very sensitive data as well as the user’s password. We can address the problem by fixing the JWT secret but that would lead to the problem that all users would be logged out of our application, but we also have an alternative and which we will apply next would be to save the refresh token in the database for the user to do their login, with this we can check if the token kept in the database matches what the user is providing in his request, so we can invalidate a user’s token by removing it from our database.

Another detail that we need to bear in mind is that when the user logs out of the application, we need to delete the update token from the bank because if some cybercriminals discover the token and try to use it in order to obtain data from the user before this token expires, it will not be possible.

We identified above that refresh token is sensitive data, we can improve our solution above by doing the same strategy applied to passwords by hashing our refresh token before saving it to our database, so we can prevent cybercriminals from using it even if there is a leak from our database.

How to implement our solution

ConfigModule.forRoot({
  validationSchema: Joi.object({
    ACCESS_TOKEN_SECRET: Joi.string().required(),
    ACCESS_TOKEN_EXPIRATION: Joi.string().required(),
    REFRESH_TOKEN_SECRET: Joi.string().required(),
    REFRESH_TOKEN_EXPIRATION: Joi.string().required(),
    // ...
  })
}),

The first step we have to follow is to define our environment variables with the following data:

ACCESS_TOKEN_SECRET=secret token definitionACCESS_TOKEN_EXPIRATION=900sREFRESH_TOKEN_SECRET=secret refresh token definitionREFRESH_TOKEN_EXPIRATION=1y

With the variables defined we need to install the necessary dependency

$ npm i --save @nestjs/config

After the installation process is complete, we can import the ConfigModule into our AppModule and control its behavior using the static .forRoot() method

Next, we need to add the refresh_token column to our User entity.

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Exclude } from 'class-transformer';
@Entity({ name: 'users' })
class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true })
@Exclude()
refresh_token?: string;
// ...
}
export default User;

In our service, we will create a function with the method that we will perform the refresh token when the user triggers the request for our /refresh route

The ability to provide a secret when calling the jwtService method was added in version 7.1.0 of @nestjs/jwt


// ...
public createAccessTokenFromRefreshToken
(refreshToken: string) {
try {
  const decoded = this.jwtService.decode(refreshToken) as TokenPayload;
  if (!decoded) {
    throw new Error();
  }
  const user = await this.userService.getUserByEmail(decoded.email);
  if (!user) {
     throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND);
   }
  const isRefreshTokenMatching = await bcrypt.compare(refreshToken, user.refresh_token);
  if (!isRefreshTokenMatching) {
    throw new UnauthorizedException('Invalid token');
  }
    await this.jwtService.verifyAsync<Token>(refreshToken, this.getRefreshTokenOptions(user));
    return this.login(user);
  } catch {
    throw new UnauthorizedException('Invalid token');
  }
}
getRefreshTokenOptions(user: User): JwtSignOptions {
  return this.getTokenOptions('refresh', user);
}
private getTokenOptions(type: string, user: User) {
  const options: JwtSignOptions = {
    secret: this.config.get().refreshTokenSecret,
  };
  const expiration: string = this.config.get().refreshTokenExpiration;
  if (expiration) {
    options.expiresIn = expiration;
  }
  return options;
}
// ...
}

We need to create a method that will save the generated refresh_token in the database.

async setCurrentRefreshToken(refreshToken: string, userId: number) {
  const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10);
  
  return await this.userRepository.update(userId, {
    refresh_token: currentHashedRefreshToken
  });
}

Now we can handle the refresh token received and thus check if the token sent matches the one saved in the database, we can create our route to perform the refresh.

async removeRefreshToken(email: string) {
    const user = await this.userService.getUserByEmail(email);

    if (!user) {
      throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND);
    }

    return this.userRepository.update({email}, {
      refresh_token: null
    });
 }

Everything working! Now, let’s make our user delete the refresh token added at login or after his last token update request from the database when he logs out.

async removeRefreshToken(email: string) {
  const user = await this.userService.getUserByEmail(email);
  if (!user) {
    throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND);
  }
  return this.userRepository.update({email}, {
    refresh_token: null 
  });
}
  @Get('log-out')
  @ApiTags('Authentication')
  @ApiOperation({ description: 'Logout' })
  @UseGuards(JwtAuthGuard)
  @HttpCode(200)
  async logOut(@Request() req: any, @CurrentUser() user: GetUserDto) {
    await this.authService.removeRefreshToken(user.email);
    req.res.setHeader('Authorization', null);
  }

We added to our service the removeRefreshToken method as in the example below:

Finally, to finish developing our solution for refresh token we need to create our logout route that will be accessed by our user and execute the method created in the previous session.

Comments