key-cape/docs/optional-mfa.md
tegwick 632b1f1376
All checks were successful
Authentication acceptance / acceptance (push) Successful in 1m10s
Build and Publish Container Image / build-and-push (push) Successful in 44s
Support provider credential renewal and reject unsuccessful OTP validation
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
2026-09-13 14:23:24 +02:00

5 KiB

Optional MFA for browser clients

Set mfaOptional: true on a reviewed browser registration to allow AAL1 login when privacyIDEA authoritatively reports no active enrolled factor. Once an active factor exists, require AAL2. A previous AAL1 session cannot satisfy that requirement. Explicit AAL2 requests still require MFA. Lookup errors deny login through the existing account-recovery path. This setting overrides only the provider requireForAll default for that client.

Do not combine this with mfaRequired. In particular, mfaRequired: false means unconditional AAL1 and is not an opt-in enrollment policy. Unchanged registrations keep their existing policy. Service clients reject mfaOptional.

The adapter sends the raw privacyIDEA JWT in Authorization, filters active factors before pagination, and requires a successful, complete response before concluding there is no active factor. The credential must have realm-scoped administrative tokenlist permission: a user-role token only lists its own factors regardless of the requested username. Never replace it with a self-service JWT. See the provider API.

Deployment gate — not yet enabled

Live inspection on 2026-09-13 found requireForAll: true; both the demo-company and account-portal registrations inherit it. Configured token-list credentials return HTTP 401, so enabling this setting now would replace the OTP prompt with a lookup failure. No live policy has been changed.

Credential owner: railiance-platform / OpenBao, route net-kingdom-privacyidea-admin-token. Its concrete delivery and renewal contract is unpublished (resolvable: false). Obtain an owner-approved realm-scoped factor-read credential through the native custody path, with renewal and revocation ownership. Do not put credentials in chat, arguments, work records, or config examples. The older refresh-pi-token-live.sh needs review before use.

After credential delivery:

  1. Verify the deployed provider accepts the raw JWT and returns authoritative count/tokens results for controlled accounts with and without a factor. Confirm realm/resolver mapping and administrative tokenlist scope, including visibility of another user's factor; an empty list alone is not proof.
  2. Verify self-service at https://pink-account.coulomb.social: password login, TOTP enrollment with possession confirmation before activation, cancellation, and factor removal/recovery. This user-facing flow is not yet verified.
  3. Build and pin the reviewed issuer image. Migrate only the exact vergabe-demo-company registration to mfaOptional using the owner CAS rollout lane; keep unrelated registrations and Secret bytes intact. The current vergabe-client-rollout.py intentionally refuses registration differences and needs an explicit migration before this policy can be applied.
  4. Resolve account-portal access for unenrolled users without weakening privileged access. Review portal policy and self-service permissions before expanding the optional setting to this shared client. Add the verified enrollment link to account management once that flow works.
  5. Verify fresh password-only admission without a factor, enrollment confirmation, then OTP enforcement (including reuse of an old AAL1 session). Verify explicit AAL2 still challenges, lookup failures recover without issuing a code, logout permits identity switching, and other clients retain their policies.

Rollback: restore the exact previous client registration and image pin via CAS. This restores mandatory MFA for the demo client; it is not password-only access.

Credential renewal consumer contract

KeyCape accepts either privacyidea.adminToken (existing startup configuration) or privacyidea.adminTokenFile (an absolute protected mounted-file path). Configure one source only. With a file, KeyCape reads the current value for each factor lookup and OTP validation. Deliver renewal atomically; do not truncate a live file in place. A projected Secret volume may update asynchronously; a subPath mount will not provide live renewal. The deployment owner must verify propagation.

The file contains only the raw provider JWT with an optional trailing newline; input is bounded to 16 KiB. Missing, empty, malformed, oversized or unreadable files deny the operation without a cached or inline fallback. File contents and paths are excluded from credential errors. Provider rejection also denies access; file presence alone does not prove credential validity, scope or freshness. Default provider requests have a ten-second timeout and do not follow redirects.

The credential must remain provider-issued, realm-scoped and renewed through owner custody. This consumer feature neither mints credentials nor establishes an OpenBao path. Deploying an approved file requires a separate protected mount, read permission for KeyCape's runtime identity, renewal/expiry ownership, and positive factor/no-factor plus failed-credential/recovery evidence. Existing live configuration and policy have not been changed by implementing this feature.