Backend API
What a resource server must do
Section titled “What a resource server must do”When a client sends a request with an Authorization: Bearer <token> header, your backend must:
- 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. - Verify the signature — confirm the JWT was signed by Keycloak, not forged by an attacker.
- Verify the issuer (
iss) — the token’sissclaim must equal<keycloak-url>/realms/<realm>. A token from a different Keycloak realm or a different server should be rejected. - Verify the audience (
aud) — theaudclaim should include your client ID, ensuring the token was issued for your API. - 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.
Node/Express example with jose
Section titled “Node/Express example with jose”The jose library is a modern, dependency-free JWT library that supports JWKS fetching and all standard verification checks. Install it:
npm install joseimport { 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'));What the code does
Section titled “What the code does”createRemoteJWKSetbuilds a cached key set from Keycloak’s JWKS URL. When Keycloak rotates keys,joseautomatically fetches the new keys.jwtVerifyverifies the signature, checksissandaud, and rejects expired tokens. If any check fails, it throws — thecatchblock returns401.- After successful verification,
payloadcontains all decoded JWT claims (sub, preferred_username, realm_access, etc.) and you can use them for authorisation logic.
Checking roles from the token
Section titled “Checking roles from the token”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);}