DEV Community

Marian Salvan
Marian Salvan

Posted on

Configuring React Application with Keycloak

Introduction

In the previous section, we secured a .NET API and validated access using Keycloak.

Now we will integrate a React client so users can authenticate with the same Keycloack instance and call protected API endpoints from the browser.

Note: This article assumes that you have a basic knowledge in React components, contexts and hooks

This section contains the following:

  1. Add React service to Docker Compose
  2. Configure the Keycloak client for React
  3. Configure the audience mapper
  4. Install dependencies and configure Keycloak in React
  5. Create the Keycloak provider and context
  6. Add a simple token service
  7. Protect routes in React
  8. Add login, logout, and profile actions
  9. Testing the authentication and authorization
  10. Conclusion

Add the React service in Docker Compose

If your compose file does not already include the React app, add a frontend service:

services:
  reactapp:
    build:
      context: reactapp.client
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    depends_on:
      - backend
      - keycloak
    networks:
      - app-network
Enter fullscreen mode Exit fullscreen mode

After this, your app should be reachable at:

Configure the Keycloak client for React

Create a new client in Keycloak for the React app, for example: react-client.

Use these settings:

  • Client type: OpenID Connect
  • Client authentication: Off (public client)
  • Authorization: Off
  • Standard flow: On
  • Direct access grants: Off (optional, recommended for browser apps)

In Login settings, set:

This configuration allows the SPA to complete the login flow and return to the app correctly.

Configure the audience mapper

When the React app calls the .NET API, it sends the access token obtained from Keycloak. However, recall that the .NET API is configured to validate the audience field in the token:

ValidAudience = "net-web-api"
Enter fullscreen mode Exit fullscreen mode

By default, tokens issued to the react-client do not include net-web-api as an audience — they are scoped to the React client itself. Without the mapper, every API call from the React app will return 401 Unauthorized.

To fix this, go to the react-client configuration in Keycloak, navigate to Client Scopes, select the react-client-dedicated scope, and create a new mapper of type Audience with the following settings:

  • Name: net-web-api
  • Included Client Audience: net-web-api
  • Add to access token: On

After saving, tokens issued to react-client will include net-web-api in the aud claim, and the .NET API will accept requests coming from the React frontend.

You can verify this by decoding the access token at jwt.io and checking that the aud array contains net-web-api:

{
  "aud": ["net-web-api", "account"],
  ...
}
Enter fullscreen mode Exit fullscreen mode

Install dependencies and configure Keycloak in React

Inside the React project folder, install the required packages:

npm install keycloak-js react-router-dom
Enter fullscreen mode Exit fullscreen mode

Then create the Keycloak configuration file:

import Keycloak from "keycloak-js";

const keycloak = new Keycloak({
  url: "http://localhost:8080",
  realm: "master",
  clientId: "react-client",
});

export default keycloak;
Enter fullscreen mode Exit fullscreen mode

Create Keycloak provider and context

Create a provider that:

  • Initializes Keycloak when the app starts
  • Stores access and refresh tokens
  • Refreshes tokens when they expire
import React, { createContext, useEffect, useState, ReactNode } from "react";
import Keycloak from "keycloak-js";
import keycloak from "./KeycloakConfig";
import { tokenService } from "../services/TokensService";

interface KeycloakContextType {
  keycloakInstance: Keycloak | null;
  authenticated: boolean;
}

export const KeycloakContext = createContext<KeycloakContextType>({
  keycloakInstance: null,
  authenticated: false,
});

export const KeycloakProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [keycloakInstance, setKeycloakInstance] = useState<Keycloak | null>(null);
  const [authenticated, setAuthenticated] = useState(false);

  useEffect(() => {
    const initializeKeycloak = async () => {
      try {
        const auth = await keycloak.init({
          onLoad: "login-required",
          redirectUri: "http://localhost:3000",
        });

        if (auth) {
          setKeycloakInstance(keycloak);

          tokenService.setTokens({
            accessToken: keycloak.token!,
            refreshToken: keycloak.refreshToken!,
          });

          //this handles the auto refresh of the access token
          keycloak.onTokenExpired = () => {
            keycloak
              .updateToken(-1)
              .then((refreshed) => {
                if (refreshed) {
                  tokenService.setTokens({
                    accessToken: keycloak.token!,
                    refreshToken: keycloak.refreshToken!,
                  });
                }
              })
              .catch(() => {
                keycloak.login();
              });
          };
        }

        setAuthenticated(auth);
      } catch (error) {
        console.error("Keycloak init error:", error);
      }
    };

    initializeKeycloak();

    return () => {
      tokenService.clearTokens();
    };
  }, []);

  return (
    <KeycloakContext.Provider value={{ keycloakInstance, authenticated }}>
      {children}
    </KeycloakContext.Provider>
  );
};
Enter fullscreen mode Exit fullscreen mode

Add a simple token service

interface Tokens {
  accessToken: string;
  refreshToken: string;
}

export const tokenService = {
  setTokens(tokens: Tokens) {
    localStorage.setItem("access_token", tokens.accessToken);
    localStorage.setItem("refresh_token", tokens.refreshToken);
  },

  getTokens(): Tokens | null {
    const accessToken = localStorage.getItem("access_token");
    const refreshToken = localStorage.getItem("refresh_token");

    if (accessToken && refreshToken) {
      return { accessToken, refreshToken };
    }

    return null;
  },

  clearTokens() {
    localStorage.removeItem("access_token");
    localStorage.removeItem("refresh_token");
  },
};
Enter fullscreen mode Exit fullscreen mode

Protect routes in React

