Runbook gains an MFA section and how to get a code; synthetic-data lists the fixture TOTP secret and the extra grant parameter; demo-script gains the S-15c note and its staff logins now mention the second factor. check_realms.py grows an 'otp' argument that prints a current code for a manual demo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2.9 KiB
Keycloak runbook
Keycloak (infra/keycloak/docker-compose.yml) runs in dev mode with four realms
imported at boot from infra/keycloak/realms/: digid, eherkenning, eidas,
medewerker. It mocks the Dutch identity brokers so portals can do real OIDC logins
locally. Host port :8180.
Quick test (make)
make keycloak-up # start Keycloak + import realms (~30-60s first boot)
make keycloak-smoke # start + verify every realm logs in and returns its claim
make keycloak-down # stop + wipe
make keycloak-smoke runs infra/keycloak/check_realms.py, which does a password-grant
login per realm and asserts the identifying claim:
| Realm | User | Claim asserted |
|---|---|---|
| digid | jan-burger | bsn |
| eherkenning | acme-ondernemer | kvk |
| eidas | pierre-dupont | eidas_id |
| medewerker | merel-behandelaar | role behandelaar |
The medewerker row also asserts that the password alone is refused — that realm enforces MFA (below).
All test users / credentials are in ../synthetic-data.md.
Notes
- Admin console: http://localhost:8180/ —
admin/admin(dev only). - Client
big-portalis public withstandardFlowEnabled(browser redirect login) anddirectAccessGrantsEnabled(password grant, used by the smoke test). - Dev store: in-memory H2 via
start-dev; realms re-import on each boot, so changes made in the admin UI don't persist. Edit the realm JSONs to make durable changes. - Image pinned to
quay.io/keycloak/keycloak:26.1. - Claims are injected by OIDC protocol mappers on
big-portal(user attribute → token claim);medewerkerroles come throughrealm_access.roles.
MFA on the medewerker realm (S-15c)
Staff logins (behandel + beheer portals) need a second factor; citizen/company realms
(digid, eherkenning, eidas) do not. Two halves in medewerker-realm.json:
- Every seeded medewerker carries a TOTP credential with the fixture secret
BIGMEDEWERKEROTPSEED, so Keycloak's built-in conditional OTP step fires on every login — browser flow (an#otpprompt after the password) and direct grant (atotpform field) alike. CONFIGURE_TOTPis a default required action, so any medewerker added later must enrol an authenticator before the first login.
See ../architecture/adr-0031-mfa-on-the-medewerker-realm.md.
Getting a code
python3 infra/keycloak/check_realms.py otp # prints a valid 6-digit code right now
Or enrol a phone once: the secret in base32 is IJEUOTKFIRCVORKSJNCVET2UKBJUKRKE
(otpauth://totp/medewerker?secret=IJEUOTKFIRCVORKSJNCVET2UKBJUKRKE). The e2e computes its
own code in tests/e2e/medewerker-login.ts.
Fixture only. A shared, committed secret is a demo convenience, never a production posture — see the ADR's consequences.