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
105 lines
3.1 KiB
JSON
105 lines
3.1 KiB
JSON
{
|
|
"realm": "medewerker",
|
|
"enabled": true,
|
|
"displayName": "Medewerkers",
|
|
"requiredActions": [
|
|
{
|
|
"alias": "CONFIGURE_TOTP",
|
|
"name": "Configure OTP",
|
|
"providerId": "CONFIGURE_TOTP",
|
|
"enabled": true,
|
|
"defaultAction": true,
|
|
"priority": 10
|
|
}
|
|
],
|
|
"roles": {
|
|
"realm": [
|
|
{ "name": "behandelaar", "description": "Behandelt registratieaanvragen" },
|
|
{ "name": "teamlead", "description": "Teamleider behandeling" },
|
|
{ "name": "beheerder", "description": "Beheert catalogus en default-fill (beheer-portal, S-15)" }
|
|
]
|
|
},
|
|
"clients": [
|
|
{
|
|
"clientId": "big-portal",
|
|
"enabled": true,
|
|
"publicClient": true,
|
|
"standardFlowEnabled": true,
|
|
"directAccessGrantsEnabled": true,
|
|
"redirectUris": ["*"],
|
|
"webOrigins": ["*"],
|
|
"protocolMappers": [
|
|
{
|
|
"name": "realm roles",
|
|
"protocol": "openid-connect",
|
|
"protocolMapper": "oidc-usermodel-realm-role-mapper",
|
|
"config": {
|
|
"multivalued": "true",
|
|
"claim.name": "realm_access.roles",
|
|
"jsonType.label": "String",
|
|
"id.token.claim": "true",
|
|
"access.token.claim": "true",
|
|
"userinfo.token.claim": "true"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"users": [
|
|
{
|
|
"username": "merel-behandelaar",
|
|
"enabled": true,
|
|
"firstName": "Merel",
|
|
"lastName": "Behandelaar",
|
|
"email": "merel@big.example.nl",
|
|
"emailVerified": true,
|
|
"credentials": [
|
|
{ "type": "password", "value": "test123", "temporary": false },
|
|
{
|
|
"type": "otp",
|
|
"userLabel": "seeded TOTP (fixture)",
|
|
"secretData": "{\"value\":\"BIGMEDEWERKEROTPSEED\"}",
|
|
"credentialData": "{\"subType\":\"totp\",\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\"}"
|
|
}
|
|
],
|
|
"realmRoles": ["behandelaar"]
|
|
},
|
|
{
|
|
"username": "tom-teamlead",
|
|
"enabled": true,
|
|
"firstName": "Tom",
|
|
"lastName": "Teamlead",
|
|
"email": "tom@big.example.nl",
|
|
"emailVerified": true,
|
|
"credentials": [
|
|
{ "type": "password", "value": "test123", "temporary": false },
|
|
{
|
|
"type": "otp",
|
|
"userLabel": "seeded TOTP (fixture)",
|
|
"secretData": "{\"value\":\"BIGMEDEWERKEROTPSEED\"}",
|
|
"credentialData": "{\"subType\":\"totp\",\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\"}"
|
|
}
|
|
],
|
|
"realmRoles": ["behandelaar", "teamlead"]
|
|
},
|
|
{
|
|
"username": "bram-beheerder",
|
|
"enabled": true,
|
|
"firstName": "Bram",
|
|
"lastName": "Beheerder",
|
|
"email": "bram@big.example.nl",
|
|
"emailVerified": true,
|
|
"credentials": [
|
|
{ "type": "password", "value": "test123", "temporary": false },
|
|
{
|
|
"type": "otp",
|
|
"userLabel": "seeded TOTP (fixture)",
|
|
"secretData": "{\"value\":\"BIGMEDEWERKEROTPSEED\"}",
|
|
"credentialData": "{\"subType\":\"totp\",\"digits\":6,\"counter\":0,\"period\":30,\"algorithm\":\"HmacSHA1\"}"
|
|
}
|
|
],
|
|
"realmRoles": ["beheerder"]
|
|
}
|
|
]
|
|
}
|