PPactDocs
Administration

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/begin and /register/complete — enroll a new authenticator.
  • GET /v1/auth/passkey/list and DELETE /v1/auth/passkey/{credential_pk} — manage enrolled credentials.
  • POST /v1/auth/passkey/login/begin and /login/complete (plus /login/available) — passwordless / second-factor sign-in.
  • POST /v1/auth/passkey/enrollment-handoff/start and /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 in tenant_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_until is 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 are password (re-prompt, compared against the existing hash) and webauthn (a fresh passkey assertion). totp returns 501.
  • 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.