Skip to content

Backend API

When a client sends a request with an Authorization: Bearer <token> header, your backend must:

  1. Fetch the realm’s JWKS — the set of public keys Keycloak uses to sign JWTs. The URL is <keycloak-url>/realms/<realm>/protocol/openid-connect/certs.
  2. Verify the signature — confirm the JWT was signed by Keycloak, not forged by an attacker.
  3. Verify the issuer (iss) — the token’s iss claim must equal <keycloak-url>/realms/<realm>. A token from a different Keycloak realm or a different server should be rejected.
  4. Verify the audience (aud) — the aud claim should include your client ID, ensuring the token was issued for your API.
  5. Verify expiry (exp) — reject tokens that have expired.

Failing any of these checks means the token is invalid and the request must be rejected with 401 Unauthorized.

The jose library is a modern, dependency-free JWT library that supports JWKS fetching and all standard verification checks. Install it:

Terminal window
npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose';
import express from 'express';

const KEYCLOAK_URL = 'http://localhost:8080';
const REALM = 'my-app';
const CLIENT_ID = 'my-app-backend';

// Cache the JWKS remote keyset — jose handles key rotation automatically.
const JWKS = createRemoteJWKSet(
  new URL(`${KEYCLOAK_URL}/realms/${REALM}/protocol/openid-connect/certs`)
);

async function requireAuth(req, res, next) {
  const authHeader = req.headers['authorization'] ?? '';
  const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;

  if (!token) {
    return res.status(401).json({ error: 'Missing bearer token' });
  }

  try {
    const { payload } = await jwtVerify(token, JWKS, {
      issuer: `${KEYCLOAK_URL}/realms/${REALM}`,
      audience: CLIENT_ID,
    });
    req.user = payload; // attach claims to the request
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

const app = express();

app.get('/profile', requireAuth, (req, res) => {
  res.json({
    username: req.user.preferred_username,
    email: req.user.email,
    roles: req.user.realm_access?.roles ?? [],
  });
});

app.listen(3001, () => console.log('API listening on :3001'));
  • createRemoteJWKSet builds a cached key set from Keycloak’s JWKS URL. When Keycloak rotates keys, jose automatically fetches the new keys.
  • jwtVerify verifies the signature, checks iss and aud, and rejects expired tokens. If any check fails, it throws — the catch block returns 401.
  • After successful verification, payload contains all decoded JWT claims (sub, preferred_username, realm_access, etc.) and you can use them for authorisation logic.

Keycloak encodes realm roles in realm_access.roles and client-specific roles in resource_access.<clientId>.roles. A simple role-check helper:

function hasRole(user: Record<string, unknown>, role: string): boolean {
const roles = (user.realm_access as { roles?: string[] })?.roles ?? [];
return roles.includes(role);
}
OptionBenefitCost
Verify the JWT on every request (stateless validation)No need to query a session store every time — scales easily because the backend holds no stateVerifying the cryptographic signature on every request adds a small CPU cost per request
Cache the JWKS in memory (e.g. jose’s createRemoteJWKSet)Fewer calls to Keycloak’s JWKS endpoint, lowering latencyIf Keycloak rotates a key suddenly before the cache expires, there may be a short window where verification fails until the library fetches the new key
Use a standard JWT-verification library (e.g. jose)A community-vetted library that fully supports JWKS, key rotation, and the OIDC standardStill requires writing your own middleware to wire it into your framework — it isn’t a Keycloak-specific adapter
  • Not checking the aud/iss claims on every request — checking only the signature isn’t enough, because a correctly signed token from a different realm or client also passes signature verification. Always check that iss matches your realm and aud matches your backend’s client ID.
  • Trusting an attached token without verifying its signature against the JWKS — decoding a JWT and reading its claims directly without verifying the signature lets an attacker forge any payload they want. Always use a library such as jose or jjwt to verify against the JWKS public key.
  • Not handling key rotation — hardcoding the public key in code instead of fetching it dynamically from the JWKS endpoint means that once Keycloak rotates its key, the backend fails to verify new tokens until you deploy new code.

Real-world example

Spring Security’s oauth2ResourceServer — a standard example of backend-side JWT-validation middleware: set just the issuer-uri and it fetches the JWKS, and checks signature, iss, aud, and exp automatically with no validation logic to write.

jose + createRemoteJWKSet — used in this lesson’s Node/Express example, the same pattern many backend teams use in production because it caches the JWKS automatically and supports key rotation without a service restart.

What does the iss (issuer) claim in a Keycloak JWT contain?
What happens if the aud (audience) claim does not match your backend client ID?
Why does createRemoteJWKSet from jose cache the JWKS?
Where does Keycloak publish its public signing keys (JWKS)?