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);}Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| Verify the JWT on every request (stateless validation) | No need to query a session store every time — scales easily because the backend holds no state | Verifying 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 latency | If 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 standard | Still requires writing your own middleware to wire it into your framework — it isn’t a Keycloak-specific adapter |
Common mistakes
Section titled “Common mistakes”- Not checking the
aud/issclaims 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 thatissmatches your realm andaudmatches 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, andexpautomatically 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.