I’ve been working on JWT Authentication implementation using Axios Interceptors
, Redux Toolkit
(w/ redux-persist
& redux-persist-transform-encrypt
). Every time my token expires I am trying to handle a 401
error but in vein.
To start with, this is my store.ts
configuration:
import storage from "redux-persist/lib/storage";
import { useDispatch, useSelector } from "react-redux";
import { persistReducer, persistStore } from "redux-persist";
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import { encryptTransform } from "redux-persist-transform-encrypt";
import type { AppDispatch, RootState } from "@/definitions";
import authReducer from "@/store/features/auth/authSlice";
import userReducer from "@/store/features/user/userSlice";
export const rootReducer = combineReducers({
auth: authReducer,
user: userReducer,
});
const persistConfig = {
key: "root",
storage,
transforms: [
encryptTransform({
secretKey: import.meta.env.VITE_REDUX_PERSIST_ENCRYPT_SECRET,
onError: function (error) {
console.error("Encryption error:", error);
},
}),
],
whitelist: ["user", "auth"],
};
const persistedReducer = persistReducer<RootState>(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
devTools: true,
});
export const persistor = persistStore(store);
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();
authSlice.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { AuthState, AuthResponse, RootState } from "@/definitions";
import { login, refreshTokens } from "@/store/features/auth/authThunks";
const initialState: AuthState = {
accessToken: null,
refreshToken: null,
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {
clearCredentials: (state) => {
state.accessToken = null;
state.refreshToken = null;
},
},
extraReducers: (builder) => {
builder.addCase(
refreshTokens.fulfilled,
(state, action: PayloadAction<AuthResponse>) => {
const { accessToken, refreshToken } = action.payload;
state.accessToken = accessToken;
state.refreshToken = refreshToken;
},
);
builder.addCase(refreshTokens.rejected, () => {
console.log("Refresh rejected");
});
},
});
export const { clearCredentials } = authSlice.actions;
export const getAccessToken = (state: RootState) => state.auth.accessToken;
export const getRefreshToken = (state: RootState) => state.auth.refreshToken;
export default authSlice.reducer;
authThunks.ts
export const refreshTokens = createAsyncThunk(
"auth/refreshTokens",
async (refreshToken: string) => {
try {
const response = await api.post<AuthResponse>("/auth/refresh", {
refreshToken,
});
return response.data; // this is being handled in `authSlice`'s `extraReducer` on `refreshTokens.fullfilled`
} catch (error) {
console.error(error);
throw error;
}
},
);
index.ts
(axios configuration)
const api = axios.create({
baseURL,
});
let isRetry: boolean = false;
api.interceptors.response.use(
(response) => response,
async (error) => {
// initially, I get an error that my originalRequest is failed, let's say `GET /posts` has failed
const originalRequest = error.config;
if (error.response.status === 401 && !isRetry) {
try {
isRetry = true;
const refreshToken = getRefreshToken(store.getState());
await store.dispatch(refreshTokens(refreshToken!)); // when it comes to this part, I do not get an error that my `POST /auth/refresh` is rejected, I get the initial `GET /posts` 401 error
console.log("here"); // it does not come to this part
const newAccessToken = getAccessToken(store.getState());
console.log("here2");
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
console.log("here3");
return api(originalRequest);
} catch (error) {
// the error is not even being logged here
if (error.response) {
console.error(error.response);
}
}
} else {
store.dispatch(clearCredentials());
store.dispatch(clearMe());
window.location.href = "/login";
}
return Promise.reject(error.response);
},
);
So after the first try to refresh tokens, I get the second error from the original request. After this my isRetry
is already true, so the execution proceeds to else
block and then redirects the user to /login
page.
The funny part is that if I debug this in browser with debugger, I can get to the part where I do my POST /auth/refresh
, get the second error from original request, refresh the page and it seems that my tokens have been successfully updated and the new GET /users
has succeeded with a new accessToken
.
At first I had my thunks set up in a way where I do the request, get the data and then dispatch the data to the store using thunkAPI
. Rewrote it to handle thunks in extraReducers, the issue persisted.