Multi-factor authentication
Passkeys/WebAuthn as the implemented second factor, per-role MFA enforcement, and step-up auth on sensitive operations.
Multi-factor authentication
Pact's second factor is passkeys (WebAuthn). Credentials live in
tenant_user_credentials and are managed under /v1/auth/passkey/*
(api/routes/auth_passkey.py). On top of enrollment, Pact adds two
enforcement layers: per-role MFA requirements and step-up (re-prompt) auth on
sensitive operations.
Passkeys today; TOTP reserved
Passkeys/WebAuthn is the implemented factor. The step-up primitive has a
totp method reserved in its schema, but it is not implemented — calling
it returns 501 Not Implemented. Do not plan a rollout around authenticator-app
TOTP codes yet.
Enrolling and using passkeys
The passkey routes cover the full WebAuthn ceremony:
POST /v1/auth/passkey/register/beginand/register/complete— enroll a new authenticator.GET /v1/auth/passkey/listandDELETE /v1/auth/passkey/{credential_pk}— manage enrolled credentials.POST /v1/auth/passkey/login/beginand/login/complete(plus/login/available) — passwordless / second-factor sign-in.POST /v1/auth/passkey/enrollment-handoff/startand/redeem— hand off enrollment to another device.
Each enrolled authenticator is one row in tenant_user_credentials.
Per-role enforcement
Admins can require MFA per role. /v1/iam/role-mfa
(api/routes/iam_policies.py) reads and writes role_mfa_requirements
(one requires_mfa + optional grace_until per role, per tenant). The check
in core/iam/mfa_policy.py works like this:
user_has_mfa(...)is true when the user has at least one row intenant_user_credentials(i.e. at least one passkey enrolled).- If a role requires MFA and the user has none, they're blocked with a 403 —
unless
grace_untilis set and still in the future, which lets an admin turn the requirement on and give users a window to enroll before enforcement bites.
Step-up on sensitive operations
Step-up auth issues a short-lived, single-use token scoped to a specific
action_class (api/routes/auth_stepup.py):
POST /v1/auth/stepup/start— create a challenge bound to an action class (e.g.billing.change).POST /v1/auth/stepup/verify— prove a fresh credential and receive the one-shot token. Implemented methods arepassword(re-prompt, compared against the existing hash) andwebauthn(a fresh passkey assertion).totpreturns501.GET /v1/auth/stepup/me— inspect the current step-up state.
The sensitive-ops policy (core/iam/sensitive_ops.py) decides which
mutations demand a fresh step-up token. It is per-tenant and default-OFF
(stored under the step_up_sensitive_ops org-setting key): until a tenant
opts in, gated endpoints behave exactly as before — zero blast radius on
deploy. When enabled, it covers billing changes, data exports, and
integration-credential changes.