key-cape/docs/optional-mfa.md
tegwick 113f3a6296
All checks were successful
Authentication acceptance / acceptance (push) Successful in 1m6s
Build and Publish Container Image / build-and-push (push) Successful in 53s
Verify reader scope before accepting absence of enrolled factors
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
2026-09-13 16:59:09 +02:00

7.1 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 — optional client policy not yet enabled

Factor-read custody, automatic renewal and mounted-file delivery were restored on 2026-09-13 through RPF-WP-0040 / CCR-2026-0023. KeyCape uses adminTokenFile; per-user provider lookup and mounted renewal passed. Historical resolver incident lanes remain separate; there is no missing-owner gate for this service lane.

The live provider has mfa-passthru-phase1, so a positive validation value alone can mean directory-password success. KeyCape accepts AAL2 only when a successful response identifies a TOTP/HOTP token by serial and type. Static-password tokens, missing factor evidence and unsuccessful status cannot grant AAL2.

privacyIDEA's active flag does not imply completed enrollment. Its verification policy sets rollout_state=verify while leaving active unchanged. KeyCape therefore recognizes enrolled and the provider's legacy empty state as enrolled; verify, clientwait and pending remain incomplete. Missing/unknown/broken states and incomplete result pages fail closed. Existing verified factors still require OTP while an additional token is pending. Mandatory and explicit AAL2 policies remain mandatory throughout onboarding.

Current provider self-service policy allows TOTP enrollment/deletion/disabling, but possession confirmation is not yet required. Review and exercise that transition plus authenticated recovery before enabling the optional client.

Remaining live acceptance:

  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.

Distinguish missing factors from revoked reader access

The installed provider returns HTTP 200 with zero tokens after tokenlist policy withdrawal. Before accepting a no-factor result, KeyCape therefore requires a second exact-serial lookup for privacyidea.readProbeSerial. This must identify a disabled, unassigned token in exactly the configured realm. Missing probe, missing visibility, assignment, activation, realm mismatch or provider failure denies the decision. Existing confirmed factors still require MFA.

The production probe is KCFACTORSCOPE01, created idempotently by NetKingdom's sso-mfa/k8s/privacyidea/keycape-scope-probe.py. Its key is generated by the provider and never delivered to KeyCape. Keep it disabled and unassigned; it exists solely to prove realm visibility. Removing it intentionally makes no-factor decisions unavailable until scope proof is restored.

provider-onboarding-contract.py uses a temporary in-memory database and no production configuration or credentials. Against the installed provider it proved password-only pass-through, pending-active enrollment, cancellation, TOTP possession confirmation (explicit type=totp on confirmation), confirmed state, empty-success on permission withdrawal, permission recovery and rejection of a genuinely provider-issued expired JWT. Production expiry/revocation drills remain separately recorded under RPF-WP-0040-T04.