Keycloak Timeline Setup - Configuration Reference¶
Overview¶
The Timeline Navigation feature requires the preferred_username field in JWT tokens to map test users to their BSN (Burgerservicenummer). This document explains how to configure Keycloak to include this field.
Why This Is Needed¶
The Problem¶
- Timeline needs to fetch BRP data using a BSN
- In production, BSN comes from DigiD via SAML assertion
- In test/ACC, BSN is mapped from Keycloak username
- JWT token doesn't include username by default
The Solution¶
Add a Keycloak Protocol Mapper that includes username in the JWT token as preferred_username.
Configuration Steps¶
1. Access Keycloak Admin Console¶
URL: http://localhost:8080 (local)
https://acc.keycloak.open-regels.nl (ACC)
https://keycloak.open-regels.nl (PROD)
Login: admin / <admin-password>
2. Navigate to Client Scopes¶
- Select Realm:
ronl(top-left dropdown) - In left menu, click Clients
- Find and click
ronl-business-api - Click Client scopes tab
- Click on
ronl-business-api-dedicated(in the table)
3. Add Protocol Mapper¶
- Click Mappers tab
- Click Add mapper button
- Select By configuration
- Select User Property
4. Configure Mapper¶
Fill in the form:
| Field | Value |
|---|---|
| Name | username |
| Mapper Type | User Property |
| Property | username |
| Token Claim Name | preferred_username |
| Claim JSON Type | String |
| Add to ID token | ☑️ ON |
| Add to access token | ☑️ ON |
| Add to userinfo | ☑️ ON |
| Multivalued | ☐ OFF |
- Click Save
Verification¶
Test the Configuration¶
- Login to MijnOmgeving with a test user
- Open browser DevTools (F12)
- Go to Console tab
- Look for the log:
🔍 Full token as JSON: - Verify the token contains:
{
"preferred_username": "test-citizen-utrecht",
"municipality": "utrecht",
"sub": "uuid-here",
"roles": ["citizen"],
"loa": "hoog"
}
If It's Not Working¶
Token missing preferred_username:
- Clear browser cache and cookies
- Logout and login again (force new token)
- Verify mapper is saved correctly
- Check mapper is in
ronl-business-api-dedicatedscope (not a different scope)
Still not working:
- Restart Keycloak:
docker compose restart keycloak - Check Keycloak logs:
docker compose logs keycloak
Production Considerations¶
DigiD Integration¶
In production with real DigiD, you'll also need:
- BSN Mapper — Maps SAML attribute to
bsnclaim - Municipality Mapper — Maps organization to
municipalityclaim - LoA Mapper — Maps DigiD assurance level to
loaclaim
See Authentication & IAM for full DigiD setup.
Security Notes¶
- ✅
preferred_usernameis safe to include (not sensitive data) - ✅ BSN should NEVER be in logs (masked as
999-99-2235) - ✅ Token lifetime should be 15 minutes maximum
- ✅ Tokens must be validated on every request
Troubleshooting¶
Mapper Not Appearing in Token¶
Check:
- Mapper is added to correct client scope (
ronl-business-api-dedicated) - Client scope is assigned to client (
ronl-business-api) - "Add to access token" is enabled
- User has logged out and back in (to get new token)
Wrong Value in Token¶
If preferred_username shows UUID instead of username:
- Check Property field is set to
username(notid) - Verify user actually has a username set in Keycloak
If preferred_username is null:
- User might not have username set
- Check user attributes in Keycloak Users section
Multiple Mappers Conflict¶
If you have multiple mappers for preferred_username:
- Delete all except one
- Logout/login to refresh token
- Verify only one
preferred_usernameclaim in token
Related Configuration¶
Other Timeline-Related Settings¶
Beyond this mapper, ensure these are also configured:
- Shared Package Types —
KeycloakUserinterface includespreferred_username - Backend tsconfig — Removed
composite: true(see Technical Architecture) - BSN Mapping Service — Maps usernames to BSN (see Developer Guide)
Test Users Setup¶
Verify test users have correct attributes:
{
"username": "test-citizen-utrecht",
"attributes": {
"municipality": ["utrecht"],
"assurance_level": ["hoog"]
},
"realmRoles": ["citizen"]
}
Import test users from: config/keycloak/ronl-realm.json
Configuration Checklist¶
Use this checklist when setting up timeline in a new environment:
- Keycloak realm
ronlexists - Client
ronl-business-apiexists - Client scope
ronl-business-api-dedicatedexists - Protocol mapper
username→preferred_usernamecreated - Mapper enabled for access token, ID token, userinfo
- Test user has username attribute set
- Token verified to contain
preferred_username - Timeline feature tested and working
- Documentation updated with environment-specific URLs
Realm Export/Import¶
Exporting Realm with Mapper¶
To export realm configuration including the mapper:
# Inside Keycloak container
/opt/keycloak/bin/kc.sh export \
--dir /tmp/export \
--realm ronl \
--users realm_file
# Copy from container
docker cp keycloak:/tmp/export/ronl-realm.json ./config/keycloak/
Importing Realm¶
# From project root
docker compose exec keycloak \
/opt/keycloak/bin/kc.sh import \
--file /opt/keycloak/data/import/ronl-realm.json
Note: Manual mapper configuration is recommended over import to avoid UUID mismatches.
API Impact¶
Before Configuration¶
JWT Token:
Timeline Behavior:
- Cannot map user to BSN
- Timeline fails to load
- Error: "No BSN found for user"
After Configuration¶
JWT Token:
{
"sub": "uuid-abc-123",
"preferred_username": "test-citizen-utrecht",
"municipality": "utrecht",
"roles": ["citizen"]
}
Timeline Behavior:
- ✅ Maps
test-citizen-utrecht→ BSN999992235 - ✅ Fetches person data from BRP API
- ✅ Timeline loads successfully