Skip to main content

What is multi-factor authentication (MFA)?

Requests made to Turnkey’s public API are required to be authenticated using a stamp. MFA adds an additional layer of security by requiring users to provide multiple forms of authentication before they can perform specific activities or establish a session with elevated permissions. Turnkey’s MFA policies can be configured to require any combination of supported authentication methods.

MFA is evaluated for the user who stamped

Since every request is authenticated by a stamp, the stamp is also what decides whose MFA policies apply. Turnkey evaluates the policies of the user whose credential signed the request, not of the user the activity happens to be about. This matters most at login, where the two are easily different:
  • A login stamped in the browser by the end user, such as STAMP_LOGIN carrying an attested stamp from an email OTP or an OIDC token, is attributed to the sub-organization user. Their MFA policies apply.
  • A login stamped with a parent-organization API key is attributed to that parent user, even though the login itself targets the sub-organization. This is what happens when your backend submits OTP_LOGIN or OAUTH_LOGIN with its own API key, and what the Auth Proxy does. The sub-organization user’s MFA policies are never consulted, and the login succeeds with no error.
The second case is a normal, supported way to run authentication, and nothing is wrong with it until you add MFA. It is worth checking which one you are on before writing a policy, because a backend-driven login is easy to build without ever noticing that the stamp came from your infrastructure rather than from the user. So an activity.resource == 'AUTH' policy only takes effect if the login itself is stamped by the end user. If your login is backend-driven, keep INIT_OTP and VERIFY_OTP on your server and let the browser stamp the login with the verification token, which is bound to a client-generated key and useless to anyone else.

MFA policies

MFA policies are a unique resource type in Turnkey that allows configuration of authentication requirements, scoped to a specific condition. MFA policies can be configured in both parent and sub-organizations, and are enforced at the user level rather than the organization level. MFA applies to all users, including root users and non-root users. Once an MFA policy is created, it is immediately enforced for the user.

MFA policy structure

An MFA policy can be created using the CreateMfaPolicy activity and passing in the following parameters:
  • userId: The ID of the user the policy applies to
  • mfaPolicyName: The name of the MFA policy
  • condition: A string of policy language that evaluates to true or false based on the context of an activity. If the condition evaluates to true, the specified authentication methods are required.
  • requiredAuthenticationMethods: An ordered list of authentication steps that must be satisfied when the policy condition is met. Each step contains an any array of acceptable methods. If multiple methods are listed in any, satisfying any one of them fulfills that step.
  • order: An integer that specifies the order of evaluation for multiple MFA policies. Policies with lower order values are evaluated first.
  • notes: Optional field for any additional information about the policy
When updating a policy with UpdateMfaPolicy, an empty requiredAuthenticationMethods array means “leave unchanged”, not “remove”. A policy always has at least one required method, so to relax a policy, send the methods you want rather than an empty array. To stop requiring MFA in a case, narrow the condition or delete the policy.

Condition

The condition field is a string written in Turnkey’s policy language. It determines when the MFA policy applies based on the context of the incoming activity. When a user submits a request, each of their MFA policies are evaluated in order. If a policy’s condition evaluates to true, the authentication requirements defined in that policy must be satisfied before the activity can proceed. Conditions have access to the same keywords available in regular policy conditions. Examples:
You can find more examples here.

Required authentication methods

The requiredAuthenticationMethods field defines an ordered list of authentication steps that the user must complete when the policy’s condition is met. Each step is a RequiredAuthenticationMethodParams object containing an any array of AuthenticationMethodParams. The structure works as follows:
  • Each entry in requiredAuthenticationMethods represents a step. All steps must be satisfied, but the order in which they are satisfied does not matter.
  • Within each entry, the any array contains one or more authentication methods. If multiple methods are listed, the user must satisfy any one of them.
In the example above, the user must:
  1. Authenticate with an API key
  2. Authenticate with either a passkey or email OTP
Do not list the same method in more than one step. Each step is checked independently against everything the user has presented, and satisfying a step does not use the credential up. A method listed in two steps therefore satisfies both.Suppose you want to require any two of passkey and email OTP, and write this:
The user stamps with their passkey. Step 1 looks for a passkey or an email OTP and finds the passkey. Step 2 looks for the same two things and finds the same passkey. Both steps pass, no challenge is raised, and the activity proceeds on a single factor.Listing several methods in one step is fine, and is how you let the user choose. The rule is only that no method may appear in more than one step:
This is also why “any two of these three” cannot be expressed. Every step must name the specific methods it accepts.
You can find more examples here. Each AuthenticationMethodParams accepts:
  • type (required): The authentication type. Supported values:
    • AUTHENTICATION_TYPE_PASSKEY
    • AUTHENTICATION_TYPE_API_KEY
    • AUTHENTICATION_TYPE_SESSION
    • AUTHENTICATION_TYPE_EMAIL_OTP
    • AUTHENTICATION_TYPE_SMS_OTP
    • AUTHENTICATION_TYPE_OAUTH
  • id (optional): A specific authentication method’s ID. When provided, only that specific authentication method satisfies the requirement. When omitted, any authentication method of the specified type can be used. Note that Email OTP and SMS OTP do not have IDs.
For AUTHENTICATION_TYPE_SESSION, the id is a session profile ID, and the all-zero UUID is a special case that inverts the check: it requires a session with no profile attached, meaning an ordinary read/write session. Use it to exclude scoped sessions from an activity.

The user must already be able to satisfy the policy

Turnkey rejects a policy the user has no way to meet, at creation time:
Each type is checked against what the user has today: So enroll the credential first, then create the policy. This also means you cannot lock a user out by writing a policy, only by removing a credential a policy already depends on.

Evaluation order

Only one MFA policy is ever enforced per request. MFA policies are evaluated in order, and once a policy’s condition evaluates to true, that policy is enforced and evaluation stops. The order field is an integer that determines the evaluation priority when a user has multiple MFA policies. Policies with lower order values are evaluated first. This lets you place narrowly-scoped, high-sensitivity policies first, and keep broader “catch-all” policies with higher order values as a fallback.