API with NestJS #13. Implementing refresh tokens using JWT

JavaScript NestJS TypeScript

This entry is part 13 of 14 in the API with NestJS

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  . 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.

Now, let’s add the column in our User entity so that we can save the refresh tokens in the database.

We also need to create a function for creating a method for creating a cookie with the refresh token.

The possibility to provide the secret while calling the   method has been added in the    version of 

An improvement to the above would be to fiddle with the   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.

Let’s make sure that we send both cookies when logging in.

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  .

Now, we need to create a new strategy for Passport. Please note that we use the   parameter so that we can access the cookies in our   method.

To use the above strategy, we also need to create a new guard.

Now, the last thing to do is to create the   endpoint.

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.

Now, we need to create a piece of code that removes the refresh token from the database.

Let’s add all of the above to our   endpoint.

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?

Series Navigation<< API with NestJS #12. Introduction to ElasticsearchAPI with NestJS #14. Improving performance of our Postgres database with indexes >>
Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
gls
gls
22 days ago

The maximum input length is 72 bytes (note that UTF8 encoded characters use up to 4 bytes) and the length of generated hashes is 60 characters.

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.

Tony
Tony
20 days ago

Thank for sharing !