SPA Frontend
Installing keycloak-js
Section titled “Installing keycloak-js”keycloak-js is the official browser-side adapter for Keycloak. It handles the Authorization Code + PKCE flow, token storage, silent refresh, and logout — all without you managing redirect URLs or token exchanges by hand.
Install it from npm:
npm install keycloak-jsThe package ships its own TypeScript types, so no separate @types package is needed.
Creating the Keycloak instance
Section titled “Creating the Keycloak instance”Pass your realm’s URL, your realm name, and your client ID. These three values are the minimum required to identify your application to Keycloak:
import Keycloak from 'keycloak-js';
const keycloak = new Keycloak({
url: 'http://localhost:8080',
realm: 'my-app',
clientId: 'my-app-frontend',
});
keycloak
.init({ onLoad: 'login-required', pkceMethod: 'S256' })
.then((authenticated) => {
if (authenticated) {
console.log('Logged in as', keycloak.tokenParsed?.preferred_username);
startApp();
}
});
function startApp() {
// Your app bootstrap goes here.
// keycloak.token is available from this point on.
}Key points:
onLoad: 'login-required'redirects the user to Keycloak immediately if they are not yet authenticated. Use'check-sso'if you want the page to be accessible without login and only show a login button.pkceMethod: 'S256'enables PKCE (Proof Key for Code Exchange). This is mandatory for public clients (SPAs) because they cannot store a client secret securely. PKCE replaces the secret with a per-request code challenge.
Calling a protected API
Section titled “Calling a protected API”After init resolves with authenticated: true, keycloak.token holds the raw JWT string. Attach it as a Bearer token in every API request:
async function fetchProfile() {
await keycloak.updateToken(30); // refresh if expiring in < 30 seconds
const response = await fetch('https://api.example.com/profile', {
headers: {
Authorization: `Bearer ${keycloak.token}`,
},
});
return response.json();
}updateToken(minValidity) checks whether the current token will expire within minValidity seconds and, if so, uses the refresh token to silently obtain a new one. Always call it before attaching keycloak.token to a request so you never send an expired JWT.
Reading token claims
Section titled “Reading token claims”The decoded payload is available in keycloak.tokenParsed. Common claims you will use:
keycloak.tokenParsed?.preferred_username // usernamekeycloak.tokenParsed?.email // email addresskeycloak.tokenParsed?.realm_access?.roles // array of realm roleskeycloak.tokenParsed?.resource_access?.['my-app-frontend']?.roles // client rolesTrade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Keep tokens in memory | Safer — other JavaScript, including a script injected via XSS, has a much harder time reaching it, and the token disappears when the tab closes | The token is lost on every page refresh; you must trigger a new silent SSO check via an iframe or redirect |
| Keep tokens in localStorage | The user stays logged in even after a page refresh, and it’s simpler because you don’t have to manage silent refresh yourself | Opens the door to token theft via XSS, because any script running on the page can read localStorage |
| Use keycloak-js (the official adapter) | Handles the PKCE flow, silent refresh, and token storage automatically, reducing the chance of writing security code incorrectly yourself | Ties you directly to Keycloak — migrating to a different identity provider means writing a new integration |
Common mistakes
Section titled “Common mistakes”- Storing tokens in localStorage — the single most common SPA mistake, because localStorage is reachable from every line of JavaScript running on the page. If any third-party script or compromised dependency has an XSS vulnerability, the attacker gets the token immediately. Let keycloak-js keep tokens in memory by default instead.
- Trusting client-side “logged in” state without verifying at the backend — checking that
keycloak.authenticatedis true on the frontend only tells the UI what to show; it is not proof the request is valid. The backend must verify the JWT signature on every endpoint. - Not calling
updateTokenbefore every API call — storing the token in a variable and reusing it without checking whether it’s close to expiring causes requests to fail mid-flight with a 401, when a silent refresh ahead of time would have avoided it.
Real-world example
The official keycloak-js pattern — Keycloak itself recommends keeping the token only in memory (a JavaScript variable), never localStorage or sessionStorage. On page refresh, the adapter does a silent SSO check through a hidden iframe to get a new token without the user noticing.
The Auth0 SPA SDK — uses the same approach: in-memory storage plus refresh-token rotation instead of localStorage, which has become the standard best practice for browser-based OIDC client libraries across the industry.