Multi-Factor Authentication
Add a second authentication factor for your identities, enforced per Environment and tightened per organization.
Overview
Multi-factor authentication (MFA) requires an identity to present a second factor, beyond their password, when they sign in. In Canopy, MFA is configured per Environment, so you can require it in production while leaving staging open, and it applies the same way whether your users authenticate through the hosted login pages or the Direct API. Your own administrators' second factor is a separate thing, enrolled on their profile and required per account; see Administrator Two-Factor Authentication under Administration.
Per-Environment enforcement
Five settings, scoped to each Environment
Every Environment carries its own MFA policy:
Factor types
Canopy supports four factor types:
Login-time decisions
At sign-in, the response tells your integration which MFA step comes next:
Step-up & admin reset
Two operational behaviors round out the flow:
Remember this device
Off until you give it a window
A challenge answer can carry remember_device: true, and the device then skips the second factor on later sign-ins to the same Environment. The Environment decides whether that is honoured: mfa_trusted_device_days is 0 by default, which remembers nothing, and is raised in the Console under Tenant → Authentication → Multi-Factor Auth (up to 90 days). Read the value from GET /v1/identity/auth/config before you show a checkbox, and read device_remembered on the answer to know what happened: false means the request was declined, not that it failed. Canopy's hosted challenge page follows the same rule and shows the checkbox only when the window is set.
How the remembered device travels
POST /v1/identity/auth/mfa/challenge/totp
X-API-Key: cnpy_...
{ "challenge_token": "...", "code": "123456", "remember_device": true }
200
{
"data": {
"access_token": "...",
"refresh_token": "...",
"device_remembered": true,
"trusted_device_token": "...",
"trusted_device_expires_at": "2026-10-31T00:00:00.000Z"
}
}
POST /v1/identity/auth/login
X-API-Key: cnpy_...
{ "publishable_key": "...", "email": "...", "password": "...", "trusted_device_token": "..." }Passkeys and your domain
A passkey only works on the domain it was created for
WebAuthn scopes every credential to a relying-party id: a registrable domain such as acme.com. The browser refuses a ceremony unless the page's domain is that id or a subdomain of it, and Canopy refuses an answer from any origin it has not been told about. So where a passkey can be used is decided by where it was enrolled, and you have to tell Canopy which domain that is.
Two surfaces, two relying parties
Name your domain on the Environment
Two fields on the Environment's auth settings, editable in the Console under Tenant → Authentication → Multi-Factor Auth (Passkey relying party) or through the portal API:
PATCH /portal/v1/accounts/{accountSlug}/applications/{appSlug}/environments/{envSlug}/auth-settings
{
"mfa": {
"mfa_factor_allowlist": ["totp", "webauthn"],
"webauthn_rp_id": "acme.com",
"webauthn_origins": ["https://portal.acme.com", "https://acme.com"]
}
}What follows from the binding
Where MFA applies
MFA covers both the Direct API and Hosted OAuth
The hosted login pages run the same challenge and enrollment flow as the Direct API, and the same organization policy (its MFA tightening, Require single sign-on and sign-in methods), so your users are covered however they sign in. Tokens issued after a factor is verified record it in the standard amr (authentication methods reference) claim: for example ["pwd", "mfa"] for a password login plus a second factor, or ["sso", "mfa"] for an SSO step-up, so your backend can confirm a session was MFA-verified before allowing a sensitive action.
Per-organization policy
Organizations tighten the Environment, never loosen it
With the organizations container on, each organization carries its own policy over the Environment's: mfa_required, mfa_factor_allowlist, and mfa_after_sso, each nullable and inheriting the Environment when unset, plus require_sso. The effective policy is the Environment's tightened by the organization's, decided at login for the organization the session starts in, on the Direct API and the hosted pages alike, and again at every organization switch, which can raise an MFA challenge of its own. A write that would loosen the Environment is refused. It exists so one customer can mandate MFA, or SSO only, while another keeps the Environment's default: the customer's own admin decides in your product, and your backend writes the organization's policy through the API.
Tell us how we can improve this guide.