Skip to content

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.

For an employee who signs in through Entra ID (the entra-flevoland provider), the same client mappers produce the same claims. The values behind them come from identity-provider mappers instead of hand-set user attributes: municipality is flevoland, organisation_type is province, loa is substantieel, and realm_access.roles includes the roles mapped from the Entra app roles. sub is the Keycloak user's own id, not Entra's. See Entra ID.


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.