Search code examples
reactjsoauth-2.0oauthkeycloak

Error "Session doesn't have required client" when using refresh token to renew an access token in Keycloak with React public client having useEffect


I figured out this error in my case, and just want to document it.

Similar problems

Environment

  • React Vite: template React + TS v5.3.1
  • Keycloak (KC): running on Docker, v25.0.1

Problem

I need to implement OAuth 2, specifically the Authorization Code Flow and the Refreshing an Access Token Flow. First, I created a React public client, and ran a default KC instance, acting as my authorization server. The AuthZ Code Flow worked fine, and I received all the necessary data, including an access token and a refresh token. To perform the next flow, I sent a POST request to KC's endpoint /token with the urlencoded body as grant_type=refresh_token;client_id=clientIdHere;refresh_token=refreshTokenHere, as instructed in here. For some reason, KC refused and returned an error like this:

{
    "error": "invalid_grant",
    "error_description": "Session doesn't have required client"
}

KC Image setup

Code

# running the default KC instance
docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak start-dev

# useEffect code
const navigate = useNavigate();

useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const authZCode = params.get("code");

    const fetchDataToLS = async (codeHere: string) => {
        const tokenRes = await axios.post<TKCTokenResponse>(
            import.meta.env.VITE_KC_SERVER_TOKEN,
            new URLSearchParams({
                client_id: "myclient",
                code: codeHere,
                grant_type: "authorization_code",
            })
        );

        const { access_token, refresh_token, id_token } = tokenRes.data;
        localStorage.setItem("access_token", access_token);
        localStorage.setItem("refresh_token", refresh_token);
        localStorage.setItem("id_token", id_token);
        navigate("/protected");
    };

    fetchDataToLS(authZCode!);
    callTime++;
}, [callTime, navigate]);

Solution

  • From what I could gather, there are 2 main reasons causing this error:

    • the auth_code-token exchange function is written inside useEffect, which will make a second run if in React Strict Mode
    • OAuth 2, by proposed standard, will deny and revoke the authZ code, if the client (my React public client) use the code more than 1 time, AND also revoke all tokens previously issued from that authZ code

    As I was testing KC, my code was still in development mode, hence the double run from useEffect, which are captured like this, regardless of the order of those requests. And because React used the same authZ code a 2nd time, which might be the main reason, making KC denied and revoked that code, thus the refresh token associated with that authZ code would not be valid anymore.

    To fix this, you could try to add logic so that the exchange should be run one time only, though I'm not sure this is an optimal solution.