Skip to content

SPA Frontend

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:

Terminal window
npm install keycloak-js

The package ships its own TypeScript types, so no separate @types package is needed.

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.

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.

The decoded payload is available in keycloak.tokenParsed. Common claims you will use:

keycloak.tokenParsed?.preferred_username // username
keycloak.tokenParsed?.email // email address
keycloak.tokenParsed?.realm_access?.roles // array of realm roles
keycloak.tokenParsed?.resource_access?.['my-app-frontend']?.roles // client roles
OptionBenefitCost
Keep tokens in memorySafer — other JavaScript, including a script injected via XSS, has a much harder time reaching it, and the token disappears when the tab closesThe token is lost on every page refresh; you must trigger a new silent SSO check via an iframe or redirect
Keep tokens in localStorageThe user stays logged in even after a page refresh, and it’s simpler because you don’t have to manage silent refresh yourselfOpens 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 yourselfTies you directly to Keycloak — migrating to a different identity provider means writing a new integration
  • 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.authenticated is 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 updateToken before 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.

What does the pkceMethod: "S256" option do when passed to keycloak.init()?
Why should you call keycloak.updateToken(30) before attaching the token to an API request?
Where does keycloak-js store the access token by default?
Which onLoad value redirects the user to Keycloak immediately if they are not authenticated?