Token, refresh token, and creating an asynchronous wrapper for a REST request

Login Information
Having made a REST request to the api, where we sent the username and password, in return we get json of the following format (the values are random and the lines are usually longer):
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgSld",
"refresh_token": "1eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJPbmxpbmUgS",
"expires_in": 124234149563
}
There may be more fields in the response, for example, “token_type” , “expires_on” , etc., but, for this implementation, we only need the three fields above.
Let's take a closer look at them:
- access_token - the token that we will need to send in the header of each request to receive data in response
- refresh_token - the token that we will need to send in order to receive a new token when the old one expires
- expires_in - token lifetime in seconds
Receiving token
Now create a function that will receive the json described above and save it.
We will store data for authorization in sessionStorage or localStorage , depending on our needs. In the first case, the data is stored until the user completes the session or closes the browser, in the second case, the data will be stored in the browser for an unlimited time until for some reason the localStorage is cleared.
Function to save the token in sessionStorage:
function saveToken(token) {
sessionStorage.setItem('tokenData', JSON.stringify(token));
}
Function to get the token:
function getTokenData(login, password) {
return fetch('api/auth', {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
login,
password,
}),
})
.then((res) => {
if (res.status === 200) {
const tokenData = res.json();
saveToken(JSON.stringify(tokenData)); // сохраняем полученный токен в sessionStorage, с помощью функции, заданной ранее
return Promise.resolve()
}
return Promise.reject();
});
}
Thus, we received a token with the fields “access_token” , “refresh_token” and “expires_in” and saved it in sessionStorage for further use.
Token Update
The token we received earlier has a limited lifetime, which is set in the "expires_in" field . After its lifetime expires, the user will not be able to receive new data by sending this token in the request, so you need to get a new token.
We can get the token in two ways: the first way is to log in again by sending the username and password to the server. But this does not suit us, because it is wrong to force the user to re-enter authorization data every time after a certain period of time - this should be done automatically. But storing a login / password pair somewhere in the memory for automatic sending is unsafe, that's why we need a refresh_token , which was received earlier with access_tokenand stored in sessionStorage. By sending this token to another address that api provides, we can get a new “fresh” token in response.
Function for Token Update
function refreshToken(token) {
return fetch('api/auth/refreshToken', {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token,
}),
})
.then((res) => {
if (res.status === 200) {
const tokenData = res.json();
saveToken(JSON.stringify(tokenData)); // сохраняем полученный обновленный токен в sessionStorage, с помощью функции, заданной ранее
return Promise.resolve();
}
return Promise.reject();
});
}
Using the code above, we rewritten the token in sessionStorage and now we can send requests to the api in a new way.
Creating a wrapper function
Now let's create a function that will add authorization data to the request header, and if necessary, automatically update it before making the request.
Since if the token has expired, we will need to request a new token, then our function will be asynchronous. For this we will use the async / await construct.
Wrapper function
export async function fetchWithAuth(url, options) {
const loginUrl = '/login'; // url страницы для авторизации
let tokenData = null; // объявляем локальную переменную tokenData
if (sessionStorage.authToken) { // если в sessionStorage присутствует tokenData, то берем её
tokenData = JSON.parse(localStorage.tokenData);
} else {
return window.location.replace(loginUrl); // если токен отсутствует, то перенаправляем пользователя на страницу авторизации
}
if (!options.headers) { // если в запросе отсутствует headers, то задаем их
options.headers = {};
}
if (tokenData) {
if (Date.now() >= tokenData.expires_on * 1000) { // проверяем не истек ли срок жизни токена
try {
const newToken = await refreshToken(tokenData.refresh_token); // если истек, то обновляем токен с помощью refresh_token
saveToken(newToken);
} catch () { // если тут что-то пошло не так, то перенаправляем пользователя на страницу авторизации
return window.location.replace(loginUrl);
}
}
options.headers.Authorization = `Bearer ${tokenData.token}`; // добавляем токен в headers запроса
}
return fetch(url, options); // возвращаем изначальную функцию, но уже с валидным токеном в headers
}
Using the code above, we created a function that will add a token to requests in the api. With this function we can replace fetch in the requests we need, where authorization is required and for this we do not need to change the syntax or add any more data to the arguments.
It will just be enough to “import” it into a file and replace the standard fetch with it.
import fetchWithAuth from './api';
function getData() {
return fetchWithAuth('api/data', options)
}