SSO Integrationintermediate13 min readApril 19, 2026

React SPA SSO with Keycloak using OIDC + PKCE

Integrate Keycloak SSO into a React single-page application using Authorization Code Flow with PKCE. Covers library setup, token storage, silent refresh, and protected routes.

KT

KeycloakPro Team

KeycloakPro Team

Introduction

Browser-based single-page applications (SPAs) cannot safely store client secrets. The correct OIDC flow for a React app is Authorization Code Flow with PKCE (Proof Key for Code Exchange) — a public client with no shared secret, where the browser proves it initiated the request using a cryptographic code challenge.

This guide uses oidc-client-ts and react-oidc-context, the actively maintained successor to the older oidc-client library. The pattern works with any React app (Vite, CRA, Next.js SPA mode).

Prerequisites

  • Keycloak or KeycloakPro 24+ with HTTPS
  • React 18+ project (npm/yarn/pnpm)
  • Keycloak admin access

Step 1 — Create the Keycloak public client

In the Keycloak admin console, navigate to your realm and create a new client:

  1. Clients → Create client
  2. Client type: OpenID Connect
  3. Client ID: react-app
  4. Click Next

Capability config — this is where public clients differ:

  • Client authentication: OFF (this makes it a public client — no secret)
  • Standard flow: ON
  • Direct access grants: OFF
  • Click Next

Login settings:

  • Root URL: http://localhost:3000 (dev) + https://app.example.com (prod)
  • Valid redirect URIs: http://localhost:3000/* and https://app.example.com/*
  • Valid post logout redirect URIs: http://localhost:3000/ and https://app.example.com/
  • Web origins: http://localhost:3000 and https://app.example.com (controls CORS)
Keycloak admin console: new public client react-app with Client authentication turned OFF, confirming it is a public client
Public client — no client secret, PKCE enforced automatically by Keycloak 21+

1.1 — Verify PKCE is enforced

Go to Clients → react-app → Advanced → scroll to Proof Key for Code Exchange Code Challenge Method.

Set it to S256. This rejects any authorization request that does not include a proper PKCE code challenge.

Keycloak admin console: Advanced tab for react-app client showing Proof Key for Code Exchange Code Challenge Method set to S256
S256 PKCE enforcement — requests without a code challenge are rejected

Step 2 — Install the OIDC libraries

npm install oidc-client-ts react-oidc-context

Step 3 — Wrap your app with AuthProvider

Create an OIDC configuration object and wrap your root component:

// src/main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { AuthProvider } from "react-oidc-context";
import App from "./App";

const oidcConfig = {
  authority: "https://keycloak.example.com/realms/YOUR_REALM",
  client_id: "react-app",
  redirect_uri: window.location.origin + "/",
  post_logout_redirect_uri: window.location.origin + "/",
  scope: "openid email profile",
  // Store tokens in memory — safest option for SPAs
  userStore: new WebStorageStateStore({ store: window.sessionStorage }),
};

ReactDOM.createRoot(document.getElementById("root")!).render(
  <React.StrictMode>
    <AuthProvider {...oidcConfig}>
      <App />
    </AuthProvider>
  </React.StrictMode>
);

Replace YOUR_REALM with your actual realm name.

Step 4 — Access auth state and protect routes

// src/App.tsx
import { useAuth } from "react-oidc-context";

export default function App() {
  const auth = useAuth();

  if (auth.isLoading) {
    return <div>Loading…</div>;
  }

  if (auth.error) {
    return <div>Error: {auth.error.message}</div>;
  }

  if (!auth.isAuthenticated) {
    return (
      <div>
        <h1>Welcome</h1>
        <button onClick={() => auth.signinRedirect()}>Log in with Keycloak</button>
      </div>
    );
  }

  return (
    <div>
      <h1>Hello, {auth.user?.profile.name}</h1>
      <p>Email: {auth.user?.profile.email}</p>
      <button onClick={() => auth.signoutRedirect()}>Log out</button>
      <ProtectedContent token={auth.user?.access_token} />
    </div>
  );
}

4.1 — Using the access token in API calls

async function fetchProtectedData(accessToken: string) {
  const response = await fetch("https://api.example.com/data", {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
  return response.json();
}

Your API should validate the token against Keycloak's JWKS endpoint.

React application pre-login state showing a Welcome heading and a Log in with Keycloak button
Pre-login state — the app shows a login button when the user is unauthenticated

Step 5 — The OIDC login flow in action

Clicking Log in with Keycloak triggers auth.signinRedirect(), which:

  1. Generates a random code_verifier and derives a code_challenge using SHA-256
  2. Stores the code_verifier in session storage
  3. Redirects the browser to Keycloak with response_type=code&code_challenge=...&code_challenge_method=S256

Keycloak shows its login page:

Keycloak login page displayed in the browser during the React app OIDC authorization flow
Keycloak handles authentication — your React app never sees the user's password

After login, Keycloak redirects back with ?code=.... The library exchanges the code for tokens using the stored code_verifier — Keycloak validates the PKCE challenge and issues an access token and ID token.

React application post-login showing user name, email from profile claims, and token expiry timestamp
Post-login — user profile claims from the ID token are available via auth.user.profile
Browser DevTools Network tab showing the POST request to Keycloak token endpoint with code and code_verifier parameters
DevTools view of the PKCE code exchange — the code_verifier proves the original redirect

Step 6 — Token refresh

oidc-client-ts handles silent token refresh automatically when you configure automaticSilentRenew: true. Prefer refresh token rotation over iframe-based silent renew (iframe renew is blocked by third-party cookie restrictions in modern browsers):

const oidcConfig = {
  // ...
  automaticSilentRenew: true,
  // Prefer refresh token rotation (Keycloak default from v18+)
  // Do NOT set silent_redirect_uri — that enables iframe renew
};

In Keycloak: Realm Settings → Sessions → enable Refresh Token Rotation.

Step 7 — Troubleshooting common issues

CORS preflight failures

Check Web origins in the Keycloak client settings. Add every origin your app runs on. Do not use * in production.

nonce mismatch

The library stores the nonce in session storage before the redirect. If session storage is cleared between the redirect and the callback (e.g., by a browser extension or sessionStorage.clear() in your code), the validation fails.

React strict mode double-invocation

React 18 Strict Mode mounts components twice in development. If you call auth.signinRedirect() in a useEffect, add a hasFired ref guard to prevent a double redirect.

Logout does not clear Keycloak session

auth.signoutRedirect() redirects to Keycloak's end-session endpoint. If the Keycloak session persists, check that post_logout_redirect_uri is in the Valid post logout redirect URIs list in the Keycloak client.

Step 8 — Production checklist

  • HTTPS enforced — Keycloak rejects non-HTTPS redirect URIs in production
  • PKCE Code Challenge Method set to S256 in Keycloak Advanced tab
  • sessionStorage used for token storage (not localStorage)
  • automaticSilentRenew: true — tokens refresh before expiry
  • Refresh token rotation enabled in Keycloak realm settings
  • Web origins restricted to your production domain(s)
  • Direct access grants disabled on the Keycloak client
  • API backend validates access tokens via Keycloak introspection or JWKS

Need help integrating React with Keycloak?

We deliver production-ready React + Keycloak integrations in 1–3 weeks.

Fixed-price, zero vendor lock-in, full source code ownership.

See integration packages