MFA Orchestration¶
The MFA orchestrator manages challenge lifecycle: creation, verification, expiry, and attempt limiting.
Supported Methods¶
| Method | Status | Description |
|---|---|---|
| TOTP | Active | RFC 6238, SHA-1, 6 digits, 30s period |
| Push | Active | HMAC-SHA256 signed approval with number matching |
| WebAuthn | Implemented, unproven on real hardware | FIDO2 / passkey, phishing-resistant (ES256+ via go-webauthn). Tested against a software authenticator only — see the caveat in WebAuthn. |
| Planned | OTP via email | |
| SMS | Planned | OTP via SMS (low security, last resort) |
Challenge Lifecycle¶
- Create — When the decision is
challenge, the orchestrator creates aChallengewith a unique ID, nonce, allowed methods, TTL (default 300s), and max attempts (default 5). - Verify — The user submits a code or approval. The orchestrator checks:
- Challenge is still
pending - Not expired
- Attempts remaining
- Resolve — On success, status →
approved. On max attempts, status →failed. On TTL expiry, status →expired.
TOTP¶
- Enrollment:
POST /v1/identities/{id}/factors/totp/enrollreturns a secret andotpauth://URI. The factor starts aspending_activation. - Activation:
POST /v1/identities/{id}/factors/totp/activatewith a valid code transitions the factor toactive. - Verification: validates the code against all active TOTP factors for the identity, with a 1-period skew tolerance.
Push¶
- Device registration:
POST /v1/devicesreturns a device record and a one-time secret. The secret is SHA-256 hashed at rest. - Number matching: each push challenge includes a random 0–99 number that the user must confirm on their device.
- Signature: the device signs
challengeID|number|timestampwith HMAC-SHA256 using the hashed secret. - Timestamp skew: configurable (default 60s) to prevent replay.
WebAuthn¶
Verification status — read before relying on this factor. The WebAuthn factor is implemented and unit-tested against a software test authenticator (
core/internal/webauthntest) performing real ES256 ceremonies: attestation objects, CBOR encoding and genuine signatures. It has not been proven against a real browser passkey or a hardware security key (e.g. YubiKey), including platform authenticators, resident/discoverable credentials, roaming-credential quirks and thelargeBlob/hmac-secretextension paths. Treat it as experimental and validate against your own authenticator fleet before production use. Cross-user credential scoping, assertion replay and ceremony expiry are covered by negative tests.
WebAuthn provides phishing-resistant MFA using platform or roaming
authenticators (passkeys, security keys). It is implemented via the
github.com/go-webauthn/webauthn library and follows the same
enroll/activate-style ceremony pattern as TOTP, adapted to WebAuthn's
challenge/response ceremonies.
- Configuration: Relying Party settings under
webauthn:inconfig.yaml(rp_id,rp_origins,rp_display_name) withOIAF_WEBAUTHN_RP_ID/OIAF_WEBAUTHN_RP_ORIGINS/OIAF_WEBAUTHN_RP_DISPLAY_NAMEenv overrides.rp_idis the effective domain (no scheme/port);rp_originsis a comma-separated allowlist of fully qualified browser origins. Without both, the factor is disabled and its endpoints return503. - Registration:
POST /v1/identities/{id}/factors/webauthn/registration/begincreates apending_activationfactor and returns the WebAuthnpublicKeycreation options for the browser. The browser's rawnavigator.credentials.create()result is POSTed to.../registration/{factor_id}/finish, which verifies the attestation and activates the factor. - Verification (identity-scoped):
.../verification/beginreturns assertion options; the rawnavigator.credentials.get()result goes to.../verification/finish. - Verification (challenge-orchestrated / policy step-up):
POST /v1/challenge/{id}/webauthn/beginthenPOST /v1/challenge/{id}/webauthn/finish. This path binds the ceremony to the challenge's TTL and attempt accounting and is the method a policy selectingwebauthndrives end-to-end. - Credential storage: each registered credential is a
types.Factor(method=webauthn) whose opaqueCredentialsbytes hold the JSON credential record (public key, sign counter, flags). In-flight ceremony sessions ride on the pending factor (registration) or on the challenge (WebAuthnSession, verification), so no new storage interface is needed and the PostgresStore skeleton stays compatible (credentials → bytea). - Attestation:
noneconveyance preference (low-friction MFA). The sign counter is persisted per assertion to support clone detection. - User verification:
webauthn.require_user_verificationselectspreferred(default — the authenticator is asked for UV but the ceremony still succeeds without it) orrequired(the ceremony fails unless the user-verified flag is set). Setrequiredif you need UV to be enforced server-side rather than merely requested; withpreferredan authenticator that skips UV is accepted. - Ceremony expiry: registration ceremonies expire server-side after the same TTL
as verification sessions (5 minutes), so an abandoned
pending_activationfactor cannot be finished indefinitely. Expired ceremonies are rejected and the factor stays pending.
Method Preference¶
When a policy specifies challenge.methods, the orchestrator uses those
methods. If no methods are specified, TOTP is the default. The order in the
array indicates preference.