JWT Claims¶
RONL Business API validates every request against a JWT access token issued by Keycloak. The token contains standard OIDC claims plus custom claims injected via Keycloak protocol mappers.
Full token example¶
{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-123"
},
"payload": {
"exp": 1740492000,
"iat": 1740491100,
"iss": "https://keycloak.open-regels.nl/realms/ronl",
"aud": "ronl-business-api",
"sub": "user-uuid-abc-123",
"typ": "Bearer",
"azp": "ronl-business-api",
"preferred_username": "test-citizen-utrecht",
"email_verified": false,
"municipality": "utrecht",
"organisation_type": "municipality",
"realm_access": { "roles": ["citizen"] },
"loa": "hoog"
}
}
Standard OIDC claims¶
| Claim | Type | Description |
|---|---|---|
iss |
string | Token issuer โ Keycloak realm URL |
aud |
string | Intended audience โ must be ronl-business-api |
sub |
string | Subject โ unique user UUID, used as userId in audit logs |
exp |
number | Expiry โ Unix timestamp; token lifetime is 15 minutes |
iat |
number | Issued at โ Unix timestamp |
preferred_username |
string | Human-readable username |
typ |
string | Always Bearer |
Custom RONL claims¶
These claims are added by Keycloak protocol mappers configured on the ronl-business-api client in the realm export (config/keycloak/ronl-realm.json):
| Claim | Type | Mapper | Source | Description |
|---|---|---|---|---|
municipality |
string | User Attribute | municipality |
Tenant identifier โ the organisation the caller belongs to (utrecht, flevoland, toeslagen, โฆ) |
organisation_type |
string | User Attribute | organisation_type |
Organisation category: municipality, province, national, or commercial |
realm_access.roles |
string[] | User Realm Role | realm roles | The caller's realm roles; the backend reads its roles from here |
loa |
string | User Attribute | assurance_level |
Level of assurance: basis, midden, substantieel, or hoog |
mandate |
string | User Attribute | mandate |
Representation authority (optional). Passed through to req.user; no check reads it |
employeeId |
string | User Attribute | employee_id |
Present on caseworker accounts onboarded via HrOnboardingProcess; absent for citizens and non-onboarded caseworkers |
given_name, family_name, email |
string | User Property | firstName, lastName, email |
Used for signer details; when the name claims are missing, the name is split from name or preferred_username |
The client maps no bsn claim. The frontend reads a bsn claim first when it looks up a citizen's service number โ the field DigiD fills โ and falls back to a fixed mapping for the test usernames.
How claims are used by the backend¶
After successful JWT validation in jwt.middleware.ts, the claims are mapped onto req.user:
req.user field |
From claim | Used for |
|---|---|---|
userId |
sub |
Audit log; applicantId and initiator on a started process |
tenantId |
municipality |
The MISSING_TENANT presence check and every tenant decision |
organisationType |
organisation_type |
Propagated to process variables |
roles |
realm_access.roles |
Role checks, the citizen/staff distinction at process start, task candidate groups |
assuranceLevel |
loa |
Assurance-level checks (basis for decision evaluation, midden for a process start) |
mandate |
mandate |
Carried only |
employeeId |
employeeId |
HR onboarding profile lookup |
The tenant middleware only checks that tenantId is present, answering 403 MISSING_TENANT when it is not. Which organisation may reach a process instance or task is decided separately, in auth/tenant-access.ts, from the instance's municipality variable โ see Authentication & IAM โ Tenancy.
Inspecting a token in the browser¶
// In browser DevTools console after login:
const token = /* keycloak.token */;
JSON.parse(atob(token.split('.')[1]));
Or paste the token at jwt.io for a formatted view.
Token lifetime¶
| Setting | Value | Keycloak config key |
|---|---|---|
| Access token | 15 minutes | accessTokenLifespan: 900 |
| SSO session idle | 30 minutes | ssoSessionIdleTimeout: 1800 |
| SSO session max | 10 hours | ssoSessionMaxLifespan: 36000 |
The Keycloak JS adapter in the frontend automatically refreshes the access token before it expires, as long as the SSO session is still valid.