API with NestJS #13. Implementing refresh tokens using JWT

JavaScript NestJS TypeScript

This entry is part 13 of 121 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
31 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
gls
gls
4 years 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.

Oss
Oss
4 years ago
Reply to  gls

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

Last edited 4 years ago by Oss
Le Dung
Le Dung
1 year ago
Reply to  gls

Yeah, I face this issue, so I change to use crypto.createHash and compare with the hash in the database.

Tony
Tony
4 years ago

Thank for sharing !

Hao
Hao
4 years ago

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?

Last edited 4 years ago by Hao
Rajarshi
2 years ago
Reply to  Marcin Wanago

This hampers user experience.

Yiannis
3 years ago

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?

Sjnao
Sjnao
3 years ago

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?

zuvak
zuvak
3 years ago

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?

John
John
3 years ago

how long does the refresh token last compared to access token?

Chris
Chris
3 years ago

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.

cybercoder
cybercoder
3 years ago

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.

ViveLaPologne
ViveLaPologne
3 years ago

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

Cybertech
Cybertech
3 years ago
Reply to  ViveLaPologne

How do you mean? the code is correct. getCookiewithJwtRefreshToken returns a string not an. object

Oliver K
Oliver K
2 years ago
Reply to  Cybertech

Hi,

to set the refresh-token cookie correctrly u should chnge following line from:

to

Andri
Andri
2 years ago

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

Aleks
Aleks
2 years ago

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 ?

Stephanie
Stephanie
2 years ago

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

Last edited 2 years ago by Stephanie
zhao97
zhao97
2 years ago

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>. When access_token and refresh_token both expired, how the log-out endPoint work? Can’t get any user info in guards. How to remove refresh token in database?

huluhulu
huluhulu
2 years ago

How is this secure? If somebody steals a refresh token they will be able to get access token.

Karoż
Karoż
2 years ago
Reply to  huluhulu

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…

SirAlex
SirAlex
2 years ago

I’m getting 401 Unauthorized at /refresh. Any ideas why?

Last edited 2 years ago by SirAlex
SirAlex
SirAlex
2 years ago
Reply to  Marcin Wanago

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)

Last edited 2 years ago by SirAlex
SirAlex
SirAlex
2 years ago
Reply to  SirAlex

update
Call stack:
Strategy.authenticate at passport-local/lib/strategy.js(75:92),

Variables:

Last edited 2 years ago by SirAlex
SirAlex
SirAlex
2 years ago
Reply to  SirAlex

Nevermind, it’s my mistake. I imported Strategy from the password-local instead of password-jwt.

Ole
Ole
1 year ago

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 🙂

Last edited 1 year ago by Ole
Ngoc Anh
Ngoc Anh
1 year ago
Reply to  Ole

just get the values from localstoreage with getCookies values instead

soonyeop
soonyeop
1 year ago

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’)};
}

i don’t understand and have error with secret propery to signoptions.

we already configure secret and signOptions in the Authentication Module.

Last edited 1 year ago by soonyeop
soonyeop
soonyeop
1 year ago
Reply to  soonyeop

sorry. i solved.