skip to Main Content

I have the following frontend:

AuthContext.js:

const { createContext, useState, useContext } = require("react");

const AuthContext = createContext();

export function useAuth() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const [auth, setAuth] = useState();

  return (
    <AuthContext.Provider value={{
      auth,
      setAuth
    }}>
      {children}
    </AuthContext.Provider>
  )
}

So here is the global [auth, setAuth] authentication object that will hold accessToken and refreshToken via JWT authentication. The auth object is populated upon login from database:

login.js:

import React, { useEffect, useState } from 'react'
import axios from '../api/axios';
import { NavLink, useLocation, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';

const LOGIN_URL = '/login';

function Login() {
  const { auth, setAuth } = useAuth();

  const navigate = useNavigate();
  const location = useLocation();

  const from = location.state?.from?.pathname || '/';
  const errorMessage = location.state?.errorMessage;

  const [user, setUser] = useState('');
  const [pwd, setPwd] = useState('');
  const [errMsg, setErrMsg] = useState('');


  useEffect(() => {
    setErrMsg('');
  }, [user, pwd]);

  useEffect(() => {
    if (errorMessage) {
      setErrMsg(errorMessage);
    }
  }, [])

  async function handleSubmit(e) {
    e.preventDefault();
    try {
      const res = await axios.post(LOGIN_URL, {
        username: user,
        password: pwd
      });
      const accessToken = res.data.accessToken;
      const refreshToken = res.data.refreshToken;
      const roles = res.data.roles;
      setAuth({ user, pwd, accessToken, refreshToken, roles }); // POPULATING AUTHENTICATION OBEJCT
      console.log(refreshToken); // debug what is current refresh token
      setUser('');
      setPwd('');
      navigate(from, { replace: true });
    } catch (err) {
      if (err.response?.status === 403) {
        setErrMsg('wrong username or password');
      } else {
        setErrMsg('Login Failed');
      }
    }
  }

  return (
    <section>
      <p className={errMsg ? 'errmsg' : 'hide'}>{errMsg}</p>
      <h1>Sign In</h1>
      <form onSubmit={handleSubmit}>
        <label htmlFor="username">Username:</label>
        <input type="text" id="username" onChange={e => setUser(e.target.value)} value={user} required />
        <label htmlFor="password">Password:</label>
        <input type="password" id="password" onChange={e => setPwd(e.target.value)} value={pwd} required />
        <button>Sign In</button>
      </form>
      <p>
        Need an Account? <br />
        <NavLink className="link" to='/register'>Sign Up</NavLink>
      </p>
    </section>
  )
}

export default Login

When the accessToken expires, the frontend send request to GET /refresh to get new accessToken. This works fine:

useRefreshToken.js:

import { useAuth } from '../context/AuthContext';
import axios from '../api/axios';

function useRefreshToken() {
  const { auth, setAuth } = useAuth();

  async function refresh() {
    console.log(auth.refreshToken);
    const res = await axios.get('/refresh', {
      headers: {
        'Refresh-Token': auth.refreshToken
      }
    });
    setAuth(prev => {
      return { ...prev, accessToken: res.data };
    });
    return res.data;
  }
  return refresh;
}

export default useRefreshToken;

Also, the request for /refresh is done automatically, via axios interceptors, whenever the accessToken expires (signaled via 401 http status for a protected resource):

useAxiosJwt.js:

import useRefreshToken from "./useRefreshToken"
import { useAuth } from '../context/AuthContext';
import { axiosJwt } from "../api/axios";
import { useEffect } from "react";

function useAxiosJwt() {
  const { auth } = useAuth();
  const refresh = useRefreshToken();

  useEffect(() => {
    const requestInterceptor = axiosJwt.interceptors.request.use(
      conf => {
        if (!conf.headers['Authorization']) {
          conf.headers['Authorization'] = `Bearer ${auth.accessToken}`;
        }
        return conf;
      }, err => Promise.reject(err)
    )

    const responseInterceptor = axiosJwt.interceptors.response.use(
      res => res,
      async err => {
        const prevReq = err.config;
        if (err.response.status === 401 && !prevReq.sent) {
          prevReq.sent = true;
          const newAccessToken = await refresh();
          prevReq.headers['Authorization'] = `Bearer ${newAccessToken}`;
          return axiosJwt(prevReq);
        }
        return Promise.reject(err);
      }
    )

    return () => {
      axiosJwt.interceptors.request.eject(requestInterceptor);
      axiosJwt.interceptors.request.eject(responseInterceptor);
    }
  }, [auth]);

  return axiosJwt;
}

export default useAxiosJwt

The automatic refreshing also works fine. The problem is when the refreshToken expires. When that happen, the user have to login again, so that auth object is populated with the new refreshToken (I will show backend doing this after this). The backend simply removes the refreshToken when it expires (when hitting /refresh) and creates new one upon login. And the backend does returns correct new refreshToken. But that new refreshToken is not reflected in the reacts state (setAuth({ user, pwd, accessToken, refreshToken, roles });), even though is confirmed by debugging console.log(refreshToken) that is indeed is new refreshToken (so backend is working fine). So when next /refresh is hit, react send old refreshToken which is no longer in backend (was removed when the old expired). So why is react not reflecting the new refreshToken returned from backend?

Here is the firefox console (with added comments by me for clarification):

50362bcc-7ee7-47c3-88bb-6748b4475c67 Login.js:43 // this is first login so it returns currently stored refresh token from backend's database
50362bcc-7ee7-47c3-88bb-6748b4475c67 useRefreshToken.js:8 // debugging the current token
XHRGET
http://localhost:8080/user // asking for protected resource, but access token has expired
[HTTP/1.1 401  15ms]

50362bcc-7ee7-47c3-88bb-6748b4475c67 useRefreshToken.js:8 // again debugging current refresh token
XHRGET
http://localhost:8080/refresh // automatically going for refresh to get new access token, this works fine
[HTTP/1.1 200  22ms]

XHRGET
http://localhost:8080/user //asking for protected resource with new accessToken, works fine
[HTTP/1.1 200  55ms]

XHRGET
http://localhost:8080/user // asking for protected resource again (for testing), again access token expired (it has deliberately low duration for testing)
[HTTP/1.1 401  10ms]

50362bcc-7ee7-47c3-88bb-6748b4475c67 useRefreshToken.js:8 // current refresh token
XHRGET
http://localhost:8080/refresh // now the refresh token itself expired (400 status, chosen by design), so the user has to login again
[HTTP/1.1 400  20ms]

XHRPOST
http://localhost:8080/login // logging to get new refresh token
[HTTP/1.1 200  196ms]

7eef8b87-9be6-42e3-85ae-4d607ceb5e27 Login.js:43 // this is the new refresh token from backend (previous was 50362bcc-7ee7-47c3-88bb-6748b4475c67)
XHRGET
http://localhost:8080/user // asking for protected resource with new refresh token
[HTTP/1.1 200  50ms]

XHRGET
http://localhost:8080/user // but access token expired so going to refresh
[HTTP/1.1 401  11ms]

50362bcc-7ee7-47c3-88bb-6748b4475c67 useRefreshToken.js:8 // debug wrong - old refresh token, the new one was not setAuth'ed, dont know why
XHRGET
http://localhost:8080/refresh // so axios sending the old refresh token (50362bcc-7ee7-47c3-88bb-6748b4475c67), instead of the new one (7eef8b87-9be6-42e3-85ae-4d607ceb5e27) so this is the error.
[HTTP/1.1 403  29ms]

From the console you can see the new refresh token is sent from backend, but not reflected by setAuth in react. Why?

Edit:
here is backend controller so that you know what endpoints return. As I said previously, backend works fine,the console.log debugging proves that. This is only for clarification:

AuthController.java:

@RestController
@AllArgsConstructor
public class AuthController {
    private AuthenticationManager authenticationManager;
    private AuthService authService;
    private JwtService jwtService;
    private RefreshTokenService refreshTokenService;

    @PostMapping("/register")
    public void register(@RequestBody RegisterDtoin registerDtoin) {
        authService.createUserByRegistration(registerDtoin);
    }

    @PostMapping("/login") // the refresh token is deleted if expired (done in authService.getUserByLogin, not shown) and recreated and returned (again as correctly shown in console.log(refreshToken)) so LoginDtout is {String acessToken, String refreshToken, Long id, String username, and List roles}, all retuned by this endpoint
    public ResponseEntity<LoginDtoout> login(@RequestBody LoginDtoin loginDtoIn) {
        Authentication authentication = authenticationManager.authenticate(
                 new UsernamePasswordAuthenticationToken(
                         loginDtoIn.username,
                         loginDtoIn.password
                 )
        );
        if(authentication.isAuthenticated()){
            return ResponseEntity.ok(authService.getUserByLogin(loginDtoIn));
        } else {
            throw new UsernameNotFoundException("invalid user request");
        }
    }

    @GetMapping("/refresh") // returns only new accessToken, new refreshToken is obtained from login
        public ResponseEntity<String> refresh(@RequestHeader("Refresh-Token") String rtoken) throws RefreshTokenExpiredException {
        RefreshToken refreshToken = refreshTokenService.findRefreshTokenByToken(rtoken);
        if(refreshTokenService.hasExpired(refreshToken)){
            throw new RefreshTokenExpiredException("refresh token has expired. Please login again to create new one");
        }
        String newJwt = jwtService.generateToken(refreshToken.getUser().getUsername());
        return ResponseEntity.ok(newJwt);
    }
}

source code:
https://github.com/shepherd123456/authentication_spring_and_react

This is for those downloading the source code from the provided github link: when git cloning the source code and running backend (spring boot), spring entities are mysql tables (and the database of your mysql has to be renamed in application.properties of course). When they are created for the first time, there is table "role" (Role entity). You have to populate it with
insert into role(name) values('USER'), ('EDITOR'), ('ADMIN');
so that when user is created (registered), the role "USER" is automatically assigned to him. That’s all. Also, the react app is in src/main/frontend, and you need to install dependency npm i and then started the create-react-app development server by npm run start (anyone who has worked with react knows that). There should be no CORS problems, since spring boot SecurityConfig.java is allowing localhost:3000 origin, which is where react’s development server runs by default. But as I said in my original post, the backend is not a problem here. I really want to know why is react’s setAuth not setting new refreshToken upon login (when the old expired) and instead is using the old one. Because using the old one, which is deleted (automatically when expires), cause nullException of course. Because simply the new one, which is correctly creating HAS TO be setted in reacts state (via setAuth) after login.

2

Answers


  1. It seems like the issue might be related to the way you’re updating the state with the new refreshToken in the useRefreshToken hook.

    In the setAuth function, you are spreading the previous state (prev) and updating only the accessToken. However, you also need to update the refreshToken with the new value received from the backend. You should modify the setAuth function to include the updated refreshToken as well. Here’s the modified version:

    function useRefreshToken() {
      const { auth, setAuth } = useAuth();
    
      async function refresh() {
        console.log(auth.refreshToken);
        const res = await axios.get('/refresh', {
          headers: {
            'Refresh-Token': auth.refreshToken
          }
        });
        setAuth(prev => {
          return { ...prev, accessToken: res.data.accessToken, refreshToken: res.data.refreshToken };
        });
        return res.data.accessToken;
      }
      return refresh;
    }
    

    This modification ensures that both accessToken and refreshToken are updated in the state. Now, when you call refresh, it should correctly update the refreshToken in the state, and subsequent requests using the updated auth object should work as expected.

    Login or Signup to reply.
  2. In your authProvider can you change your code to the following

    
    const { createContext, useState, useContext } = require("react");
    
    const AuthContext = createContext();
    
    export function useAuth() {
      return useContext(AuthContext);
    }
    
    export function AuthProvider({ children }) {
      const [auth, setAuth] = useState();
    
    
    const saveAuth = useCallback(
        (key ,value) => {
          setAuth((prevValues) => ({...prevValues, [key]: value }))
        },
        [setAuth],
      )
    
      const [contextValue, setContextValue] = useState({ auth, saveAuth })
    
      useEffect(() => { setContextValue({ auth, saveAuth }) }, [auth, saveAuth])
    
      return (
        <AuthContext.Provider value={contextValue}>
          {children}
        </AuthContext.Provider>
      )
    }
    
    

    and when ever you want to update the state you can do

    const {saveAuth} = useAuth();
    saveAuth("accessKey", "yourActualAccessKey");
    

    Let me know if this works.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search