DEV Community

Cover image for React Keycloak Integration: Secure Auth for Existing Backend
Salva Torrubia
Salva Torrubia

Posted on

React Keycloak Integration: Secure Auth for Existing Backend

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 Image description

Settings

  • Access Type: Public (no client secret)
  • Standard Flow Enabled: ON (Authorization Code Flow)
  • Implicit Flow Enabled: OFF
  • Direct Access Grants Enabled: OFF

Image description

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
Enter fullscreen mode Exit fullscreen mode

Valid Redirect URIs:

http://localhost:3000/*
https://<your-app>.azurestaticapps.net/*
Enter fullscreen mode Exit fullscreen mode

Web Origins:

http://localhost:3000
https://<your-app>.azurestaticapps.net
Enter fullscreen mode Exit fullscreen mode

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

Image description

Name: api-audience

Protocol: openid-connect

Include in token scope: OFF

api-audience → Mappers → Create

Image description

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
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
);

Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

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)