Closes #132.
Staff logins (behandel + beheer portals) now need a second factor; the citizen realms are unchanged.
**How:** every seeded medewerker carries a TOTP credential, which activates Keycloak's stock *conditional OTP* step in both the browser flow and the direct grant — no custom browser-flow JSON in the export. `CONFIGURE_TOTP` is a default required action so a medewerker added later must enrol first. ADR-0031 records the choice and, explicitly, that the shared fixture secret is a demo posture only.
**Tests (red first, 30c5279):**
- `check_realms.py` asserts the medewerker password-only grant is **refused**, then that password + TOTP succeeds and still carries the `behandelaar` role. It failed with `[MFA NOT ENFORCED]` against the old export.
- The three medewerker e2e logins move to `loginMedewerker()` (`tests/e2e/medewerker-login.ts`), which submits Keycloak's OTP prompt. Both TOTP implementations (Python `hmac`, Node `crypto`) are ~6 lines of RFC 6238 — no new dependency.
Verified locally against Keycloak 26.1: password-only → `invalid_grant`, password + code → 200, and the browser flow's `#otp` prompt accepts a computed code and issues an auth code.
## Definition of Done
- [x] Failing test/verify committed first; implementation makes it pass.
- [x] Conventional Commits referencing the issue (`refs #132`).
- [ ] CI green (verify-stack compose smoke + relevant checks).
- [x] `docker compose up` reaches green health within 3 minutes (Keycloak change is import-time only).
- [x] Docs touched (runbook, synthetic-data, demo-script) + ADR-0031 + demo note.
- [x] Closed by the merging PR (`closes #132`).
🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #158
2.0 KiB
Synthetic data
All credentials here are dev-only synthetic test data — never real personal data, never used outside local development.
Keycloak realms (S-02)
Keycloak runs at http://localhost:8180 (admin console: admin / admin). Four realms
are imported at boot from infra/keycloak/realms/. Each has a public OIDC client
big-portal (standard flow + direct access grants enabled, redirect URIs * for dev).
All test users share the password test123.
| Realm | Mimics | User | Identifying claim |
|---|---|---|---|
digid |
DigiD (burgers) | jan-burger |
bsn = 123456782 |
digid |
DigiD (burgers) | sanne-burger |
bsn = 231477813 (S-26 resume e2e — its own user so it can leave an open registration) |
eherkenning |
eHerkenning (bedrijven) | acme-ondernemer |
kvk = 12345678 |
eidas |
eIDAS (EU) | pierre-dupont |
eidas_id = FR/NL/AB-1234-5678 |
medewerker |
Internal staff | merel-behandelaar |
role behandelaar |
medewerker |
Internal staff | tom-teamlead |
roles behandelaar, teamlead |
medewerker |
Internal staff | bram-beheerder |
role beheerder |
medewerker users additionally need a second factor: that realm enforces MFA (S-15c,
ADR-0031). All three share the fixture TOTP secret BIGMEDEWERKEROTPSEED; print a current
code with python3 infra/keycloak/check_realms.py otp.
The identifying claims are injected via OIDC protocol mappers on big-portal
(user-attribute → token claim); medewerker roles appear in realm_access.roles.
Get a token (for testing)
curl -s -X POST \
http://localhost:8180/realms/digid/protocol/openid-connect/token \
-d grant_type=password -d client_id=big-portal \
-d username=jan-burger -d password=test123 -d scope=openid | jq -r .access_token
For a medewerker user, add -d totp=$(python3 infra/keycloak/check_realms.py otp) —
without it the grant is refused with invalid_grant.
Decode the JWT payload to see the bsn claim. make keycloak-smoke checks every realm
automatically.