# ADR-0031 — MFA on the medewerker realm, with a fixture TOTP secret - **Status:** Accepted - **Date:** 2026-09-03 - **Slice:** S-15c (Gitea #132) ## Context Staff (behandelaar, teamlead, beheerder) act on citizens' registrations and on the ACL's default-fill: the highest-privilege logins in the platform. The medewerker realm protected them with a password alone, while the citizen realms (digid, eherkenning, eidas) mock brokers that carry their own assurance levels. A reference application that demonstrates a government architecture should show MFA on the staff realm. Two things had to be decided: **how** to enforce OTP in a realm export, and **how the automated checks and a human demo obtain a code** — the e2e drives a real browser login and `make keycloak-smoke` drives a real password grant, so neither can scan a QR. ## Decision **Enforce OTP by giving every seeded medewerker a TOTP credential**, rather than replacing Keycloak's browser flow with a copy whose OTP execution is `REQUIRED`. Keycloak's stock `browser` and `direct grant` flows both contain a *conditional OTP* subflow that fires when the user has an OTP credential. Seeding the credential therefore turns the challenge on for every seeded user, in both flows, without duplicating ~40 lines of flow JSON into the export. `CONFIGURE_TOTP` is additionally set as a **default required action**, so a medewerker created later must enrol before their first login. **The seeded secret is a fixed, committed fixture** (`BIGMEDEWERKEROTPSEED`) shared by all medewerkers. Codes are then computable: `infra/keycloak/check_realms.py` (Python, stdlib `hmac`) and `tests/e2e/medewerker-login.ts` (Node `crypto`) each implement RFC 6238 in about six lines — no OTP dependency on either side, and no enrolment step in the tests. ## Consequences - A password alone no longer yields a token on the medewerker realm; `check_realms.py` asserts that refusal, so the enforcement cannot silently regress. - Every medewerker login in the e2e goes through `loginMedewerker()`, which submits the OTP form. New staff specs must use it. - **The secret is public.** It is a demo fixture and worthless outside this synthetic stack, in the same class as the committed `test123` passwords and the mock DigiD broker. A real deployment enrols per-user authenticators (or federates to DigiD Machtigen / eHerkenning at the required assurance level) and seeds no credentials at all. - Enforcement is *effectively* realm-wide but *technically* per-user: the conditional subflow is what fires. A medewerker whose OTP credential were removed would fall back to the required action at next login (enrol, then challenge) rather than skipping MFA — an acceptable equivalence for this purpose, and the reason the required action is set. - Reversal is a one-file edit: drop the `otp` credentials and the `requiredActions` block.