- 1. API with NestJS #1. Controllers, routing and the module structure
- 2. API with NestJS #2. Setting up a PostgreSQL database with TypeORM
- 3. API with NestJS #3. Authenticating users with bcrypt, Passport, JWT, and cookies
- 4. API with NestJS #4. Error handling and data validation
- 5. API with NestJS #5. Serializing the response with interceptors
- 6. API with NestJS #6. Looking into dependency injection and modules
- 7. API with NestJS #7. Creating relationships with Postgres and TypeORM
- 8. API with NestJS #8. Writing unit tests
- 9. API with NestJS #9. Testing services and controllers with integration tests
- 10. API with NestJS #10. Uploading public files to Amazon S3
- 11. API with NestJS #11. Managing private files with Amazon S3
- 12. API with NestJS #12. Introduction to Elasticsearch
- 13. API with NestJS #13. Implementing refresh tokens using JWT
- 14. API with NestJS #14. Improving performance of our Postgres database with indexes
- 15. API with NestJS #15. Defining transactions with PostgreSQL and TypeORM
- 16. API with NestJS #16. Using the array data type with PostgreSQL and TypeORM
- 17. API with NestJS #17. Offset and keyset pagination with PostgreSQL and TypeORM
- 18. API with NestJS #18. Exploring the idea of microservices
- 19. API with NestJS #19. Using RabbitMQ to communicate with microservices
- 20. API with NestJS #20. Communicating with microservices using the gRPC framework
- 21. API with NestJS #21. An introduction to CQRS
- 22. API with NestJS #22. Storing JSON with PostgreSQL and TypeORM
- 23. API with NestJS #23. Implementing in-memory cache to increase the performance
- 24. API with NestJS #24. Cache with Redis. Running the app in a Node.js cluster
- 25. API with NestJS #25. Sending scheduled emails with cron and Nodemailer
- 26. API with NestJS #26. Real-time chat with WebSockets
- 27. API with NestJS #27. Introduction to GraphQL. Queries, mutations, and authentication
- 28. API with NestJS #28. Dealing in the N + 1 problem in GraphQL
- 29. API with NestJS #29. Real-time updates with GraphQL subscriptions
- 30. API with NestJS #30. Scalar types in GraphQL
- 31. API with NestJS #31. Two-factor authentication
- 32. API with NestJS #32. Introduction to Prisma with PostgreSQL
- 33. API with NestJS #33. Managing PostgreSQL relationships with Prisma
- 34. API with NestJS #34. Handling CPU-intensive tasks with queues
- 35. API with NestJS #35. Using server-side sessions instead of JSON Web Tokens
- 36. API with NestJS #36. Introduction to Stripe with React
- 37. API with NestJS #37. Using Stripe to save credit cards for future use
- 38. API with NestJS #38. Setting up recurring payments via subscriptions with Stripe
- 39. API with NestJS #39. Reacting to Stripe events with webhooks
- 40. API with NestJS #40. Confirming the email address
- 41. API with NestJS #41. Verifying phone numbers and sending SMS messages with Twilio
- 42. API with NestJS #42. Authenticating users with Google
- 43. API with NestJS #43. Introduction to MongoDB
- 44. API with NestJS #44. Implementing relationships with MongoDB
- 45. API with NestJS #45. Virtual properties with MongoDB and Mongoose
- 46. API with NestJS #46. Managing transactions with MongoDB and Mongoose
- 47. API with NestJS #47. Implementing pagination with MongoDB and Mongoose
- 48. API with NestJS #48. Definining indexes with MongoDB and Mongoose
- 49. API with NestJS #49. Updating with PUT and PATCH with MongoDB and Mongoose
- 50. API with NestJS #50. Introduction to logging with the built-in logger and TypeORM
- 51. API with NestJS #51. Health checks with Terminus and Datadog
- 52. API with NestJS #52. Generating documentation with Compodoc and JSDoc
- 53. API with NestJS #53. Implementing soft deletes with PostgreSQL and TypeORM
- 54. API with NestJS #54. Storing files inside a PostgreSQL database
- 55. API with NestJS #55. Uploading files to the server
- 56. API with NestJS #56. Authorization with roles and claims
- 57. API with NestJS #57. Composing classes with the mixin pattern
- 58. API with NestJS #58. Using ETag to implement cache and save bandwidth
- 59. API with NestJS #59. Introduction to a monorepo with Lerna and Yarn workspaces
- 60. API with NestJS #60. The OpenAPI specification and Swagger
- 61. API with NestJS #61. Dealing with circular dependencies
- 62. API with NestJS #62. Introduction to MikroORM with PostgreSQL
- 63. API with NestJS #63. Relationships with PostgreSQL and MikroORM
- 64. API with NestJS #64. Transactions with PostgreSQL and MikroORM
- 65. API with NestJS #65. Implementing soft deletes using MikroORM and filters
- 66. API with NestJS #66. Improving PostgreSQL performance with indexes using MikroORM
- 67. API with NestJS #67. Migrating to TypeORM 0.3
- 68. API with NestJS #68. Interacting with the application through REPL
- 69. API with NestJS #69. Database migrations with TypeORM
- 70. API with NestJS #70. Defining dynamic modules
- 71. API with NestJS #71. Introduction to feature flags
- 72. API with NestJS #72. Working with PostgreSQL using raw SQL queries
- 73. API with NestJS #73. One-to-one relationships with raw SQL queries
- 74. API with NestJS #74. Designing many-to-one relationships using raw SQL queries
- 75. API with NestJS #75. Many-to-many relationships using raw SQL queries
- 76. API with NestJS #76. Working with transactions using raw SQL queries
- 77. API with NestJS #77. Offset and keyset pagination with raw SQL queries
- 78. API with NestJS #78. Generating statistics using aggregate functions in raw SQL
- 79. API with NestJS #79. Implementing searching with pattern matching and raw SQL
- 80. API with NestJS #80. Updating entities with PUT and PATCH using raw SQL queries
- 81. API with NestJS #81. Soft deletes with raw SQL queries
- 82. API with NestJS #82. Introduction to indexes with raw SQL queries
- 83. API with NestJS #83. Text search with tsvector and raw SQL
- 84. API with NestJS #84. Implementing filtering using subqueries with raw SQL
- 85. API with NestJS #85. Defining constraints with raw SQL
- 86. API with NestJS #86. Logging with the built-in logger when using raw SQL
- 87. API with NestJS #87. Writing unit tests in a project with raw SQL
- 88. API with NestJS #88. Testing a project with raw SQL using integration tests
- 89. API with NestJS #89. Replacing Express with Fastify
- 90. API with NestJS #90. Using various types of SQL joins
- 91. API with NestJS #91. Dockerizing a NestJS API with Docker Compose
- 92. API with NestJS #92. Increasing the developer experience with Docker Compose
- 93. API with NestJS #93. Deploying a NestJS app with Amazon ECS and RDS
- 94. API with NestJS #94. Deploying multiple instances on AWS with a load balancer
- 95. API with NestJS #95. CI/CD with Amazon ECS and GitHub Actions
- 96. API with NestJS #96. Running unit tests with CI/CD and GitHub Actions
- 97. API with NestJS #97. Introduction to managing logs with Amazon CloudWatch
- 98. API with NestJS #98. Health checks with Terminus and Amazon ECS
- 99. API with NestJS #99. Scaling the number of application instances with Amazon ECS
- 100. API with NestJS #100. The HTTPS protocol with Route 53 and AWS Certificate Manager
- 101. API with NestJS #101. Managing sensitive data using the AWS Secrets Manager
- 102. API with NestJS #102. Writing unit tests with Prisma
- 103. API with NestJS #103. Integration tests with Prisma
- 104. API with NestJS #104. Writing transactions with Prisma
- 105. API with NestJS #105. Implementing soft deletes with Prisma and middleware
- 106. API with NestJS #106. Improving performance through indexes with Prisma
- 107. API with NestJS #107. Offset and keyset pagination with Prisma
- 108. API with NestJS #108. Date and time with Prisma and PostgreSQL
- 109. API with NestJS #109. Arrays with PostgreSQL and Prisma
- 110. API with NestJS #110. Managing JSON data with PostgreSQL and Prisma
- 111. API with NestJS #111. Constraints with PostgreSQL and Prisma
- 112. API with NestJS #112. Serializing the response with Prisma
- 113. API with NestJS #113. Logging with Prisma
- 114. API with NestJS #114. Modifying data using PUT and PATCH methods with Prisma
- 115. API with NestJS #115. Database migrations with Prisma
- 116. API with NestJS #116. REST API versioning
- 117. API with NestJS #117. CORS – Cross-Origin Resource Sharing
- 118. API with NestJS #118. Uploading and streaming videos
- 119. API with NestJS #119. Type-safe SQL queries with Kysely and PostgreSQL
- 120. API with NestJS #120. One-to-one relationships with the Kysely query builder
- 121. API with NestJS #121. Many-to-one relationships with PostgreSQL and Kysely
- 122. API with NestJS #122. Many-to-many relationships with Kysely and PostgreSQL
- 123. API with NestJS #123. SQL transactions with Kysely
- 124. API with NestJS #124. Handling SQL constraints with Kysely
- 125. API with NestJS #125. Offset and keyset pagination with Kysely
- 126. API with NestJS #126. Improving the database performance with indexes and Kysely
- 127. API with NestJS #127. Arrays with PostgreSQL and Kysely
- 128. API with NestJS #128. Managing JSON data with PostgreSQL and Kysely
- 129. API with NestJS #129. Implementing soft deletes with SQL and Kysely
- 130. API with NestJS #130. Avoiding storing sensitive information in API logs
- 131. API with NestJS #131. Unit tests with PostgreSQL and Kysely
- 132. API with NestJS #132. Handling date and time in PostgreSQL with Kysely
- 133. API with NestJS #133. Introducing database normalization with PostgreSQL and Prisma
- 134. API with NestJS #134. Aggregating statistics with PostgreSQL and Prisma
- 135. API with NestJS #135. Referential actions and foreign keys in PostgreSQL with Prisma
- 136. API with NestJS #136. Raw SQL queries with Prisma and PostgreSQL range types
- 137. API with NestJS #137. Recursive relationships with Prisma and PostgreSQL
- 138. API with NestJS #138. Filtering records with Prisma
- 139. API with NestJS #139. Using UUID as primary keys with Prisma and PostgreSQL
- 140. API with NestJS #140. Using multiple PostgreSQL schemas with Prisma
- 141. API with NestJS #141. Getting distinct records with Prisma and PostgreSQL
- 142. API with NestJS #142. A video chat with WebRTC and React
- 143. API with NestJS #143. Optimizing queries with views using PostgreSQL and Kysely
- 144. API with NestJS #144. Creating CLI applications with the Nest Commander
- 145. API with NestJS #145. Securing applications with Helmet
- 146. API with NestJS #146. Polymorphic associations with PostgreSQL and Prisma
- 147. API with NestJS #147. The data types to store money with PostgreSQL and Prisma
- 148. API with NestJS #148. Understanding the injection scopes
- 149. API with NestJS #149. Introduction to the Drizzle ORM with PostgreSQL
- 150. API with NestJS #150. One-to-one relationships with the Drizzle ORM
- 151. API with NestJS #151. Implementing many-to-one relationships with Drizzle ORM
- 152. API with NestJS #152. SQL constraints with the Drizzle ORM
- 153. API with NestJS #153. SQL transactions with the Drizzle ORM
- 154. API with NestJS #154. Many-to-many relationships with Drizzle ORM and PostgreSQL
- 155. API with NestJS #155. Offset and keyset pagination with the Drizzle ORM
- 156. API with NestJS #156. Arrays with PostgreSQL and the Drizzle ORM
- 157. API with NestJS #157. Handling JSON data with PostgreSQL and the Drizzle ORM
- 158. API with NestJS #158. Soft deletes with the Drizzle ORM
- 159. API with NestJS #159. Date and time with PostgreSQL and the Drizzle ORM
- 160. API with NestJS #160. Using views with the Drizzle ORM and PostgreSQL
- 161. API with NestJS #161. Generated columns with the Drizzle ORM and PostgreSQL
- 162. API with NestJS #162. Identity columns with the Drizzle ORM and PostgreSQL
- 163. API with NestJS #163. Full-text search with the Drizzle ORM and PostgreSQL
- 164. API with NestJS #164. Improving the performance with indexes using Drizzle ORM
- 165. API with NestJS #165. Time intervals with the Drizzle ORM and PostgreSQL
- 166. API with NestJS #166. Logging with the Drizzle ORM
- 167. API with NestJS #167. Unit tests with the Drizzle ORM
- 168. API with NestJS #168. Integration tests with the Drizzle ORM
- 169. API with NestJS #169. Unique IDs with UUIDs using Drizzle ORM and PostgreSQL
- 170. API with NestJS #170. Polymorphic associations with PostgreSQL and Drizzle ORM
- 171. API with NestJS #171. Recursive relationships with Drizzle ORM and PostgreSQL
- 172. API with NestJS #172. Database normalization with Drizzle ORM and PostgreSQL
- 173. API with NestJS #173. Storing money with Drizzle ORM and PostgreSQL
- 174. API with NestJS #174. Multiple PostgreSQL schemas with Drizzle ORM
- 175. API with NestJS #175. PUT and PATCH requests with PostgreSQL and Drizzle ORM
- 176. API with NestJS #176. Database migrations with the Drizzle ORM
- 177. API with NestJS #177. Response serialization with the Drizzle ORM
In the third part of this series, we’ve implemented authentication with JWT, Passport, cookies, and bcrypt. It leaves quite a bit of room for improvement. In this article, we look into refresh tokens.
You can find all of the code from this series in this repository
Why do we need refresh tokens?
So far, we’ve implemented JWT access tokens. They have a specific expiration time that should be short. If someone steals it from our user, the token is usable just until it expires.
After the user logs in successfully, we send back the access token. Let’s say that it has an expiry of 15 minutes. During this period, it can be used by the user to authenticate while making various requests to our API.
After the expiry time passes, the user needs to log in by again providing the username and password. This does not create the best user experience, unfortunately. On the other hand, increasing the expiry time of our access token might make our API less secure.
The solution to the above issue might be refresh tokens. The basic idea is that on a successful log-in, we create two separate JWT tokens. One is an access token that is valid for 15 minutes. The other one is a refresh token that has an expiry of a week, for example.
How refresh tokens work
The user saves both of the tokens in cookies but uses just the access token to authenticate while making requests. It works for 15 minutes without issues. Once the API states that the access token expires, the user needs to perform a refresh.
The crucial thing about storing tokens in cookies is that they should use the httpOnly flag. For more information, check out Cookies: explaining document.cookie and the Set-Cookie header
To refresh the token, the user needs to call a separate endpoint, called /refresh. This time, the refresh token is taken from the cookies and sent to the API. If it is valid and not expired, the user receives the new access token. Thanks to that, there is no need to provide the username and password again.
Addressing some of the potential issues
Unfortunately, we need to consider the situation in which the refresh token is stolen. It is quite a sensitive piece of data, almost as much as the password.
We need to deal with the above issue in some way. The most straightforward way of doing so is changing the JWT secret once we know about the data leak. Doing that would render all of our refresh tokens invalid, and therefore, unusable.
We might not want to log out every user from our application, though. Assuming we know the affected user, we would like to make just one refresh token invalid. JWT is in its core stateless, though.
One of the solutions that we might stumble upon while browsing the web is a blacklist. Every time someone uses a refresh token, we check if it is in the blacklist first. Unfortunately, this does not seem like a solution that would have good enough performance. Checking the blacklist upon every token refresh and keeping it up-to-date might be a demanding task.
An alternative is saving the current refresh token in the database upon logging in. When someone performs a refresh, we check if the token kept in the database matches the provided one. If it is not the case, we reject the request. Thanks to doing the above, we can easily make the token of a particular person invalid by removing it from the database.
Logging out
So far, when the user logged out, we’ve just removed the JWT token from cookies. While this might be a viable solution for tokens with a short expiry time, it creates some issues with refresh tokens. Even though we removed the refresh token from the browser, it is still valid for a long time.
We can address the above issue by removing the refresh token from the database once the user logs out. If someone tries to use the refresh token before it expires, it is not possible anymore.
Preventing logging in on multiple devices
Let’s assume that we provide services that require a monthly payment. Allowing many people to use the same account at the same time might have a negative impact on our business.
Saving the refresh token upon logging in can help us deal with the above issue too. If someone uses the same user credentials successfully, it overwrites the refresh token stored in the database. Thanks to doing that, the previous person is not able to use the old refresh token anymore.
A potential database leak
We’ve mentioned that the refresh token is sensitive data. If it leaks out, the attacker can easily impersonate our user.
We have a similar case with the passwords. This is why we keep hashes of the passwords instead of just plain text. We can improve our refresh token solution similarly.
If we hash our refresh tokens before saving them in the database, we prevent the attacker from using them even if our database is leaked.
Implementation in NestJS
The first thing to do is to add new environment variables. We want the secret used for generating refresh token to be different.
1 2 3 4 5 6 7 8 9 |
ConfigModule.forRoot({ validationSchema: Joi.object({ JWT_ACCESS_TOKEN_SECRET: Joi.string().required(), JWT_ACCESS_TOKEN_EXPIRATION_TIME: Joi.string().required(), JWT_REFRESH_TOKEN_SECRET: Joi.string().required(), JWT_REFRESH_TOKEN_EXPIRATION_TIME: Joi.string().required(), // ... }) }), |
Now, let’s add the column in our User entity so that we can save the refresh tokens in the database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Exclude } from 'class-transformer'; @Entity() class User { @PrimaryGeneratedColumn() public id: number; @Column({ unique: true }) public email: string; @Column({ nullable: true }) @Exclude() public currentHashedRefreshToken?: string; // ... } export default User; |
We also need to create a function for creating a method for creating a cookie with the refresh token.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import { Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; import { JwtService } from '@nestjs/jwt'; import { ConfigService } from '@nestjs/config'; import TokenPayload from './tokenPayload.interface'; @Injectable() export class AuthenticationService { constructor( private readonly usersService: UsersService, private readonly jwtService: JwtService, private readonly configService: ConfigService ) {} public getCookieWithJwtAccessToken(userId: number) { const payload: TokenPayload = { userId }; const token = this.jwtService.sign(payload, { secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'), expiresIn: `${this.configService.get('JWT_ACCESS_TOKEN_EXPIRATION_TIME')}s` }); return `Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get('JWT_ACCESS_TOKEN_EXPIRATION_TIME')}`; } public getCookieWithJwtRefreshToken(userId: number) { const payload: TokenPayload = { userId }; const token = this.jwtService.sign(payload, { secret: this.configService.get('JWT_REFRESH_TOKEN_SECRET'), expiresIn: `${this.configService.get('JWT_REFRESH_TOKEN_EXPIRATION_TIME')}s` }); const cookie = `Refresh=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get('JWT_REFRESH_TOKEN_EXPIRATION_TIME')}`; return { cookie, token } } // ... } |
The possibility to provide the secret while calling the jwtService.sign method has been added in the 7.1.0 version of @nestjs/jwt
An improvement to the above would be to fiddle with the Path parameter of the refresh token cookie so that the browser does not send it with every request.
We also need to create a method for saving the hash of the current refresh token.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import User from './user.entity'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>, ) {} async setCurrentRefreshToken(refreshToken: string, userId: number) { const currentHashedRefreshToken = await bcrypt.hash(refreshToken, 10); await this.usersRepository.update(userId, { currentHashedRefreshToken }); } // ... } |
Let’s make sure that we send both cookies when logging in.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
import { Req, Controller, HttpCode, Post, UseGuards, ClassSerializerInterceptor, UseInterceptors, } from '@nestjs/common'; import { AuthenticationService } from './authentication.service'; import RequestWithUser from './requestWithUser.interface'; import { LocalAuthenticationGuard } from './localAuthentication.guard'; @Controller('authentication') @UseInterceptors(ClassSerializerInterceptor) export class AuthenticationController { constructor( private readonly authenticationService: AuthenticationService ) {} @HttpCode(200) @UseGuards(LocalAuthenticationGuard) @Post('log-in') async logIn(@Req() request: RequestWithUser) { const {user} = request; const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(user.id); const refreshTokenCookie = this.authenticationService.getCookieWithJwtRefreshToken(user.id); await this.usersService.setCurrentRefreshToken(refreshToken, user.id); request.res.setHeader('Set-Cookie', [accessTokenCookie, refreshTokenCookie]); return user; } // ... } |
Creating an endpoint that uses the refresh token
Now we can start handling the incoming refresh token. For starters, let’s deal with checking if the token from cookies matches the one in the database. To do that, we need to create a new method in the UsersService.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import User from './user.entity'; import { FilesService } from '../files/files.service'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>, private readonly filesService: FilesService ) {} async getById(id: number) { const user = await this.usersRepository.findOne({ id }); if (user) { return user; } throw new HttpException('User with this id does not exist', HttpStatus.NOT_FOUND); } async getUserIfRefreshTokenMatches(refreshToken: string, userId: number) { const user = await this.getById(userId); const isRefreshTokenMatching = await bcrypt.compare( refreshToken, user.currentHashedRefreshToken ); if (isRefreshTokenMatching) { return user; } } // ... } |
Now, we need to create a new strategy for Passport. Please note that we use the passReqToCallback parameter so that we can access the cookies in our validate method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { ExtractJwt, Strategy } from 'passport-jwt'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Request } from 'express'; import { UsersService } from '../users/users.service'; import TokenPayload from './tokenPayload.interface'; @Injectable() export class JwtRefreshTokenStrategy extends PassportStrategy( Strategy, 'jwt-refresh-token' ) { constructor( private readonly configService: ConfigService, private readonly userService: UsersService, ) { super({ jwtFromRequest: ExtractJwt.fromExtractors([(request: Request) => { return request?.cookies?.Refresh; }]), secretOrKey: configService.get('JWT_REFRESH_TOKEN_SECRET'), passReqToCallback: true, }); } async validate(request: Request, payload: TokenPayload) { const refreshToken = request.cookies?.Refresh; return this.userService.getUserIfRefreshTokenMatches(refreshToken, payload.userId); } } |
To use the above strategy, we also need to create a new guard.
1 2 3 4 5 |
import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export default class JwtRefreshGuard extends AuthGuard('jwt-refresh-token') {} |
Now, the last thing to do is to create the /refresh endpoint.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import { Req, Controller, UseGuards, Get, ClassSerializerInterceptor, UseInterceptors, } from '@nestjs/common'; import { AuthenticationService } from './authentication.service'; import RequestWithUser from './requestWithUser.interface'; import JwtRefreshGuard from './jwt-refresh.guard'; @Controller('authentication') @UseInterceptors(ClassSerializerInterceptor) export class AuthenticationController { constructor( private readonly authenticationService: AuthenticationService, ) {} @UseGuards(JwtRefreshGuard) @Get('refresh') refresh(@Req() request: RequestWithUser) { const accessTokenCookie = this.authenticationService.getCookieWithJwtAccessToken(request.user.id); request.res.setHeader('Set-Cookie', accessTokenCookie); return request.user; } // ... } |
Improving the log-out flow
The last thing is to modify the log-out flow. First, let’s create a method that generates cookies to clear both the access token and the refresh token.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Injectable } from '@nestjs/common'; @Injectable() export class AuthenticationService { public getCookiesForLogOut() { return [ 'Authentication=; HttpOnly; Path=/; Max-Age=0', 'Refresh=; HttpOnly; Path=/; Max-Age=0' ]; } // ... } |
Now, we need to create a piece of code that removes the refresh token from the database.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import User from './user.entity'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>, ) {} async removeRefreshToken(userId: number) { return this.usersRepository.update(userId, { currentHashedRefreshToken: null }); } // ... } |
Let’s add all of the above to our /log-out endpoint.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { Req, Controller, HttpCode, Post, UseGuards, ClassSerializerInterceptor, UseInterceptors, } from '@nestjs/common'; import { AuthenticationService } from './authentication.service'; import RequestWithUser from './requestWithUser.interface'; import JwtAuthenticationGuard from './jwt-authentication.guard'; import { UsersService } from '../users/users.service'; @Controller('authentication') @UseInterceptors(ClassSerializerInterceptor) export class AuthenticationController { constructor( private readonly authenticationService: AuthenticationService, private readonly usersService: UsersService ) {} @UseGuards(JwtAuthenticationGuard) @Post('log-out') @HttpCode(200) async logOut(@Req() request: RequestWithUser) { await this.usersService.removeRefreshToken(request.user.id); request.res.setHeader('Set-Cookie', this.authenticationService.getCookiesForLogOut()); } // ... } |
Summary
By doing all of the above, we now have a fully functional refresh token flow. We also addressed a few issues that you might face when implementing authentication, such as potential database leaks and unwanted logging in on multiple devices. There is still a place for further improvements, such as making fewer queries to the database when authenticating with the access token.
What are your thoughts on the solutions implemented in this article?
I get this infomation from https://github.com/dcodeIO/bcrypt.js#security-considerations , so bcrypt compare the tokens always return true when the first 72 letters are the same.
Thanks for your work.
> Per bcrypt implementation, only the first 72 bytes of a string are used. Any extra bytes are ignored when matching passwords. Note that this is not the first 72 characters.
A workaround is to hash the string first using SHA-256, and then use bcrypt.
Yeah, I face this issue, so I change to use crypto.createHash and compare with the hash in the database.
Thank for sharing !
If I understand this right, this is a cookie authentication and cookie authorization based approach. It’s most suited for browser-based web apps as cookie is sent on every request. Client doesn’t need to worry about it. While a lot of new web apps send the access token to the client in the form of a json blob which will leave the client to manage the access token and send back to server as “authorization bearer” header which is based on the reason that this way will prevent CSRF attack since no cookie can be hijacked but it leaves room for XSS since javascript can access to access token. Your approach has a bit of CSRF issue but XSS is minimized. Do I understand it right?
The approach would leave the room for some CSRF attacks, but we can increase the security by various means. One of the ways would be to maintain a same-origin policy and to have SameSite attribute set to Strict.
This topic is worth looking into.
This hampers user experience.
Great article, thank you for sharing. What about the case of a public pc and the user does not check the “remember me” option and closes the browser without logging out. How should we handle such cases?
Refresh token should not be stored but what should we do?
In the log-in process we expect the client to send the id of the user, usually a client will just log-in with email/username and password, am I missing something?
I’ve read that unless your backend stores sessions, it is quite pointless to use cookies instead of localstorage. Is this true? To what extent?
how long does the refresh token last compared to access token?
This is a really cool approach. I believe that the cookie approach is often required with SSR frameworks such as NextJs as there is not the 2 step approach of React. Get the JS then process and make and follow up call to the server for things such as authorisations. Also in the micro-services world it is necessary to have a stateless approach.
When the access token is expired we can check refresh one and generate both new tokens and set them as cookies. Thus, the client won’t need to call /refresh endpoint.
But without cookies ( like auth in WebSocket transport in socket.io ), i like the /refresh endpoint.
Hello Marcin,
I thought you might want to fix a bug in this article and replace
with
else
won’t compile
Many thanks for the excellent work
How do you mean? the code is correct. getCookiewithJwtRefreshToken returns a string not an. object
Hi,
to set the refresh-token cookie correctrly u should chnge following line from:
to
Hi,
So I have 2 apps; one is a mobile app and the other is a dashboard web app. Both connected to the same backend service therefore sharing jwt secret and expiring time, which is 15 minutes.
Is it possible to have a different expired time for both app? So 24 hours for mobile and 15 minutes for web? If it’s possible, how do I do that?
If I create 2 ‘modules’ for auth each for mobile and web, don’t they access the same other backend services like product and etch, which is defined using the lesser expiring time?
Thanks,
Andri
Hello.
Refresh token is more secure, as it should be only sent to one specific endpoint.
So, wouldn’t it be correct to modify refresh token cookie path to /refresh ?
I know but wouldn’t it be nice to re-fresh with endpoint showing in the end how the new refresh token work in api?
To Recap (for myself to)
/login +refreshtoken
/logout -refreshtoken
/refresh – check refresh token
Thanks for the sharing! I got a question:
The
log-out
endPoint need<span style="background-color: rgb(250, 250, 250); color: rgb(172, 76, 99);">JwtAuthenticationGuard</span>
. Whenaccess_token
andrefresh_token
both expired, how thelog-out
endPoint work? Can’t get any user info inguards
. How to remove refresh token in database?How is this secure? If somebody steals a refresh token they will be able to get access token.
If someone steals your password, it will be log as you. The same situation 🙂 This is only one of many prevent methods and make hacking harder…
I’m getting 401 Unauthorized at /refresh. Any ideas why?
Did you try using the breakpoints in the debugger to investigate your issue?
Sure
info: { message: ‘missing credentials’ }, but I check variables and it sees my cookies (Authentication and Refresh) at @nestjs\passport\dist\auth.guard.js (49 line)
update
Call stack:
Strategy.authenticate at passport-local/lib/strategy.js(75:92),
Variables:
Nevermind, it’s my mistake. I imported Strategy from the password-local instead of password-jwt.
What would be the approach to implement auto log-out feature on the Frontend? That is, when the token expires, user is logged out from the account automatically? I know how to do it if the token is stored in the localStorage, but not in the cookies 🙂
just get the values from localstoreage with getCookies values instead
public getCookieWithJwtAccessToken(userId: number) {
${this.configService.get(‘JWT_ACCESS_TOKEN_EXPIRATION_TIME’)}sconst payload: TokenPayload = { userId };
const token = this.jwtService.sign(payload, {
secret: this.configService.get('JWT_ACCESS_TOKEN_SECRET'),
expiresIn:
});
Authentication=${token}; HttpOnly; Path=/; Max-Age=${this.configService.get(‘JWT_ACCESS_TOKEN_EXPIRATION_TIME’)}return
;
}
i don’t understand and have error with secret propery to signoptions.
we already configure secret and signOptions in the Authentication Module.
sorry. i solved.
Thank you very much for your quality posts. Instead of being a JWT, shouldn’t the refresh token be an entry in a database linked to this user allowing to track the browser client used, the ip, and especially to be able to revoke it and mark it as unusable?
The advantage of JWT is that the backend doesn’t need to fetch DB on every request to trust the user. And the disadvantage is that it’s not expirable. So a DB entry for the refresh token that is called relatively slowly seems ok (and that we can check the deletion).