Refresh JWT Token MERN Axios Interceptors?

Hi guys,

I’m trying to create a refresh token after my first token expires so that a user can continue to do CRUD functionalities and continue to be logged in. However, I am getting a 403 Forbidden error after a user tries to make a comment and a 401 Unauthorized error after trying to refresh a token. I also see in the Redux devtools the token is still the same old token and doesn’t show a new one.

I added the code below that i believe can most helpful. It’s a MERN project with Redux and Axios. Im trying to store JWT in cookies. Any help would be appreciated. Thanks!

Screenshot 2023-03-13 at 3.25.13 PM

FRONTEND

interceptor.js


import axios from 'axios';
import { store } from '../store/store'

const axiosPrivate = axios.create({
    baseURL: `${process.env.REACT_APP_URL}/api`,
    withCredentials: true
});

axiosPrivate.interceptors.request.use(
    config => {

/* const user = {access: "token" } */
        const user = store.getState().auth.user
        if (user) {
            config.headers['Authorization'] = `Bearer ${user.access}`
        }
        return config;
    },
    error => {
        return Promise.reject(error);
    }
);

axiosPrivate.interceptors.response.use(
    response => response,
    async (error) => {
        const prevRequest = error?.config;
        if (error?.response?.status === 403 && !prevRequest?.sent) {
            prevRequest.sent = true;
            const newAccessToken = await axios.get(`${process.env.REACT_APP_URL}/api/auth/refresh`, { withCredentials: true })
            axios.defaults.headers.common["Authorization"] = `Bearer ${newAccessToken} `
            return axiosPrivate(prevRequest);
        }
        return Promise.reject(error);
    }
);



export default axiosPrivate

PostDetails.js

 // Function to create a new comment and display new comment by calling fetchComments
    const submitComment = (data) => {
        (async () => {
            try {
                await axiosPrivate.post(`/blog/post/${id}/comment/newComment`, data)
                console.log("new comment");
                fetchComments()
                toast.success("You added a new comment!")
            } catch (error) {
                if (error.response) setErrorsServer(error.response.data.errors)
                toast.error("Unable to create a new comment")
            }
        })();
        reset()
        if (errorsServer) setErrorsServer('')
    }

BACKEND

verifyJWT.js

const User = require('../models/userSchema')

const jwt = require('jsonwebtoken')

const verifyJWT = (req, res, next) => {
    const authHeader = req.headers.authorization || req.headers.Authorization

    if (!authHeader?.startsWith('Bearer ')) {
        return res.status(401).json({ message: 'Unauthorizedd' })
    }

    const token = authHeader.split(' ')[1]

    jwt.verify(
        token,
        process.env.ACCESS_TOKEN,
        (err, decoded) => {
            if (err) return res.status(403).json({ message: 'Forbidden' })
            req.user = User.findById(decoded.id).select('-password')
            next()
        }
    )
}

module.exports = verifyJWT

auth.js

/* Create JWT Token */
const jwt = require('jsonwebtoken')

const accessToken = (id) => {
    return jwt.sign({ id }, process.env.ACCESS_TOKEN, { expiresIn: '10s' })
}

const refreshToken = (id) => {
    return jwt.sign({ id }, process.env.REFRESH_TOKEN, { expiresIn: '20s' })
}

/* accessToken created at LOGIN*/


router.get("/refresh", (req, res, next) => {
    const cookies = req.cookies

    if (!cookies?.jwt) return res.status(401).json({ message: 'Unauthorized' })

    const refreshToken = cookies.jwt

    jwt.verify(
        refreshToken,
        process.env.REFRESH_TOKEN,
        async (err, decoded) => {
            if (err) return res.status(403).json({ message: 'Forbidden' })

            const foundUser = await UserService.find({ _id: decoded.id })

            if (!foundUser) return res.status(401).json({ message: 'Unauthorized' })

            const token = accessToken(foundUser._id)

            return res.status(200).json({ token })
        }
    )
})

Hello there,

I would do this slightly differently. Currently, you appear to be trying to set the credentials on the client side:

However, I would set the JWT on the server side:

I might be missing something, but I hope this helps

