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:
- Add React service to Docker Compose
- Configure the Keycloak client for React
- Configure the audience mapper
- Install dependencies and configure Keycloak in React
- Create the Keycloak provider and context
- Add a simple token service
- Protect routes in React
- Add login, logout, and profile actions
- Testing the authentication and authorization
- 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
After this, your app should be reachable at:
- React: http://localhost:3000
- API: http://localhost:5080
- Keycloak: http://localhost:8080
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:
- Root URL: http://localhost:3000
- Home URL: http://localhost:3000
- Valid redirect URIs: http://localhost:3000/*
- Valid post logout redirect URIs: http://localhost:3000/*
- Web origins: http://localhost:3000
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"
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"],
...
}
Install dependencies and configure Keycloak in React
Inside the React project folder, install the required packages:
npm install keycloak-js react-router-dom
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;
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>
);
};
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");
},
};
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="/" />;
};
Then wrap routes with this component:
<Routes>
<Route path="/" element={<ProtectedRoute><Home /></ProtectedRoute>} />
</Routes>
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;
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;
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;
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>
);
}
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>
);
};
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)