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.
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:
- Clients → Create client
- Client type:
OpenID Connect - Client ID:
react-app - 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/*andhttps://app.example.com/* - Valid post logout redirect URIs:
http://localhost:3000/andhttps://app.example.com/ - Web origins:
http://localhost:3000andhttps://app.example.com(controls CORS)

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.

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.

Step 5 — The OIDC login flow in action
Clicking Log in with Keycloak triggers auth.signinRedirect(), which:
- Generates a random
code_verifierand derives acode_challengeusing SHA-256 - Stores the
code_verifierin session storage - Redirects the browser to Keycloak with
response_type=code&code_challenge=...&code_challenge_method=S256
Keycloak shows its login page:

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.


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
S256in Keycloak Advanced tab -
sessionStorageused for token storage (notlocalStorage) -
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.