Thanks for the reply. I’m not sure if i understood correctly but here is what I got.

I believe that I am assigning the new token on the server side. While using Postman I first sign in and get a token. Then to do a GET request to (/api/auth/refresh) I paste the token I got from signing in into the Headers option in Postman by doing

Bearer TOKEN STRING

Then I click the button to do my GET request and I get a new token.

In the axios interceptor I try to do a get request to (/refresh) after checking if my old token expired and assign the new token to my headers. That way I can continue to do CRUD functionalities (just like in the PostDetails.js I use the interceptor which should have the appropriate header already assigned).

I believe my issue is that when I do the refresh request in the interceptor it’s unable to go through because it doesn’t have a token assigned in its headers which could explain the 401 error. The 403 I think is because the first token already expired thus unable to do the post request to add a new comment.

I tried adding a the old token as a header through doing the below in the interceptor.

const user = store.getState().auth.user

axios.get(url, {
headers:{
Authorization: ${user.access})

or axiosPrivate.get(url)

I was able to get rid of my 401 error by creating another file and making an axios call to do my refresh and added it to my interceptor.

I still get the 403 Forbidden for when I try to create a new comment. I see in the network tab that there is a token in the authorization header but it is the old token from when I logged in. It wasn’t replaces with the new token. Which makes me wonder if the refresh token even happened or the new token isn’t properly replacing the old token in my store.

authService.js

const refresh = async () => {
    const response = await axios.get(`${API_URL}/auth/refresh`)
    return response.data
}

authSlice.js


const initialState = {
    user: null,
    isLoading: false,
    message: ''
}

export const refresh = createAsyncThunk('auth/refresh', async (thunkAPI) => {
    try {
        const data = await authService.refresh()
        const { token } = data
        console.log(token)
        return { token }
    } catch (error) {
        const message = "Unable to refresh token"
        return thunkAPI.rejectWithValue(message)
    }
})

export const authSlice = createSlice({
    name: 'auth',
    initialState,
    reducers: {
        reset: (state) => {
            state.isLoading = false
            state.message = ''
            state.user = null
        }
    },
    extraReducers: (builder) => {
        builder
            .addCase(refresh.pending, (state) => {
                state.isLoading = true
                state.user = null
            })
            .addCase(refresh.fulfilled, (state, action) => {
                state.isLoading = false
                state.user = action.payload
            })
            .addCase(refresh.rejected, (state, action) => {
                state.isLoading = false
                state.message = action.payload
                state.user = null
            })
       
    }
})


export const { reset } = authSlice.actions //addresses something else in my code
export default authSlice.reducer

hook folder/useRefreshToken.js

import { refresh } from '../features/auth/authSlice'
import { useDispatch } from 'react-redux';

const useRefreshToken = () => {
    //const { setAuth } = useAuth();
    const dispatch = useDispatch()

    const refreshToken = async () => {
        dispatch(refresh())
        console.log("refresh token");
    }
    return refreshToken;
};

export default useRefreshToken;

Response Intereceptor Now

axiosPrivate.interceptors.response.use(
    response => response,
    async (error) => {
        const prevRequest = error.config;
        if (error.response.status === 403 && !prevRequest._retry) {
            prevRequest._retry = true;
            const newAccessToken = await useRefreshToken()
            console.log(newAccessToken)
            prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;
            return axiosPrivate(prevRequest);
        }
        return Promise.reject(error);
    }
);

Again, what I would expect is:

// CLIENT
console.log(browser.cookies.get({name: "access_token"})); // old-token
const res = await fetch('/protected');
console.log(browser.cookies.get({name: "access_token"})); // new-token
// SERVER
router.get('/protected', (req, res) => {
  // Check if cookie is still valid
  if (req.cookies === 'old-token') {
    // Generate new cookie, and send it
    // to the client in the response (the
    // browser knows what to do with this)
    res.cookie(jwt.sign('new-token'));
  }
  return res.status(200);
});

The client should not have to wait for a failed request, before requesting a new cookie (unless you want some explicit input from the user).
The client should not set its own cookies. It should let the response update the necessary cookies.

If you want to go a different way, that is fine, but I am not sure what you are wanting to do.

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.