2

First of all, thanks in advance to anyone who reads my question and comments. I have a CRA app that is using keycloak-js and ReactKeycloakProvcer from `@react-keycloak/web. When you first load the application page and login, keycloak is initialized correctly and functions as it should. The provider gets an instance of KC in a pretty standard way.

import keycloak from './authentication/keycloak'

const KeycloakProviderBlock = ({children}) => {
    return (
        <ReactKeycloakProvider authClient={keycloak} initOptions={{onLoad: 'login-required'}}>
            {children}
        </ReactKeycloakProvider>
    );
};

Later in my axios wrapper, I am pulling the KC token out to add to all requests as the bearer token like this:

import keycloak from "./authentication/keycloak";
const {authenticated} = keycloak;
    if (authenticated) {
        client.defaults.headers.common = {
            ...client.defaults.headers.common,
            Authorization: `Bearer ${keycloak.token}`,
        };
    } else {
        logger.error("Request client used before KeyCloak initialized");
    }

My keycloak file just returns a new instance of KC --> contents of /authentication/keycloak.js

import Keycloak from "keycloak-js";

const keycloak = new Keycloak({
    realm: process.env.REACT_APP_KEYCLOAK_REALM,
    url: process.env.REACT_APP_KEYCLOAK_URL,
    clientId: process.env.REACT_APP_KEYCLOAK_CLIENT,
})

export default keycloak

Everything works like it should until the user hard refreshes the page. When the page reloads keycloak.authenticated is not present on the KC object, so all HTTP calls fail because there is no Bearer token.

I'm using keycloak-js version 15.0.2. Any/all thoughts appreciated.

Griff
  • 1,796
  • 3
  • 23
  • 48

1 Answers1

3

I figured out how to fix this and figured I would post the answer in case it helps anyone else. It turns out when the page refreshes, it takes KC longer to restore the session information from the cookie than it does for the code to run. So there was a race condition when the page reloaded it was trying to reach the backend before KC had a chance to validate the user was indeed logged in. It turns out that the KeycloakProvider emits events and tells you when the token is refreshed. The events were not firing because I had the KCProvider wrapped in a JSX component, so the events were not properly bound. I removed the unnecessary block and the events started to fire. From there, it was pretty easy to display a Loading throbber and block the rest of the components from rendering until the provider actually got the onReady event. The new code looks like this:

In App.js

   // Keycloak
    onKeycloakEvent = (event, error) => {
        console.log('onKeycloakEvent', event, error)
        console.log(`Keycloak Event ${event}`);
        if(event && event === 'onReady'){
            this.setState({keycloakReady: true})
        }
    }

    onKeycloakTokens = (tokens) => {
        console.log('onKeycloakTokens', tokens)
    }

... In the Render Pass the functions to the provider:

<ReactKeycloakProvider authClient={keycloak} initOptions={{onLoad: 'login-required'}}
                        keycloak={keycloak}
                        onEvent={this.onKeycloakEvent}
                        onTokens={this.onKeycloakTokens}>
                        <SplashScreen keycloakReady={keycloakReady}>
                        ....
                        </SplashScren>
</ReactKeycloakProvder>

Then in the SplashScreen only render children if KC is ready:

import React, {Component, PureComponent, ReactChildren} from "react";
import LoadingSpinner from "../navigation/LoadingSpinner";

type PassedProps = {
    keycloakReady: boolean;
}

class SplashScreen extends PureComponent<PassedProps, any> {
    constructor(props: PassedProps) {
        super(props);
    }

    render() {
        console.log(`SplashScreen keycloak ready ${this.props.keycloakReady}`);
        if(!this.props.keycloakReady){
            return <LoadingSpinner/>
        }else{
            return this.props.children
        }
    }
}

export default SplashScreen
Griff
  • 1,796
  • 3
  • 23
  • 48