In a recent greenfield project, I inherited a Keycloak instance already deployed and configured for the back-end services. My next challenge was to research and apply best practices for securely integrating that same Keycloak realm into the React front-end. The goal was to enable seamless, PKCE-based single-page-app authentication, preserve silent SSO, and ensure the .NET API accepts tokens issued to the front-end—without exposing secrets in the browser.
Why Two Clients
I already have a confidential Keycloak client (“api-service”) protecting your .NET API. To integrate my React front end securely:
Create a second, public client (spa-app) in the same realm, using Authorization Code Flow + PKCE.
Add a Client Scope + Audience Mapper so tokens for spa-app also include the api-service audience claim.
Wrap your React app in a single ReactKeycloakProvider, avoiding React 18 StrictMode’s double-init() issue.
Provide minimal login, logout, and user-info examples using @react-keycloak/web.
Include a tiny silent-check-sso.html for background SSO checks.
This pattern keeps your secrets on the server, enforces least-privilege via audiences, and delivers seamless silent SSO for your SPA.
Create the Public SPA Client in Keycloak
Clients → Create
- Client ID: spa-app
-
Protocol: openid-connect
Settings
- Access Type: Public (no client secret)
- Standard Flow Enabled: ON (Authorization Code Flow)
- Implicit Flow Enabled: OFF
- Direct Access Grants Enabled: OFF
Advanced
Proof Key for Code Exchange Code Challenge Method: S256 (enables PKCE)
Login Settings
Root URL / Home URL:
http://localhost:3000
https://<your-app>.azurestaticapps.net
Valid Redirect URIs:
http://localhost:3000/*
https://<your-app>.azurestaticapps.net/*
Web Origins:
http://localhost:3000
https://<your-app>.azurestaticapps.net
Save.
Share the API Audience via Client Scope
By default, tokens for spa-app carry only "aud": ["spa-app"], but your API expects "aud": "api-service". Add it without code changes:
Client Scopes → Create
Name: api-audience
Protocol: openid-connect
Include in token scope: OFF
api-audience → Mappers → Create
Name: api-audience-mapper
Mapper Type: Audience
Included Client Audience: api-service
Token Claim Name: aud
Add to access token: on
Add to ID token: off
Clients → spa-app → Client Scopes → Default:
add api-audience.
React Integration: Provider + Silent SSO
Install the Keycloak Adapter
First, install the official Keycloak JavaScript adapter and React bindings, which provide the low-level keycloak-js client and a convenient wrapper:
npm install keycloak-js @react-keycloak/web
Configure keycloak.ts
Create a dedicated module to initialize a single Keycloak instance (avoiding double-init errors) and export your initOptions for Authorization Code Flow + PKCE with silent SSO:
import Keycloak from 'keycloak-js';
// Instantiate Keycloak once, pointing at your realm and SPA client
export const keycloak = new Keycloak({
url: import.meta.env.VITE_KEYCLOAK_URL,
realm: import.meta.env.VITE_KEYCLOAK_REALM,
clientId: 'spa-app',
});
export const initOptions = {
onLoad: 'check-sso', // perform a silent session
flow: 'standard', // use the OIDC Authorization Code Flow
pkceMethod: 'S256', // enforce PKCE for extra SPA security
silentCheckSsoRedirectUri: `${window.location.origin}/silent-check-sso.html`,
checkLoginIframe: true, // enable the hidden iframe for silent token refresh
checkLoginIframeInterval: 30, // polling interval in seconds
enableLogging: true, // turn on adapter debug logging
};
Create the Silent-SSO Callback Page
Keycloak’s JavaScript adapter will load this page inside a hidden iframe to perform a background session check. When it finishes, it uses postMessage to notify the parent window, resolving the silent SSO promise. Place this file at public/silent-check-sso.html:
<!doctype html>
<html>
<body>
<script>
// Notify the parent window with the full URL (including tokens in the fragment)
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
This minimal snippet is exactly what the official Keycloak docs recommend for the silent-SSO iframe callback.
Provider Setup (Avoid React 18 Double-Init)
React 18’s StrictMode mounts and unmounts class-based components twice in development, which would trigger keycloak.init() two times and break with “A ‘Keycloak’ instance can only be initialized once.” To prevent this, render your outside of :
import ReactDOM from 'react-dom/client';
import { ReactKeycloakProvider } from '@react-keycloak/web';
import App from './App';
import { keycloak, initOptions } from './keycloak';
ReactDOM.createRoot(document.getElementById('root')!).render(
// Note: no <React.StrictMode> wrapper here
<ReactKeycloakProvider authClient={keycloak} initOptions={initOptions}>
<App />
</ReactKeycloakProvider>
);
By removing the StrictMode wrapper around the provider, you ensure keycloak.init() is called exactly once.
Simple Login / Logout / User-Info Example
Use the useKeycloak hook inside any component to trigger login, logout, and display basic user info:
import React from 'react';
import { useKeycloak } from '@react-keycloak/web';
export default function UserMenu() {
const { keycloak, initialized } = useKeycloak();
// Wait until Keycloak finishes initialization (including silent SSO)
if (!initialized) {
return <div>Loading authentication…</div>;
}
// If not authenticated yet, show a Login button
if (!keycloak.authenticated) {
return <button onClick={() => keycloak.login()}>Login</button>;
}
// Once authenticated, extract user info from the parsed token
const username = keycloak.tokenParsed?.preferred_username;
const roles = keycloak.tokenParsed?.realm_access?.roles || [];
return (
<div>
<span>Hello, {username}</span>
<span>Roles: {roles.join(', ')}</span>
<button onClick={() => keycloak.logout()}>Logout</button>
</div>
);
}
Summary
With this configuration your React SPA and .NET API remain securely isolated yet seamlessly integrated under the same Keycloak realm by using a public client for the front end and a confidential client for the back end, leveraging PKCE and silent SSO for a friction-free user experience, and sharing the API’s audience through a simple client scope mapper so no server code changes are required, all while avoiding double initialization in React 18 by placing the Keycloak provider outside StrictMode and keeping your secrets safely on the server.
Top comments (0)