1

I get 401 error after a while when the page is reloaded, I figured it could be because the access token is expired. How do I set a new token with my refresh token? The below function runs every time the user visits a new page or refreshes the page. But it doesn't seem to work.

export async function currentAccount() {


  if (store.get('refreshToken')) {
    const query = {
      grant_type: 'refresh_token',
      companyId: store.get('lastCompanyId'),
      refresh_token: store.get('refreshToken'),
    }
    const queryString = new URLSearchParams(query).toString()
    const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
    return apiClient
      .post(actionUrl, { auth: 'basic' })
      .then(async response => {
        if (response) {
          const { access_token: accessToken } = response.data
            store.set('accessToken', accessToken)
          return response.data
        }
        return false
      })
      .catch(err => {
        console.log('error', err)
        store.clearAll()
      })
  }
  return false
}

Login sets the access tokens

export async function login(email, password) {
  const query = {
    grant_type: 'password',
    username: email,
    password,
  }
  const queryString = new URLSearchParams(query).toString()
  const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
  return apiClient
    .post(actionUrl, { auth: 'basic' })
    .then(async response => {
      if (response) {
        const {
          data: {
            access_token: accessToken,
            refresh_token: refreshToken,
          },
        } = response
        const decoded = jsonwebtoken.decode(accessToken)
        response.data.authUser = decoded.authUser
        const { userId, profileId, companyId } = decoded.authUser
        if (accessToken) {
          store.set('accessToken', accessToken)
          store.set('refreshToken', refreshToken)
        }
        return response.data
      }
      return false
    })
    .catch(err => console.log(err))
}

saga users.js

export function* LOAD_CURRENT_ACCOUNT() {
  yield put({
    type: 'user/SET_STATE',
    payload: {
      loading: true,
    },
  })
  const { authProvider } = yield select((state) => state.settings)
  const response = yield call(mapAuthProviders[authProvider].currentAccount)
  if (response) {
    const decoded = jsonwebtoken.decode(response.access_token)
    response.authUser = decoded.authUser
    yield store.set('id', id)
    try {
      const user = yield call(LoadUserProfile)
      if (user) {
        const { company } = user
        yield put({
          type: 'user/SET_STATE',
          payload: {
            ...user,
            preferredDateFormat: user.preferredDateFormat || 'DD/MM/YYYY',
            userId,
            id,
          },
        })
      }
    } catch (error) {
    }
  }else{
    store.set('refreshToken', response.refreshToken)
  }

  yield put({
    type: 'user/SET_STATE',
    payload: {
      loading: false,
    },
  })
}
WildThing
  • 969
  • 1
  • 12
  • 30
  • 1
    A refresh token is generally a token that does not expire and can only be used to generate a new access token (which expires). So you want to generate a refresh token and send that back in the api response when requesting an access token. Also store the refresh token next to the access token on the client, when the access token returns a 401 (is expired), call an e.g.`/token` endpoint on your api with the refresh token which returns a new access token. 'How the refresh token is generated and checked' is in the api and not part of your example code. – Bram Jul 27 '21 at 14:36
  • I have updated the code, so the login function generates and sets the tokens. Is it possible I can generate new access_token in the currentAccount function itself? – WildThing Jul 27 '21 at 15:07

2 Answers2

1

You can get a new access token with your refresh token using interceptors. Intercept and check for response status code 401, and get a new access token with your refresh token and add the new access token to the header.

Example:

return apiClient
  .post(actionUrl, { auth: 'basic' })
  .then(async response => {
    if (response) { // check for the status code 401 and make call with refresh token to get new access token and set in the auth header
      const { access_token: accessToken } = response.data
        store.set('accessToken', accessToken)
      return response.data
    }
    return false
  });

Simple Interceptor example,

axios.interceptors.request.use(req => {
  req.headers.authorization = 'token';
  return req;
});

Interceptor example for 401

axios.interceptors.response.use(response => response, error => {

    if (error.response.status === 401) {
       // Fetch new access token with your refresh token
       // set the auth header with the new access token fetched
    }
 });

There are several good posts on Interceptors usage for getting a new access token with your refresh token

https://thedutchlab.com/blog/using-axios-interceptors-for-refreshing-your-api-token

https://medium.com/swlh/handling-access-and-refresh-tokens-using-axios-interceptors-3970b601a5da

Automating access token refreshing via interceptors in axios

https://stackoverflow.com/a/52737325/8370370

dee
  • 2,244
  • 3
  • 13
  • 33
1

The above answer is good. But I found below method is better than that also using Axios Interceptors and "jwt-decode". Give it a try. (I'm using session storage for this example. You can use your own way to store the tokens securely)

Methodology

  1. Login to get an access token and long live refresh token and then store them securely.
  2. Create an axios instance to check the access token expiration with "jwt-decode". Then add the access token into the request if there is a valid access token, or else request a new access token using the stored refresh token and then apply the new access token into the request.

Login:

import axios from 'axios'

const handleLogin = async (login) => {
  await axios
  .post('/api/login', login, {
    headers: {
      'Content-Type': 'application/json'
    }
  })
  .then(async response => {
    sessionStorage.setItem('accessToken', response.data.accessToken)
    sessionStorage.setItem('refreshToken', response.data.refreshToken)
  })
  .catch(err => {
    if (errorCallback) errorCallback(err)
  })
 }

Create axios instance:

import axios from 'axios'
import jwt_decode from 'jwt-decode'
import dayjs from 'dayjs'

const axiosInstance = axios.create({
  headers: { 'Content-Type': 'application/json' }
})

axiosInstance.interceptors.request.use(async req => {
  const accessToken = sessionStorage.getItem('accessToken') ? sessionStorage.getItem('accessToken') : null

  if (accessToken) {
    req.headers.Authorization = `Bearer ${accessToken}`
  }

  const tokenData = jwt_decode(accessToken)
  const isExpired = dayjs.unix(tokenData.exp).diff(dayjs()) < 1

  if (!isExpired) return req

  const refreshToken = sessionStorage.getItem('refreshToken')

  const response = await axios.post('/api/refresh', { refreshToken }, {
    headers: {
      'Content-Type': 'application/json'
    }
  })

  req.headers.Authorization = `Bearer ${response.data.accessToken}`
  sessionStorage.setItem('accessToken', response.data.accessToken)

  return req
})

export default axiosInstance

Use axios instance in all the requests (Redux Toolkit Example):

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'

// Import axiosInstance
import axiosInstance from 'src/utils/axiosInstance'

export const getItems = createAsyncThunk(
  'appItems/getItems',
  async (args, { rejectedWithValue }) => {
    try {
      
      const response = await axiosInstance.get('/api/items')

      return response.data
    } catch ({ response }) {
      return rejectedWithValue({ code: response.status, ...response.data })
    }
  }
)
Ujith Nimantha
  • 167
  • 1
  • 16