It's the beginning of 2022, and before I dive into this tech filled post, I just wanted to start off cozy and calm. With this amazing picture by Aleksandar Cvetianovic. Take it in. Breathe... and let's go.
Photo by Aleksandar Cvetanovic on Unsplash
TL;DR
In this article, we're going to look into storing and managing refreshing authentication tokens from third-party API's using Next.js. We'll use HttpOnly cookies and deal with expiring tokens using axios interceptors. Scroll down to "The setup" for all the details.
Background
Last year I was working on a personal project where I was using a third-party API. I researched ways in which I could store the user access token without having to create my own database. One of the possibilities was using HttpOnly cookies. I had already decided to go for Next.js because of the quick server setup that comes with it. I implemented the authentication flow and searched for the logic to refresh tokens. This is how I solved it:
The setup
Preface
To follow along, you need to already know how to do the following:
- set up a Next.js project
- authenticate the third-party API you're going to use through Next.js API routes.
- understand the logic to refresh your tokens for your third-party API.
Storing your refresh token inside a HttpOnly cookie
To securely store the third-party API refresh token, we'll use a HttpOnly cookie. To read more about the security they can provide, check out the docs at MDN..
To figure out yourself:
To start, make sure you have your Next.js project setup with a server that manages getting the refresh token from your third-party API. I'm assuming you've created your own endpoint in the
pages/api
folder. For this example, I'll call the filegetRefreshToken.js
.
We're going to use the cookie library to help deal with setting our cookie. To add it to our project:
$ npm install cookie
// or
$ yarn add cookie
We will create our cookie in the getRefreshToken.js
file. After getting your refresh token, use the res
parameter that is exposed from the request handler in the get-token
endpoint.
// pages/api/getRefreshToken.js
// --- all the logic you wrote yourself to get the refresh_token
res.setHeader('Set-Cookie', [
cookie.serialize('refreshToken', refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
maxAge: 60 * 60 * 24,
sameSite: 'strict',
path: '/'
})
]);
To enable the cookie as HttpOnly, we set httpOnly: true
. To only allow access through HTTPS protocol, add secure: process.env.NODE_ENV !== 'development'
. Currently, HTTPS is usually not used on localhost
, so we set it up to only use secure: true
on production. If you're curious about this, you can read up on it on MDN.
Set maxAge: 60 * 60 * 24
, to define the amount of seconds before the cookie expires. In this case it sets it to 24 hours. This will force the token to be invalidated after 24 hours.
Eventually the endpoint will look something like this:
// pages/api/getRefreshToken.js
import axios from 'axios';
import cookie from 'cookie';
const getRefreshToken = async (req, res) => {
// we use this 'res' parameter to set the cookie.
// any logic you need to get your refresh token, including
const options = {
// all necessary options for getting the refresh token
};
const fetchData = () =>
axios(options)
.then(async response => {
const { refresh_token } = response.data;
res.setHeader('Set-Cookie', [
cookie.serialize('refreshToken', refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
maxAge: 60 * 60 * 24,
sameSite: 'strict',
path: '/'
})
]);
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ refresh_token }));
})
.catch(error => {
// logic for handling errors
});
await fetchData();
};
export default getRefreshToken;
If you ever want to use this cookie, you can find it on the req
object on any call to your Next.js server. It will be available in req.cookies
.
Encrypting our refresh token
Because a refresh token is an important part of the authentication flow, we'll add an extra layer of security by encrypting it. We will use the library crypto-js
for this. This library can help us encrypt our token using an 'encryption key' or password. This password will only be available to our server. This way the server is able to encrypt and decrypt the token.
$ npm install crypto-js
// or
$ yarn add crypto-js
In our env.local
file (which we do not commit!) we add an environment variable with a encryption key of approximately 32 characters. Make sure this key is truly secret, random and secure!
// .env.local
ENCRYPT_KEY=theverylongpasswordwith32characters
In the getRefreshToken.js
file, import AES
from crypto-js/aes
. In the object where we set refreshToken
, use the encode
key in the cookie object to pass the function that will encrypt the token:
// pages/api/getRefreshToken.js
import AES from 'crypto-js/aes';
// ...
cookie.serialize('refreshToken', refresh_token, {
httpOnly: true,
secure: process.env.NODE_ENV !== 'development',
maxAge: 60 * 60 * 24,
sameSite: 'strict',
path: '/',
encode: value => AES.encrypt(value, process.env.ENCRYPT_KEY).toString()
});
Whenever you want to use this token you do need to decrypt it using the encryption key like so:
import CryptoJS from 'crypto-js';
// In the place where you use your refresh token:
const { refreshToken } = req.cookies;
const decryptedRefresh = CryptoJS.AES.decrypt(
refreshToken,
process.env.ENCRYPT_KEY
).toString(CryptoJS.enc.Utf8);
Setting up an axios instance to manage refresh tokens
Whenever a token expires or is incorrect, we'll try and refresh them. Usually, in the cases that this happens, an API would return 401 Unauthorized
.
To deal with this we're going to use axios Interceptors.
You can use an interceptor to 'intercept' requests or responses before they are actually handled. In this example we're going to:
- Create our own axios instance and add a request and response interceptor to it.
- Use this axios instance everywhere we are doing calls that use an access token.
This way, if an endpoint is using an access token to get data, and receives an 401 Unauthorized
, we can handle this by refreshing the token. Let's break this down:
Create your own axios instance inside a file called axios-instance.js
:
// axios-instance.js
import axios from 'axios';
const axiosInstance = axios.create();
export default axiosInstance;
To add our interceptor logic we'll start with the response interceptor. Whenever we get a response from an API we check if it is 401 Unauthorized
. If that is the case we refresh the access token and try the call again. To do this we'll be using axios-auth-refresh which makes it really easy to set this up.
Add the library:
npm install 'axios-auth-refresh'
// or
yarn add 'axios-auth-refresh'
Inside the axios-instance.js
file, import createAuthRefreshInterceptor
. Then create a refreshAuthLogic
function. This function has a failedRequest parameter that we receive from our interceptor.
To figure out yourself:
Now it's up to you to create a function we can call that handles refreshing the access token from your third-party API. You can grab the refresh token from
req.cookies
. Don't forget to decrypt it like I showed you before! And make sure it returns the access token without encryption.
Inside refreshAuthLogic
we will use the refresh function you created yourself, in this example it's called refreshAccessToken
. That function returns our new token, which we set as the response Authorization header. Finally, we return a resolved promise.
We then call the createAuthRefreshInterceptor
function and pass in the axiosInstance
and refreshAuthLogic
function we created.
// axios-instance.js
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import refreshAccessToken from './refresh-access-token'; // this file contains any logic you need to refresh your token with your third-party API
const axiosInstance = axios.create();
const refreshAuthLogic = failedRequest =>
refreshAccessToken().then(tokenRefreshResponse => {
// get the new token
failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.accessToken}`; // set the new token as the authorization header.
return Promise.resolve();
});
createAuthRefreshInterceptor(axiosInstance, refreshAuthLogic);
export default axiosInstance;
To figure out yourself:
Something to keep in mind for the
refreshAccessToken
logic that you'll be creating yourself to get your refreshed token, is that you're going to have to make sure you also set this new refresh token as a cookie. You can use the same logic as we have used before for that.
Now let's deal with the request interceptors. This is where the fun starts.
Inside our axios-instance.js
file, we're going to create a let requestToken;
empty variable. Then inside refreshAuthLogic
, we assign the refreshed token to requestToken
. This way, requestToken
will always be up to date with the latest version of our token.
After this we're going to set our own request interceptor. We tell it to check if requestToken
is empty or not. If it is empty, we'll use the refreshAccessToken
function to get a new token. If it is not empty, we use requestToken
as our authorization header:
// axios-instance.js
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import refreshAccessToken from './refresh-access-token';
let requestToken;
const axiosInstance = axios.create();
const refreshAuthLogic = failedRequest =>
refreshAccessToken().then(tokenRefreshResponse => {
failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.accessToken}`;
requestToken = tokenRefreshResponse.accessToken; // change the requestToken with the refreshed one
return Promise.resolve();
});
createAuthRefreshInterceptor(axiosInstance, refreshAuthLogic);
axiosInstance.interceptors.request.use(async request => {
if (!requestToken) {
refreshAccessToken().then(tokenRefreshResponse => {
requestToken = tokenRefreshResponse.accessToken;
});
}
request.headers.Authorization = `Bearer ${requestToken}`;
return request;
});
export default axiosInstance;
From this point on, any request that is made using the axios instance, will grab the authorization token from the let requestToken
in this file before doing a request. So if an earlier request refreshed the token, the next one is able to use the refreshed one.
And that's it! I'm curious to hear other ways people do this! So feel free to share in the comments.
Top comments (0)