Create a route guard so unauthenticated users cannot access protected pages:

import { Navigate } from "react-router-dom";
import { JSX, useContext } from "react";
import { KeycloakContext } from "../providers/KeycloakContext";

interface ProtectedRouteProps {
  children: JSX.Element;
}

export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
  const { keycloakInstance, authenticated } = useContext(KeycloakContext);

  if (!keycloakInstance) {
    return <div>Loading...</div>;
  }

  return authenticated ? children : <Navigate to="/" />;
};
Enter fullscreen mode Exit fullscreen mode

Then wrap routes with this component:

<Routes>
  <Route path="/" element={<ProtectedRoute><Home /></ProtectedRoute>} />
</Routes>
Enter fullscreen mode Exit fullscreen mode

Add login, logout, and profile actions

Let's add basic user management functionality for login, logout and navigating to the profile. This is easy to do because the KeycloakContext already exposes the keycloakInstance that allows us to access this functionality.

import { useContext } from "react";
import { KeycloakContext } from "../providers/KeycloakContext";

export const LoginButton = () => {
    const { keycloakInstance, authenticated } = useContext(KeycloakContext);

    const login = () => {
        keycloakInstance?.login();
    };

    return <button onClick={login} disabled={authenticated}>Login</button>;
};

export default LoginButton;
Enter fullscreen mode Exit fullscreen mode
import { useContext } from "react";
import { KeycloakContext } from "../providers/KeycloakContext";

export const LogOutButton = () => {
    const { keycloakInstance, authenticated } = useContext(KeycloakContext);

    const logout = () => {
        keycloakInstance?.logout();
    };

    return <button onClick={logout} disabled={!authenticated}>Logout</button>;
}

export default LogOutButton;
Enter fullscreen mode Exit fullscreen mode
import { useContext } from "react";
import { KeycloakContext } from "../providers/KeycloakContext";

const ProfileButton = () => {
    const { keycloakInstance, authenticated } = useContext(KeycloakContext);

    const viewProfile = () => {
        const accountUrl = keycloakInstance?.createAccountUrl() ?? '';
        window.location.href = accountUrl;
    };

    return <button onClick={viewProfile} disabled={!authenticated}>Profile</button>;
};

export default ProfileButton;
Enter fullscreen mode Exit fullscreen mode

The final App component should look something like this:

function App() {
    return (
        <div>
            <LoginButton/>
            <ProfileButton/>
            <LogoutButton  />

            <BrowserRouter>    
                <Routes>
                    <Route path="/" element={ <ProtectedRoute><Home /></ProtectedRoute> }/>
                </Routes>
            </BrowserRouter>
      </div>

    );
}
Enter fullscreen mode Exit fullscreen mode

Testing the authentication and authorization

Now that we have the setup for the boiler plate, let's create a Home component that will call the weatherforecast API method and will display the results in a table:

import { useContext, useEffect, useState } from 'react';
import './Home.css';
import { KeycloakContext } from '../../providers/KeycloakContext';

interface Forecast {
    date: string;
    temperatureC: number;
    temperatureF: number;
    summary: string;
}

export const Home = () => {
    const [forecasts, setForecasts] = useState<Forecast[]>();
    const { keycloakInstance, authenticated } = useContext(KeycloakContext);

    useEffect(() => {
        console.log(authenticated);
        populateWeatherData();
    }, []);

    async function populateWeatherData() {
        const response = await fetch('http://localhost:5080/api/weatherforecast', {
            headers: {
                Authorization: `Bearer ${keycloakInstance?.token}`,
            },
        });
        if (response.ok) {
            const data = await response.json();
            setForecasts(data);
        }
    }

    const contents = forecasts === undefined
        ? <p><em>Loading... Please refresh once the ASP.NET backend has started. See <a href="https://aka.ms/jspsintegrationreact">https://aka.ms/jspsintegrationreact</a> for more details.</em></p>
        : <table className="table table-striped" aria-labelledby="tableLabel">
            <thead>
                <tr>
                    <th>Date</th>
                    <th>Temp. (C)</th>
                    <th>Temp. (F)</th>
                    <th>Summary</th>
                </tr>
            </thead>
            <tbody>
                {forecasts.map(forecast =>
                    <tr key={forecast.date}>
                        <td>{forecast.date}</td>
                        <td>{forecast.temperatureC}</td>
                        <td>{forecast.temperatureF}</td>
                        <td>{forecast.summary}</td>
                    </tr>
                )}
            </tbody>
        </table>;

    return (
        <div>
            <h1 id="tableLabel">Weather forecast</h1>
            <p>This component demonstrates fetching data from the server.</p>
            {contents}
        </div>
    );
};
Enter fullscreen mode Exit fullscreen mode

If we try to navigate to our react app at http://localhost:3000 we should be redirected to the login screen of the identity provider:

We can log in with the admin user and we should be redirect to the the react application. Notice the the bearer token has been added to the weatherforecast request and the API returns the list of forecasts.

If we click on the Profile button we should be redirected to the Keycloack user profile page.

I we click on the Logout button we will be redirected to the login page and all the tokens will be removed from the localstorage.

Conclusion

With this integration, the React frontend is now fully connected to both the identity provider and the .NET backend API. Specifically:

  • Users can authenticate via Keycloak
  • Tokens are managed on the client, including storage and refresh
  • Access to protected React routes requires authentication
  • API requests include Bearer tokens, which are validated by the .NET API

Overall, this creates a complete end-to-end architecture: Keycloak acts as the identity provider, .NET serves as the protected resource server, and React functions as the authenticated client application.

Top comments (0)