CI / build (pull_request) Successful in 1m10s
CI / lint (pull_request) Successful in 1m27s
CI / unit (pull_request) Successful in 1m24s
CI / frontend (pull_request) Successful in 3m27s
CI / mutation (pull_request) Successful in 6m29s
CI / verify-stack (pull_request) Successful in 9m41s
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>
68 lines
2.9 KiB
Markdown
68 lines
2.9 KiB
Markdown
# 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`)
|
|
|
|
```bash
|
|
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](../synthetic-data.md).
|
|
|
|
## Notes
|
|
|
|
- **Admin console:** <http://localhost:8180/> — `admin` / `admin` (dev only).
|
|
- **Client `big-portal`** is public with `standardFlowEnabled` (browser redirect login)
|
|
*and* `directAccessGrantsEnabled` (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); `medewerker` roles come through `realm_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 `#otp` prompt after the password) and direct grant (a `totp`
|
|
form field) alike.
|
|
- `CONFIGURE_TOTP` is 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](../architecture/adr-0031-mfa-on-the-medewerker-realm.md).
|
|
|
|
### Getting a code
|
|
|
|
```bash
|
|
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.
|