From a94b31331f50d22bf2fd434430819d3a9e98f9b0 Mon Sep 17 00:00:00 2001 From: Amir Cheikh Date: Fri, 3 Apr 2026 17:04:32 -0400 Subject: [PATCH 1/4] Started mfa docs --- authentication/mfa.mdx | 218 ++++++++++++++++++++++++++++++++++++ authentication/overview.mdx | 3 + docs.json | 3 +- 3 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 authentication/mfa.mdx diff --git a/authentication/mfa.mdx b/authentication/mfa.mdx new file mode 100644 index 00000000..6ec64a2b --- /dev/null +++ b/authentication/mfa.mdx @@ -0,0 +1,218 @@ +--- +title: "Multi-Factor Authentication (MFA)" +description: "Hello" +--- + +## What is multi-factor authentication (MFA)? + +Requests made to Turnkey's public API are required to authenticated using an API key or WebAuthn stamp. MFA adds an additional layer of security by requiring users to provide multiple forms of authentication before they can perform specific activities. + +MFA is highly customizable and can be configured to require any combination of supported authentication methods for different activities. + +## MFA policies +MFA policies are a unique resource type in Turnkey that allows configuation of authentication requirements, scoped to a specific condition. + +MFA policies can be created by parent, or sub-organization users and are tied to the targetted user. + +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](../concepts/policies/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 array of authentication methods that are required when the policy condition is met. Methods can be logically "OR-ed" together by grouping them in nested "ANY" blocks. +- `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 + +### Condition + +The `condition` field is a string written in Turnkey's [policy language](../concepts/policies/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 is 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](../concepts/policies/language#keywords). + +**Examples:** + +``` ts +// Require MFA for all signing activities +activity.action == 'SIGN' + +// Require MFA for high-value Ethereum transfers (value > 1 ETH in wei) +activity.action == 'SIGN' && eth.tx.value > 1000000000000000000 + +// Require MFA for everything +true +``` + +### 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 **sequential step** - all steps must be satisfied in order. +- Within each step, the `any` array contains one or more authentication methods. If multiple methods are listed, the user must satisfy **any one of them** (logical OR). + +```json +{ + "requiredAuthenticationMethods": [ + { + "any": [ + { "type": "AUTHENTICATION_TYPE_API_KEY" } + ] + }, + { + "any": [ + { "type": "AUTHENTICATION_TYPE_PASSKEY" }, + { "type": "AUTHENTICATION_TYPE_EMAIL_OTP" } + ] + } + ] +} +``` + +In the example above, the user must: +1. **Step 1**: Authenticate with an API key +2. **Step 2**: Authenticate with **either** a passkey **or** email OTP + +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 authenticator ID. When provided, only that specific authenticator satisfies the requirement. When omitted, any authenticator of the specified type can be used. + +### Evaluation order + +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**. + +When a request is made, each MFA policy is checked in order. The first policy whose condition evaluates to `true` determines the authentication requirements for that request. This allows you to create specific policies for sensitive operations with lower order values, and broader catch-all policies with higher order values. + +```json +// Order 1: Strict MFA for signing - evaluated first +{ + "condition": "activity.action == 'SIGN'", + "order": 1, + "requiredAuthenticationMethods": [ + { "any": [{ "type": "AUTHENTICATION_TYPE_PASSKEY" }] }, + { "any": [{ "type": "AUTHENTICATION_TYPE_EMAIL_OTP" }] } + ] +} + +// Order 2: Lighter MFA for everything else - evaluated second +{ + "condition": "true", + "order": 2, + "requiredAuthenticationMethods": [ + { "any": [{ "type": "AUTHENTICATION_TYPE_PASSKEY" }, { "type": "AUTHENTICATION_TYPE_API_KEY" }] } + ] +} +``` + +## Satisfying MFA + +When a user submits an activity and one of their MFA policies evaluates to `true`, the activity is placed in `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` status. The activity will not execute until the user proves they have the required authentication methods. + +To satisfy MFA, the user must call the `APPROVE_ACTIVITY` activity, passing in the `fingerprint` of the original activity. The way the user stamps this approval request determines which authentication method is proven. + +You can learn more about stamps [here](../developer-reference/api-overview/stamps). + +### API key + +To prove API key authentication, the user stamps the `APPROVE_ACTIVITY` request with an API key. If the MFA policy specifies an `id`, the user must stamp with that specific API key. + +### Passkey + +To prove passkey authentication, the user stamps the `APPROVE_ACTIVITY` request with a WebAuthn authenticator. If the MFA policy specifies an `id`, the user must stamp with that specific authenticator. + +### Session + +To prove session authentication, the user stamps the `APPROVE_ACTIVITY` request with a session credential. A session credential is an API key that was classified as a session after a login activity (e.g., `STAMP_LOGIN`, `OTP_LOGIN`). + +If the MFA policy specifies an `id` for a session authentication method, the `id` refers to a session profile ID. The user must stamp with a session credential that was issued with that specific session profile. + +{/* TODO (Amir/Moe): Link to session profiles documentation */} + +### Email OTP, SMS OTP, and OAuth + +TODO (Amir/Moe): Talk about token stamps and link to docs + +## MFA and consensus + +MFA works alongside Turnkey's [consensus](../concepts/users/root-quorum) system for activities that require approval from multiple users. + +When an activity requires both MFA and consensus: + +1. **The proposing user must satisfy their own MFA first.** If the proposer has an MFA policy that matches the activity, the activity is returned with `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED`. The proposer must satisfy their MFA requirements before the activity can proceed to consensus. +2. **Subsequent approvers vote on the activity as normal.** Once the proposer's MFA is satisfied, other users in the quorum can approve or reject the activity. +3. **Approving users must also satisfy their own MFA.** If an approving user has an MFA policy that matches the activity, they will receive `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` when they attempt to vote. They must satisfy their own MFA requirements before their vote is recorded. +4. **The activity executes only after all required users have satisfied MFA and consensus is met.** + +This ensures that every user involved in an activity is individually held to their own MFA requirements, regardless of whether they are the proposer or an approver. + +## Enforcing MFA for end users + +Enforcing MFA for an end-user on a sub-organization can do so using a [delegated access user](../concepts/policies/delegated-access-overview). A delegated access user is a non-root user created in the sub-organization whose API key is controlled by the parent organization. + +To set this up: + +1. The sub-organization's root user creates a delegated access user with an API key controlled by the parent org. +2. The sub-organization's root user creates a policy that allows the delegated access user to manage MFA policies. +3. The delegated access user can then create MFA policies for the sub-organization's root user. + +The policy assigned to the delegated access user should be scoped to only allow MFA policy management: + +``` ts +// Policy condition: only allow MFA policy activities +activity.resource == 'MFA_POLICY' +``` + +Once this is in place, the delegated access user (controlled by the parent org) can create MFA policies on behalf of the end user. For example, to require MFA for all activities: + +```json +{ + "userId": "", + "mfaPolicyName": "MFA for everything", + "condition": "true", + "requiredAuthenticationMethods": /* The authentication methods you want to require */, + "order": 1 +} +``` + +## MFA recovery + +If an end user loses access to one of their authentication methods, they may be locked out of activities that require MFA. Because Turnkey cannot write to organizations directly, Turnkey is unable to recover users on their behalf. You must set up a recovery mechanism in advance. + +The recommended approach is to use [delegated access users](../concepts/policies/delegated-access-overview) to delete the MFA policy that is locking the user out. The delegated access user's policy should be scoped to allow deletion of MFA policies: + +``` ts +// Policy condition: only allow deleting MFA policies +activity.resource == 'MFA_POLICY' && activity.action == 'DELETE' +``` + +### Recommended: quorum-based recovery + +It is strongly recommended that you set up **two or more delegated access users** for MFA recovery, with a consensus policy requiring both to approve before an MFA policy can be deleted. This prevents any single party from removing a user's MFA protections. + +To set this up: + +1. Create two delegated access users in the sub-organization, each with an API key controlled by different parties in the parent organization. +2. Create a policy scoped to MFA policy deletion with a consensus requirement: + +``` ts +// Policy condition +activity.resource == 'MFA_POLICY' && activity.action == 'DELETE' +``` + +```ts +// Consensus requirement: both delegated users must approve +approvers.count() >= 2 +``` + +With this configuration, deleting an MFA policy requires both delegated access users to approve the `DeleteMfaPolicy` activity. diff --git a/authentication/overview.mdx b/authentication/overview.mdx index b262ef42..0fa413fa 100644 --- a/authentication/overview.mdx +++ b/authentication/overview.mdx @@ -66,4 +66,7 @@ For information about managing authenticated sessions, see our [Sessions](/authe Manage authenticated user sessions and access tokens in your application. + + Enforce multi-factor authentication policies for sensitive activities. + diff --git a/docs.json b/docs.json index 097c3180..a7ea0f06 100644 --- a/docs.json +++ b/docs.json @@ -137,7 +137,8 @@ }, "authentication/backend-setup", "authentication/proxying-signed-requests", - "authentication/credentials" + "authentication/credentials", + "authentication/mfa" ] }, "products/embedded-wallets/features/multi-chain-support", From 1ab37b7dd9f4b8f564181804e2dd2285938897e5 Mon Sep 17 00:00:00 2001 From: Amir Cheikh Date: Mon, 6 Apr 2026 17:33:03 -0400 Subject: [PATCH 2/4] Examples and session profiles --- authentication/mfa.mdx | 218 -------- .../mfa/enforcement-and-recovery.mdx | 68 +++ authentication/mfa/examples.mdx | 507 ++++++++++++++++++ authentication/mfa/overview.mdx | 121 +++++ authentication/mfa/satsifying-mfa.mdx | 83 +++ authentication/overview.mdx | 4 +- .../{sessions.mdx => sessions/overview.mdx} | 6 + authentication/sessions/session-profiles.mdx | 82 +++ docs.json | 18 +- .../embedded-consumer-wallet.mdx | 2 +- embedded-wallets/features/overview.mdx | 2 +- production-checklist/embedded-wallet.mdx | 2 +- .../react/advanced-backend-authentication.mdx | 4 +- .../advanced-backend-authentication.mdx | 4 +- 14 files changed, 892 insertions(+), 229 deletions(-) delete mode 100644 authentication/mfa.mdx create mode 100644 authentication/mfa/enforcement-and-recovery.mdx create mode 100644 authentication/mfa/examples.mdx create mode 100644 authentication/mfa/overview.mdx create mode 100644 authentication/mfa/satsifying-mfa.mdx rename authentication/{sessions.mdx => sessions/overview.mdx} (96%) create mode 100644 authentication/sessions/session-profiles.mdx diff --git a/authentication/mfa.mdx b/authentication/mfa.mdx deleted file mode 100644 index 6ec64a2b..00000000 --- a/authentication/mfa.mdx +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: "Multi-Factor Authentication (MFA)" -description: "Hello" ---- - -## What is multi-factor authentication (MFA)? - -Requests made to Turnkey's public API are required to authenticated using an API key or WebAuthn stamp. MFA adds an additional layer of security by requiring users to provide multiple forms of authentication before they can perform specific activities. - -MFA is highly customizable and can be configured to require any combination of supported authentication methods for different activities. - -## MFA policies -MFA policies are a unique resource type in Turnkey that allows configuation of authentication requirements, scoped to a specific condition. - -MFA policies can be created by parent, or sub-organization users and are tied to the targetted user. - -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](../concepts/policies/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 array of authentication methods that are required when the policy condition is met. Methods can be logically "OR-ed" together by grouping them in nested "ANY" blocks. -- `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 - -### Condition - -The `condition` field is a string written in Turnkey's [policy language](../concepts/policies/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 is 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](../concepts/policies/language#keywords). - -**Examples:** - -``` ts -// Require MFA for all signing activities -activity.action == 'SIGN' - -// Require MFA for high-value Ethereum transfers (value > 1 ETH in wei) -activity.action == 'SIGN' && eth.tx.value > 1000000000000000000 - -// Require MFA for everything -true -``` - -### 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 **sequential step** - all steps must be satisfied in order. -- Within each step, the `any` array contains one or more authentication methods. If multiple methods are listed, the user must satisfy **any one of them** (logical OR). - -```json -{ - "requiredAuthenticationMethods": [ - { - "any": [ - { "type": "AUTHENTICATION_TYPE_API_KEY" } - ] - }, - { - "any": [ - { "type": "AUTHENTICATION_TYPE_PASSKEY" }, - { "type": "AUTHENTICATION_TYPE_EMAIL_OTP" } - ] - } - ] -} -``` - -In the example above, the user must: -1. **Step 1**: Authenticate with an API key -2. **Step 2**: Authenticate with **either** a passkey **or** email OTP - -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 authenticator ID. When provided, only that specific authenticator satisfies the requirement. When omitted, any authenticator of the specified type can be used. - -### Evaluation order - -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**. - -When a request is made, each MFA policy is checked in order. The first policy whose condition evaluates to `true` determines the authentication requirements for that request. This allows you to create specific policies for sensitive operations with lower order values, and broader catch-all policies with higher order values. - -```json -// Order 1: Strict MFA for signing - evaluated first -{ - "condition": "activity.action == 'SIGN'", - "order": 1, - "requiredAuthenticationMethods": [ - { "any": [{ "type": "AUTHENTICATION_TYPE_PASSKEY" }] }, - { "any": [{ "type": "AUTHENTICATION_TYPE_EMAIL_OTP" }] } - ] -} - -// Order 2: Lighter MFA for everything else - evaluated second -{ - "condition": "true", - "order": 2, - "requiredAuthenticationMethods": [ - { "any": [{ "type": "AUTHENTICATION_TYPE_PASSKEY" }, { "type": "AUTHENTICATION_TYPE_API_KEY" }] } - ] -} -``` - -## Satisfying MFA - -When a user submits an activity and one of their MFA policies evaluates to `true`, the activity is placed in `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` status. The activity will not execute until the user proves they have the required authentication methods. - -To satisfy MFA, the user must call the `APPROVE_ACTIVITY` activity, passing in the `fingerprint` of the original activity. The way the user stamps this approval request determines which authentication method is proven. - -You can learn more about stamps [here](../developer-reference/api-overview/stamps). - -### API key - -To prove API key authentication, the user stamps the `APPROVE_ACTIVITY` request with an API key. If the MFA policy specifies an `id`, the user must stamp with that specific API key. - -### Passkey - -To prove passkey authentication, the user stamps the `APPROVE_ACTIVITY` request with a WebAuthn authenticator. If the MFA policy specifies an `id`, the user must stamp with that specific authenticator. - -### Session - -To prove session authentication, the user stamps the `APPROVE_ACTIVITY` request with a session credential. A session credential is an API key that was classified as a session after a login activity (e.g., `STAMP_LOGIN`, `OTP_LOGIN`). - -If the MFA policy specifies an `id` for a session authentication method, the `id` refers to a session profile ID. The user must stamp with a session credential that was issued with that specific session profile. - -{/* TODO (Amir/Moe): Link to session profiles documentation */} - -### Email OTP, SMS OTP, and OAuth - -TODO (Amir/Moe): Talk about token stamps and link to docs - -## MFA and consensus - -MFA works alongside Turnkey's [consensus](../concepts/users/root-quorum) system for activities that require approval from multiple users. - -When an activity requires both MFA and consensus: - -1. **The proposing user must satisfy their own MFA first.** If the proposer has an MFA policy that matches the activity, the activity is returned with `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED`. The proposer must satisfy their MFA requirements before the activity can proceed to consensus. -2. **Subsequent approvers vote on the activity as normal.** Once the proposer's MFA is satisfied, other users in the quorum can approve or reject the activity. -3. **Approving users must also satisfy their own MFA.** If an approving user has an MFA policy that matches the activity, they will receive `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` when they attempt to vote. They must satisfy their own MFA requirements before their vote is recorded. -4. **The activity executes only after all required users have satisfied MFA and consensus is met.** - -This ensures that every user involved in an activity is individually held to their own MFA requirements, regardless of whether they are the proposer or an approver. - -## Enforcing MFA for end users - -Enforcing MFA for an end-user on a sub-organization can do so using a [delegated access user](../concepts/policies/delegated-access-overview). A delegated access user is a non-root user created in the sub-organization whose API key is controlled by the parent organization. - -To set this up: - -1. The sub-organization's root user creates a delegated access user with an API key controlled by the parent org. -2. The sub-organization's root user creates a policy that allows the delegated access user to manage MFA policies. -3. The delegated access user can then create MFA policies for the sub-organization's root user. - -The policy assigned to the delegated access user should be scoped to only allow MFA policy management: - -``` ts -// Policy condition: only allow MFA policy activities -activity.resource == 'MFA_POLICY' -``` - -Once this is in place, the delegated access user (controlled by the parent org) can create MFA policies on behalf of the end user. For example, to require MFA for all activities: - -```json -{ - "userId": "", - "mfaPolicyName": "MFA for everything", - "condition": "true", - "requiredAuthenticationMethods": /* The authentication methods you want to require */, - "order": 1 -} -``` - -## MFA recovery - -If an end user loses access to one of their authentication methods, they may be locked out of activities that require MFA. Because Turnkey cannot write to organizations directly, Turnkey is unable to recover users on their behalf. You must set up a recovery mechanism in advance. - -The recommended approach is to use [delegated access users](../concepts/policies/delegated-access-overview) to delete the MFA policy that is locking the user out. The delegated access user's policy should be scoped to allow deletion of MFA policies: - -``` ts -// Policy condition: only allow deleting MFA policies -activity.resource == 'MFA_POLICY' && activity.action == 'DELETE' -``` - -### Recommended: quorum-based recovery - -It is strongly recommended that you set up **two or more delegated access users** for MFA recovery, with a consensus policy requiring both to approve before an MFA policy can be deleted. This prevents any single party from removing a user's MFA protections. - -To set this up: - -1. Create two delegated access users in the sub-organization, each with an API key controlled by different parties in the parent organization. -2. Create a policy scoped to MFA policy deletion with a consensus requirement: - -``` ts -// Policy condition -activity.resource == 'MFA_POLICY' && activity.action == 'DELETE' -``` - -```ts -// Consensus requirement: both delegated users must approve -approvers.count() >= 2 -``` - -With this configuration, deleting an MFA policy requires both delegated access users to approve the `DeleteMfaPolicy` activity. diff --git a/authentication/mfa/enforcement-and-recovery.mdx b/authentication/mfa/enforcement-and-recovery.mdx new file mode 100644 index 00000000..2afb3dac --- /dev/null +++ b/authentication/mfa/enforcement-and-recovery.mdx @@ -0,0 +1,68 @@ +--- +title: "MFA Enforcement and Recovery" +description: "Learn how to enforce multi-factor authentication (MFA) policies for end-users in your sub-organizations, and how to set up recovery mechanisms in case users lose access to their authentication methods." +sidebarTitle: "Enforcement and Recovery" +--- + +## Enforcing MFA for end users + +Enforcing MFA for an end-user on a sub-organization can do so using a [delegated access user](/concepts/policies/delegated-access-overview). + +A delegated access user is a non-root user created in the sub-organization whose API key is controlled by the parent organization and has carefully scoped permissions to perform only specific actions. + +To set this up: + +1. The sub-organization's root user creates a delegated access user with an API key controlled by the parent org. +2. The sub-organization's root user creates a policy that allows the delegated access user to manage MFA policies. +3. The delegated access user can then create MFA policies for the sub-organization's root user. + +The policy assigned to the delegated access user should be scoped to only allow MFA policy management: + +``` ts +// Policy condition: only allow MFA policy activities +activity.resource == 'MFA_POLICY' +``` + +Once this is in place, the delegated access user (controlled by the parent org) can create MFA policies on behalf of the end user. For example, to require MFA for all activities: + +```json +{ + "userId": "", + "mfaPolicyName": "MFA for everything", + "condition": "true", + "requiredAuthenticationMethods": /* The authentication methods you want to require */, + "order": 1 +} +``` + +## MFA recovery + +If an end user loses access to one of their authentication methods, they may be locked out of activities that require MFA. Because Turnkey cannot write to organizations directly, Turnkey is unable to recover users on their behalf. You must set up a recovery mechanism in advance. + +The recommended approach is to use [delegated access users](/concepts/policies/delegated-access-overview) to delete the MFA policy that is locking the user out. The delegated access user's policy should be scoped to allow deletion of MFA policies: + +``` ts +// Policy condition: only allow deleting MFA policies +activity.resource == 'MFA_POLICY' && activity.action == 'DELETE' +``` + +### Quorum-based recovery + +It is strongly recommended that you set up **two or more delegated access users** for MFA recovery, with a consensus policy requiring both to approve before an MFA policy can be deleted. This prevents any single party from removing a user's MFA protections. + +To set this up: + +1. Create two delegated access users in the sub-organization, each with an API key controlled by different parties in the parent organization. +2. Create a policy scoped to MFA policy deletion with a consensus requirement: + +``` ts +// Policy condition +activity.resource == 'MFA_POLICY' && activity.action == 'DELETE' +``` + +```ts +// Consensus requirement: both delegated users must approve +approvers.count() >= 2 +``` + +With this configuration, deleting an MFA policy requires both delegated access users to approve the `DeleteMfaPolicy` activity. diff --git a/authentication/mfa/examples.mdx b/authentication/mfa/examples.mdx new file mode 100644 index 00000000..6510306f --- /dev/null +++ b/authentication/mfa/examples.mdx @@ -0,0 +1,507 @@ +--- +title: "MFA Examples" +description: "Explore examples of multi-factor authentication (MFA) policies in Turnkey, including how to require MFA for specific activities, how to set up recovery mechanisms, and how MFA interacts with Turnkey's consensus system." +sidebarTitle: "Examples" +--- + +Here are some examples of how MFA policies can be used in practice on your end-users' sub-organization. MFA policies are highly customizable and can be configured to fit the specific needs of your organization and users. Feel free to tweak these examples to fit your use case! + +### Only require MFA for signing activities +In this example, we require users to satisfy MFA only when performing signing activities. For all other activities, no MFA is required. In this case, the user can use their existing session along with a passkey to satisfy MFA when signing. + +``` ts +mfaPolicy: { + userId: "", // Required: the user this MFA policy applies to + condition: "activity.action == 'SIGN'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATOR_TYPE_SESSION" }, + ] + }, + { + any: [ + { type: "AUTHENTICATOR_TYPE_PASSKEY" }, + ] + } + ], + order: 0, +} +``` + + +### Two factor authentication +In this example, we require users to authenticate with both a passkey and an email OTP to retrieve a session. Every other activity requires only a session. + +``` ts +// Require users to authenticate with both a passkey and an email OTP to retrieve a session +mfaPolicy: { + condition: "activity.action == 'AUTH'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_EMAIL_OTP" }, + ] + }, + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 0, +} + +// Everything else only requires the session (recieved after satisfying the above MFA requirements to authenticate) +mfaPolicy: { + condition: "true", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_SESSION" }, + ] + } + ], + order: 1, +} +``` + +### Two factor authentication, exporting requires stronger MFA + +In this example, we require users to authenticate with both a passkey and an email OTP to retrieve a session. For exporting, we require users to authenticate with their passkey and their existing session. For all other activities, only a session is required. + +``` ts +// Require users to authenticate with both a passkey and an email OTP to retrieve a session +mfaPolicy: { + condition: "activity.action == 'AUTH'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_EMAIL_OTP" }, + ] + }, + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 0, +} + +// Exporting requires both session and passkey +mfaPolicy: { + condition: "activity.action == 'EXPORT'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_SESSION" }, + ] + }, + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 1, +} + +// Everything else only requires the session +mfaPolicy: { + condition: "true", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_SESSION" }, + ] + } + ], + order: 2, +} +``` + +### Two factor authentication, signing requires MFA every 15 minutes + +In this example, MFA is required for authentication. Signing requires MFA but, a session profile with a 15 minute expiration is used so that users only need to satisfy MFA every 15 minutes when signing. + +In order to get this session profile, the user must authenticate with their existing default session (retrieved by using email OTP and a passkey) and a passkey. All other activities only require a session. + +First, we set up the session profile with a 15 minute expiration on the parent organization: +``` ts +// The UUID for this session will be generated on creation. Let's assume it is `11111111-1111-1111-1111-111111111111` for this example! +sessionProfile: { + name: 'colossal session', + capability: "true", // This session profile can be used for any activity + expirationSeconds: 900, // 15 minutes +} +``` + +Then, we set up the MFA policies on the sub-organization: +``` ts +// Requires users to authenticate with the default session and a passkey in order to retrieve the "colossal session" that has a 15 minute expiration +mfaPolicy: { + condition: "activity.action == 'AUTH' && activity.params.session_profile_id == '11111111-1111-1111-1111-111111111111'", // This condition ensures that the MFA requirements only apply when the user is trying to retrieve the "colossal session" + requiredAuthenticationMethods: [ + { + any: [ + { + type: "AUTHENTICATION_TYPE_SESSION", + id: "00000000-0000-0000-0000-000000000000" // This is the default read/write session that is issued if no session profile is specified during authentication + }, + ] + }, + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 0, +} + + +// Require users to authenticate with both a passkey and email OTP to retrieve a default read/write session +mfaPolicy: { + condition: "activity.action == 'AUTH'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_EMAIL_OTP" }, + ] + }, + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 1, +} + +// Signing requires a session issued with the session profile (which requires MFA every 15 minutes), but not the passkey +mfaPolicy: { + condition: "activity.action == 'SIGN'", + requiredAuthenticationMethods: [ + { + any: [ + { + type: "AUTHENTICATION_TYPE_SESSION", + id: "11111111-1111-1111-1111-111111111111" // Must use the "colossal session" that has a 15 minute expiration, so that MFA is required every 15 minutes when signing + }, + ] + } + ], + order: 2, +} + +mfaPolicy: { + condition: "true", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_SESSION" }, // All other activities can be satisfied with any session, so users can use the "colossal session" that requires MFA every 15 minutes, or the default read/write session that doesn't require MFA + ] + } + ], + order: 3, +} +``` + +### By-factor login capabilities + +In this example, different login methods grant different levels of access: + +- **SMS OTP login**: grants a session that can do all activities **except** export +- **Passkey login**: grants a session that can do **all activities** including export +- **SMS user wants to export**: must upgrade their session by proving they also have a passkey. The upgraded session lasts 15 minutes, after which they must re-authenticate to export again. + +First, we set up the session profiles on the parent organization: + +``` ts +// SMS basic session (assume uuid: 11111111-1111-1111-1111-111111111111) +// Can do everything except export +sessionProfile: { + name: 'sms-basic-session', + capability: "activity.action != 'EXPORT'", + expirationSeconds: 25200, // 7 hours +} + +// SMS upgraded session (assume uuid: 22222222-2222-2222-2222-222222222222) +// Only used for exporting, expires quickly to force re-authentication +sessionProfile: { + name: 'sms-upgraded-session', + capability: "activity.action == 'EXPORT'", + expirationSeconds: 900, // 15 minutes +} + +// Passkey login session (assume uuid: 33333333-3333-3333-3333-333333333333) +// Full access to all activities +sessionProfile: { + name: 'passkey-login-session', + capability: "true", + expirationSeconds: 25200, // 7 hours +} +``` + +Then, we set up the MFA policies on the sub-organization: + +``` ts +// SMS basic login: requires SMS OTP to get the basic session +mfaPolicy: { + condition: "activity.action == 'AUTH' && activity.params.session_profile_id == '11111111-1111-1111-1111-111111111111'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_SMS_OTP" }, + ] + } + ], + order: 0, +} + +// Passkey login: requires passkey to get the passkey session +mfaPolicy: { + condition: "activity.action == 'AUTH' && activity.params.session_profile_id == '33333333-3333-3333-3333-333333333333'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 1, +} + +// SMS upgrade: requires the existing SMS basic session AND a passkey to get the upgraded export session +mfaPolicy: { + condition: "activity.action == 'AUTH' && activity.params.session_profile_id == '22222222-2222-2222-2222-222222222222'", + requiredAuthenticationMethods: [ + { + any: [ + { + type: "AUTHENTICATION_TYPE_SESSION", + id: "11111111-1111-1111-1111-111111111111", // Must use the SMS basic session + }, + ] + }, + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 2, +} + +// Export: requires either the upgraded SMS session or the passkey session +mfaPolicy: { + condition: "activity.action == 'EXPORT'", + requiredAuthenticationMethods: [ + { + any: [ + { + type: "AUTHENTICATION_TYPE_SESSION", + id: "22222222-2222-2222-2222-222222222222", // SMS upgraded session + }, + { + type: "AUTHENTICATION_TYPE_SESSION", + id: "33333333-3333-3333-3333-333333333333", // Passkey session (already has full access) + }, + ] + } + ], + order: 3, +} + +// Everything else: requires any session +mfaPolicy: { + condition: "true", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_SESSION" }, + ] + } + ], + order: 4, +} +``` + +### Explicit downgrade + +In this example, users log in with SMS OTP and receive a safe session that allows all activities **except** signing. To sign, they must upgrade to a signing session by proving they have a passkey. The signing session lasts 15 minutes, after which the user falls back to the safe session. In the UX, the user can also explicitly "downgrade" back to the safe session at any time by simply discarding the signing session. + +- **SMS OTP login**: grants a safe session that can do all activities except sign +- **User wants to sign**: uses the safe session and a passkey to get a signing session that can only be used for signing activities. The signing session lasts 15 minutes. +- **Explicit downgrade**: user discards the signing session in the UX and switches back to the safe session. No Turnkey API call is needed - the app simply stops using the signing session. +- **Automatic downgrade**: after 15 minutes, the signing session expires. Any signing attempts will require the user to go through the upgrade flow again. + +First, we set up the session profiles on the parent organization: + +``` ts +// SMS safe session (assume uuid: 11111111-1111-1111-1111-111111111111) +// Can do everything except sign +sessionProfile: { + name: 'sms-safe-session', + capability: "activity.action != 'SIGN'", + expirationSeconds: 25200, // 7 hours +} + +// SMS signing session (assume uuid: 22222222-2222-2222-2222-222222222222) +// Used for signing, expires quickly +sessionProfile: { + name: 'sms-signing-session', + capability: "true", + expirationSeconds: 900, // 15 minutes +} +``` + +Then, we set up the MFA policies on the sub-organization: + +``` ts +// SMS safe login: requires SMS OTP to get the safe session +mfaPolicy: { + condition: "activity.action == 'AUTH' && activity.params.session_profile_id == '11111111-1111-1111-1111-111111111111'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_SMS_OTP" }, + ] + } + ], + order: 0, +} + +// SMS signing session creation: requires the existing safe session AND a passkey +mfaPolicy: { + condition: "activity.action == 'AUTH' && activity.params.session_profile_id == '22222222-2222-2222-2222-222222222222'", + requiredAuthenticationMethods: [ + { + any: [ + { + type: "AUTHENTICATION_TYPE_SESSION", + id: "11111111-1111-1111-1111-111111111111", // Must use the SMS safe session + }, + ] + }, + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 1, +} + +// Sign action: requires the signing session +mfaPolicy: { + condition: "activity.action == 'SIGN'", + requiredAuthenticationMethods: [ + { + any: [ + { + type: "AUTHENTICATION_TYPE_SESSION", + id: "22222222-2222-2222-2222-222222222222", // Must use the signing session + }, + ] + } + ], + order: 2, +} + +// Everything else: requires any session +mfaPolicy: { + condition: "true", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_SESSION" }, + ] + } + ], + order: 3, +} +``` + +### Enforcing MFA via delegated access + +In this example, a parent organization enforces MFA on an end-user's sub-organization using a [delegated access user](../../concepts/policies/delegated-access-overview). The delegated access user is controlled by the parent org and has a narrowly scoped policy that only allows it to manage MFA policies. + +- **Parent org** creates a sub-organization with a root user +- **Sub-org root user** creates a delegated access user whose API key is controlled by the parent org +- **Sub-org root user** assigns a policy to the delegated access user that only allows MFA policy management +- **Delegated access user** creates an MFA policy requiring the end user to authenticate with a passkey for all signing activities + +First, the sub-org root user creates a policy for the delegated access user: + +``` ts +// Policy for the delegated access user +policy: { + policyName: "Allow MFA policy management", + effect: "EFFECT_ALLOW", + condition: "activity.resource == 'MFA_POLICY' && activity.action == 'CREATE'", // Only allow creating MFA policies to prevent the delegated access user from doing anything else + notes: "Allows the delegated access user to create MFA policies", +} +``` + +Then, the delegated access user (controlled by the parent org) creates an MFA policy for the sub-org root user: + +``` ts +// MFA policy created by the delegated access user +mfaPolicy: { + userId: "", + mfaPolicyName: "Require passkey for signing", + condition: "activity.action == 'SIGN'", + requiredAuthenticationMethods: [ + { + any: [ + { type: "AUTHENTICATION_TYPE_PASSKEY" }, + ] + } + ], + order: 0, +} +``` + +### Quorum-based MFA recovery via delegated access + +In this example, a parent organization sets up a recovery mechanism using two [delegated access users](../../concepts/policies/delegated-access-overview). Both must approve before an MFA policy can be deleted, preventing any single party from removing a user's MFA protections. See [MFA Recovery](./enforcement-and-recovery#mfa-recovery) for more details. + +- **Sub-org root user** creates two delegated access users, each with an API key controlled by a different party in the parent org +- **Sub-org root user** assigns a policy requiring both delegated users to approve MFA policy deletions +- When the end user is locked out, **both** delegated access users must approve the `DeleteMfaPolicy` activity + +The sub-org root user creates a consensus policy for recovery: + +``` ts +// Policy for the delegated access users - requires both to approve +policy: { + policyName: "Quorum MFA recovery", + effect: "EFFECT_ALLOW", + condition: "activity.resource == 'MFA_POLICY' && activity.action == 'DELETE'", // Only allow deleting MFA policies to prevent the delegated access users from doing anything else + consensus: "approvers.count() >= 2", + notes: "Requires both delegated access users to approve before an MFA policy can be deleted", +} +``` + +To recover a locked-out user, the first delegated access user proposes the deletion: + +``` ts +// First delegated access user proposes deleting the MFA policy +deleteMfaPolicy: { + userId: "", + mfaPolicyId: "", +} +// Activity is returned with ACTIVITY_STATUS_CONSENSUS_NEEDED +``` + +Then the second delegated access user approves: + +``` ts +// Second delegated access user approves the deletion +approveActivity: { + fingerprint: "", +} +// Activity completes - MFA policy is deleted, user is no longer locked out +``` diff --git a/authentication/mfa/overview.mdx b/authentication/mfa/overview.mdx new file mode 100644 index 00000000..5fdf592c --- /dev/null +++ b/authentication/mfa/overview.mdx @@ -0,0 +1,121 @@ +--- +title: "Multi-Factor Authentication (MFA)" +description: "Learn about multi-factor authentication (MFA) in Turnkey and how to use MFA policies to require multiple forms of authentication for specific activities." +sidebarTitle: "Overview" +--- + +## What is multi-factor authentication (MFA)? + +Requests made to Turnkey's public API are required to be authenticated using [an API key or WebAuthn stamp](/developer-reference/api-overview/stamps). MFA adds an additional layer of security by requiring users to provide multiple forms of authentication before they can perform specific activities. + +MFA is highly customizable and can be configured to require any combination of supported authentication methods for different activities. + +## MFA policies +MFA policies are a unique resource type in Turnkey that allows configuation of authentication requirements, scoped to a specific condition. + +MFA policies can be created by parent, or sub-organization users and are tied to the targetted user. + +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](/concepts/policies/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 array of authentication methods that are required when the policy condition is met. Methods can be logically "OR-ed" together by grouping them in nested "ANY" blocks. +- `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 + +### Condition + +The `condition` field is a string written in Turnkey's [policy language](/concepts/policies/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](/concepts/policies/language#keywords). + +**Examples:** + +``` ts +// Require MFA for all signing activities +activity.action == 'SIGN' + +// Require MFA for high-value Ethereum transfers (value > 1 ETH in wei) +activity.action == 'SIGN' && eth.tx.value > 1000000000000000000 + +// Require MFA for everything +true +``` + +You can find more examples [here](./examples). + +### 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 **sequential step** - all steps must be satisfied in order. +- Within each step, the `any` array contains one or more authentication methods. If multiple methods are listed, the user must satisfy **any one of them** (logical OR). + +```json +{ + "requiredAuthenticationMethods": [ + { + "any": [ + { "type": "AUTHENTICATION_TYPE_API_KEY" } + ] + }, + { + "any": [ + { "type": "AUTHENTICATION_TYPE_PASSKEY" }, + { "type": "AUTHENTICATION_TYPE_EMAIL_OTP" } + ] + } + ] +} +``` + +In the example above, the user must: +1. Authenticate with an API key +2. Authenticate with **either** a passkey **or** email OTP + +You can find more examples [here](./examples). + +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. + +### Evaluation order + +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**. + +When a request is made, each MFA policy is checked in order. The first policy whose condition evaluates to `true` determines the authentication requirements for that request. This allows you to create specific policies for sensitive operations with lower order values, and broader catch-all policies with higher order values. + +```json +// Order 1: Strict MFA for signing - evaluated first +{ + "condition": "activity.action == 'SIGN'", + "order": 1, + "requiredAuthenticationMethods": [ + { "any": [{ "type": "AUTHENTICATION_TYPE_PASSKEY" }] }, + { "any": [{ "type": "AUTHENTICATION_TYPE_EMAIL_OTP" }] } + ] +} + +// Order 2: Lighter MFA for everything else - evaluated second +{ + "condition": "true", + "order": 2, + "requiredAuthenticationMethods": [ + { "any": [{ "type": "AUTHENTICATION_TYPE_PASSKEY" }, { "type": "AUTHENTICATION_TYPE_API_KEY" }] } + ] +} +``` \ No newline at end of file diff --git a/authentication/mfa/satsifying-mfa.mdx b/authentication/mfa/satsifying-mfa.mdx new file mode 100644 index 00000000..4b0d054b --- /dev/null +++ b/authentication/mfa/satsifying-mfa.mdx @@ -0,0 +1,83 @@ +--- +title: "Satisfying MFA" +description: "Learn how to satisfy multi-factor authentication (MFA) policies in Turnkey by proving ownership of required authentication methods." +--- + +When a user submits an activity and one of their MFA policies evaluates to `true`, the activity is placed in `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` status. The activity will not execute until the user proves they have the required authentication methods. + +To satisfy MFA, the user must call the `APPROVE_ACTIVITY` activity, passing in the `fingerprint` of the original activity. The way the user stamps this approval request determines which authentication method is proven. + +You can learn more about stamps [here](/developer-reference/api-overview/stamps). + +## API key + +To prove API key authentication, the user stamps the `APPROVE_ACTIVITY` request with an API key. + +If the MFA policy specifies an `id`, the user must stamp with that specific API key. + +## Passkey + +To prove passkey authentication, the user stamps the `APPROVE_ACTIVITY` request with a WebAuthn authenticator. + +If the MFA policy specifies an `id`, the user must stamp with that specific authenticator. + +## Session + +To prove session authentication, the user stamps the `APPROVE_ACTIVITY` request with a session credential. A session credential is an API key that was classified as a session after a login activity (e.g., `STAMP_LOGIN`, `OTP_LOGIN`). + +If the MFA policy specifies an `id` for a session authentication method, the `id` refers to a [session profile](../sessions/session-profiles) ID. The user must stamp with a session credential that was issued with that specific session profile. + + + +## Email OTP, SMS OTP, and OAuth + +TODO (Amir/Moe): Talk about token stamps and link to docs + +## MFA and consensus + +MFA works alongside Turnkey's [consensus](/concepts/users/root-quorum) system for activities that require approval from multiple users. + +When an activity requires both MFA and consensus: + +1. **The proposing user must satisfy their own MFA first.** If the proposer has an MFA policy that matches the activity, the activity is returned with `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED`. The proposer must satisfy their MFA requirements before the activity can proceed to consensus. +2. **Subsequent approvers vote on the activity as normal.** Once the proposer's MFA is satisfied, other users in the quorum can approve or reject the activity. +3. **Approving users must also satisfy their own MFA.** If an approving user has an MFA policy that matches the activity, they will receive `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` when they attempt to vote. They must satisfy their own MFA requirements before their vote is recorded. +4. **The activity executes only after all required users have satisfied MFA and consensus is met.** + +This ensures that every user involved in an activity is individually held to their own MFA requirements, regardless of whether they are the proposer or an approver. + +```mermaid +sequenceDiagram + participant P as Proposer + participant T as Turnkey + participant A as Approver + + P->>T: Submit activity (stamped with credential) + T->>T: Evaluate MFA policies + T->>T: Evaluate policies → ALLOW + + alt Proposer has matching MFA policy + T->>P: ACTIVITY_STATUS_AUTHENTICATORS_NEEDED + loop For each required authentication step + P->>T: Approve activity with fingerprint
(stamped with required credential) + T->>T: Verify authentication method + end + end + + alt Consensus required + T->>P: ACTIVITY_STATUS_CONSENSUS_NEEDED + A->>T: Approve activity with fingerprint + T->>T: Evaluate MFA policies + + alt Approver has matching MFA policy + T->>A: ACTIVITY_STATUS_AUTHENTICATORS_NEEDED + loop For each required authentication step + A->>T: Approve activity with fingerprint
(stamped with required credential) + T->>T: Verify authentication method + end + end + end + + T->>T: All MFA satisfied + consensus met + T->>T: ACTIVITY_STATUS_COMPLETED +``` diff --git a/authentication/overview.mdx b/authentication/overview.mdx index 0fa413fa..ef090b9c 100644 --- a/authentication/overview.mdx +++ b/authentication/overview.mdx @@ -36,7 +36,7 @@ All of Turnkey's authentication methods create a common user object, where you c Once a user successfully authenticates with Turnkey, Turnkey creates a session for that user that your app can use to represent an authenticated session or to make authenticated requests to your backend. -For information about managing authenticated sessions, see our [Sessions](/authentication/sessions) documentation. +For information about managing authenticated sessions, see our [Sessions](/authentication/sessions/overview) documentation. ## Related resources @@ -63,7 +63,7 @@ For information about managing authenticated sessions, see our [Sessions](/authe OAuth authentication with popular social providers like Google, Apple, and Twitter. - + Manage authenticated user sessions and access tokens in your application. diff --git a/authentication/sessions.mdx b/authentication/sessions/overview.mdx similarity index 96% rename from authentication/sessions.mdx rename to authentication/sessions/overview.mdx index 2f99afa5..36431922 100644 --- a/authentication/sessions.mdx +++ b/authentication/sessions/overview.mdx @@ -1,6 +1,7 @@ --- title: "Sessions" description: "Turnkey sessions allow a user to take multiple, contiguous actions in a defined period of time." +sidebarTitle: "Overview" --- ## What is a session? @@ -46,6 +47,11 @@ Our SDK contains several abstractions that manage authentication. You can checko **Note:** The session JWT is only metadata signed by Turnkey that references the client side stored API keypair, and is useful for verifying the session server-side or associating metadata, but it cannot be used to authenticate requests to Turnkey’s API. Only the session keypair can be used to create valid `x-stamp` signatures for API requests to Turnkey. In other words, solely the session JWT cannot be used to stamp requests outside of the client context. + +### Scoped session permissions + +You can create a session that is scoped to only allow certain activities or resources. This is done by using a **session profile** and assigning it to a session at login time. You can learn more about session profiles [here](./session-profiles). + ### Mechanisms There are two primary mechanisms we offer that provide client side key generation and signing to support read-write sessions. diff --git a/authentication/sessions/session-profiles.mdx b/authentication/sessions/session-profiles.mdx new file mode 100644 index 00000000..cfc97377 --- /dev/null +++ b/authentication/sessions/session-profiles.mdx @@ -0,0 +1,82 @@ +--- +title: "Session Profiles" +description: "Learn how to use session profiles to issue sessions with limited capabilities." +--- + +## What are session profiles? + +Session profiles are resources created by the parent organization that allow sessions to be issued with limited capabilities. When a session is issued with a session profile, the profile's capability is evaluated on every request made with that session. If the capability evaluates to `false`, the request is denied. + +Session profiles are **immutable** - once created, they cannot be edited or deleted. Make sure the configuration is correct before creating a session profile. + +Session profiles can be created using the `CreateSessionProfile` activity, via the public API or the [Turnkey dashboard](https://app.turnkey.com). + +## Session profile structure + +The `CreateSessionProfile` activity has the following parameters: + +- `sessionProfileName` (required): A human-readable name for the session profile. This name will also be used as the `session_type` in the resulting [session JWT](./overview#creating-a-read-write-session). +- `capability` (required): A string of [policy language](/concepts/policies/language) that is evaluated on every request made with this session. If the capability evaluates to `true`, the request is allowed. If it evaluates to `false`, the request is denied. +- `expirationSeconds` (optional): The maximum duration in seconds for sessions created with this profile. If not set, the expiration is determined by the value passed into the login activity intent. +- `notes` (optional): Notes for the session profile. + +### Capability + +The `capability` field uses the same [policy language](/concepts/policies/language) as policy conditions and MFA conditions. It has access to the same keywords, including `activity.type`, `activity.action`, `eth.tx`, and others. + +**Examples:** + +``` ts +// Allow all activities +true + +// Only allow signing activities +activity.action == 'SIGN' + +// Allow everything except exporting +activity.action != 'EXPORT' + +// Only allow signing with a specific wallet +activity.action == 'SIGN' && wallet.id == '11111111-1111-1111-1111-111111111111' +``` + +### Expiration + +The final session expiration is determined by taking the **minimum** of the login intent's expiration and the session profile's expiration: + +- If **both** are set: the shorter of the two is used +- If **only the intent expiration** is set: the intent expiration is used +- If **only the profile expiration** is set: the profile expiration is used +- If **neither** is set: a default expiration is used (900 seconds / 15 minutes) + +This ensures that a session profile's expiration acts as a ceiling - the login intent can request a shorter session, but never a longer one than the profile allows. + +## Issuing sessions with a session profile + +To issue a session with a session profile, pass the `sessionProfileId` into any login activity: + +- `STAMP_LOGIN` +- `OTP_LOGIN` +- `OAUTH_LOGIN` + +{/* TODO (Amir/Moe): if the jwt looks different at some point. CHANGE THIS! */} +The resulting session JWT will include the following claims, unique to sessions issued with a session profile: +- `session_profile_id`: the ID of the session profile +- `capability`: the capability string from the profile +- `session_type`: set to the session profile's name (instead of the default `SESSION_TYPE_READ_WRITE`) + +If no `sessionProfileId` is passed, the session is issued as a default read-write session with no capability restrictions. + +## Querying session profiles + +Session profiles can be queried using: +- `GetSessionProfile`: retrieve a single session profile by ID +- `GetSessionProfiles`: list all session profiles for an organization + +Sub-organizations can see session profiles created by their parent organization as well as their own. + +## Using session profiles with MFA + +Session profiles are commonly used alongside [MFA policies](../mfa/overview) to create tiered authentication flows. For example, you can require MFA to obtain a session with elevated capabilities, while allowing a basic session without MFA. + +See [Satisfying MFA](../mfa/satsifying-mfa#session) for how sessions interact with MFA authentication methods, and the [MFA examples](../mfa/examples) for complete configurations that combine session profiles with MFA policies. \ No newline at end of file diff --git a/docs.json b/docs.json index a7ea0f06..9fcb3b76 100644 --- a/docs.json +++ b/docs.json @@ -138,12 +138,26 @@ "authentication/backend-setup", "authentication/proxying-signed-requests", "authentication/credentials", - "authentication/mfa" + { + "group": "Sessions", + "pages": [ + "authentication/sessions/overview", + "authentication/sessions/session-profiles" + ] + }, + { + "group": "Multi-factor authentication (MFA)", + "pages": [ + "authentication/mfa/overview", + "authentication/mfa/satsifying-mfa", + "authentication/mfa/enforcement-and-recovery", + "authentication/mfa/examples" + ] + } ] }, "products/embedded-wallets/features/multi-chain-support", "products/embedded-wallets/features/fiat-on-ramp", - "authentication/sessions", { "group": "Wallets", "pages": [ diff --git a/embedded-wallets/code-examples/embedded-consumer-wallet.mdx b/embedded-wallets/code-examples/embedded-consumer-wallet.mdx index 0b04d528..26a922f9 100644 --- a/embedded-wallets/code-examples/embedded-consumer-wallet.mdx +++ b/embedded-wallets/code-examples/embedded-consumer-wallet.mdx @@ -81,7 +81,7 @@ Choose how users stay authorized and where session credentials live: JavaScript; SecureStorage (mobile); or LocalStorage (keys in app-accessible storage). - **Session duration:** Default: 15 minutes (configurable via `expirationSeconds`). -See [Sessions](/authentication/sessions) for mechanisms, refresh, and FAQ, and the +See [Sessions](/authentication/sessions/overview) for mechanisms, refresh, and FAQ, and the [Embedded Wallets Quickstart](/sdks/react/index) for configuration. ### Wallet architecture diff --git a/embedded-wallets/features/overview.mdx b/embedded-wallets/features/overview.mdx index af174bf6..509f24f5 100644 --- a/embedded-wallets/features/overview.mdx +++ b/embedded-wallets/features/overview.mdx @@ -34,7 +34,7 @@ sidebarTitle: "Overview" {" "} - + diff --git a/production-checklist/embedded-wallet.mdx b/production-checklist/embedded-wallet.mdx index 92a98068..fc2cc301 100644 --- a/production-checklist/embedded-wallet.mdx +++ b/production-checklist/embedded-wallet.mdx @@ -101,7 +101,7 @@ Duration Session credentials are time-bound and expire after the duration you specify. This is set via the expirationSeconds parameter when the session is created. The default is 900 seconds (15 minutes), but you can configure this to suit your needs. -To read more about sessions and how to manage them, head to the [Sessions Overview page](https://docs.turnkey.com/authentication/sessions#sessions). +To read more about sessions and how to manage them, head to the [Sessions Overview page](https://docs.turnkey.com/authentication/sessions/overview#sessions). ## Wallet type: decide between smart contract and key-based wallets diff --git a/sdks/react/advanced-backend-authentication.mdx b/sdks/react/advanced-backend-authentication.mdx index 62b82980..f6b28b93 100644 --- a/sdks/react/advanced-backend-authentication.mdx +++ b/sdks/react/advanced-backend-authentication.mdx @@ -163,11 +163,11 @@ const otpLogin = async (verificationToken: string) => { }; ``` -The private key generated from `createApiKeyPair` will be automatically stored in [`indexedDB`](/authentication/sessions#indexeddb-web-only-%3A) and used for stamping requests to Turnkey after authentication. You can learn more about stamps [here](/developer-reference/api-overview/stamps). +The private key generated from `createApiKeyPair` will be automatically stored in [`indexedDB`](/authentication/sessions/overview#indexeddb-web-only-%3A) and used for stamping requests to Turnkey after authentication. You can learn more about stamps [here](/developer-reference/api-overview/stamps). ### Storing the session -Login endpoints like `otpLogin` and `oauthLogin` will [return a session token in JWT format](/authentication/sessions#creating-a-read-write-session) that you need to store in your application. You can use the `storeSession` function from the `useTurnkey` hook to store the session token. +Login endpoints like `otpLogin` and `oauthLogin` will [return a session token in JWT format](/authentication/sessions/overview#creating-a-read-write-session) that you need to store in your application. You can use the `storeSession` function from the `useTurnkey` hook to store the session token. ```tsx import { useTurnkey } from "@turnkey/react-wallet-kit"; diff --git a/sdks/typescript-frontend/advanced-backend-authentication.mdx b/sdks/typescript-frontend/advanced-backend-authentication.mdx index 4323e144..418584de 100644 --- a/sdks/typescript-frontend/advanced-backend-authentication.mdx +++ b/sdks/typescript-frontend/advanced-backend-authentication.mdx @@ -134,11 +134,11 @@ const otpLogin = async (verificationToken: string) => { }; ``` -The private key generated from `createApiKeyPair` will be automatically stored in [indexedDB](/authentication/sessions#indexeddb-web-only-%3A) on web environments or [secure storage](/authentication/sessions#securestorage-mobile-only) on React Native and used for stamping requests to Turnkey after authentication. You can learn more about stamps [here](/developer-reference/api-overview/stamps). +The private key generated from `createApiKeyPair` will be automatically stored in [indexedDB](/authentication/sessions/overview#indexeddb-web-only-%3A) on web environments or [secure storage](/authentication/sessions/overview#securestorage-mobile-only) on React Native and used for stamping requests to Turnkey after authentication. You can learn more about stamps [here](/developer-reference/api-overview/stamps). ### Storing the session -Login endpoints like `otpLogin` and `oauthLogin` will [return a session token in JWT format](/authentication/sessions#creating-a-read-write-session) that you need to store in your application. You can use the `storeSession` function from the `TurnkeyClient` to store the session token. +Login endpoints like `otpLogin` and `oauthLogin` will [return a session token in JWT format](/authentication/sessions/overview#creating-a-read-write-session) that you need to store in your application. You can use the `storeSession` function from the `TurnkeyClient` to store the session token. ```tsx const otpLogin = async (verificationToken: string) => { From 8171f7019c321ed670ee6d97bcc5550a8c879e15 Mon Sep 17 00:00:00 2001 From: Amir Cheikh Date: Thu, 16 Apr 2026 17:23:24 -0400 Subject: [PATCH 3/4] Feedback --- .../mfa/enforcement-and-recovery.mdx | 6 +++--- authentication/mfa/overview.mdx | 14 ++++++++------ authentication/mfa/satsifying-mfa.mdx | 19 ++++++++++++++----- authentication/sessions/session-profiles.mdx | 2 +- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/authentication/mfa/enforcement-and-recovery.mdx b/authentication/mfa/enforcement-and-recovery.mdx index 2afb3dac..76bab258 100644 --- a/authentication/mfa/enforcement-and-recovery.mdx +++ b/authentication/mfa/enforcement-and-recovery.mdx @@ -6,7 +6,7 @@ sidebarTitle: "Enforcement and Recovery" ## Enforcing MFA for end users -Enforcing MFA for an end-user on a sub-organization can do so using a [delegated access user](/concepts/policies/delegated-access-overview). +Enforcing MFA for an end user on a sub-organization can be done using a [delegated access user](/concepts/policies/delegated-access-overview). A delegated access user is a non-root user created in the sub-organization whose API key is controlled by the parent organization and has carefully scoped permissions to perform only specific actions. @@ -37,9 +37,9 @@ Once this is in place, the delegated access user (controlled by the parent org) ## MFA recovery -If an end user loses access to one of their authentication methods, they may be locked out of activities that require MFA. Because Turnkey cannot write to organizations directly, Turnkey is unable to recover users on their behalf. You must set up a recovery mechanism in advance. +If an end user loses access to one of their authentication methods, they may be unable to complete activities that require MFA. Because Turnkey cannot write to organizations directly, Turnkey is unable to recover users on their behalf. **You must set up a recovery mechanism in advance.** -The recommended approach is to use [delegated access users](/concepts/policies/delegated-access-overview) to delete the MFA policy that is locking the user out. The delegated access user's policy should be scoped to allow deletion of MFA policies: +The recommended approach is to use [delegated access users](/concepts/policies/delegated-access-overview) to delete the MFA policy that is locking the user out. The delegated access user must have permission to delete MFA policies: ``` ts // Policy condition: only allow deleting MFA policies diff --git a/authentication/mfa/overview.mdx b/authentication/mfa/overview.mdx index 5fdf592c..e769dbc6 100644 --- a/authentication/mfa/overview.mdx +++ b/authentication/mfa/overview.mdx @@ -6,14 +6,14 @@ sidebarTitle: "Overview" ## What is multi-factor authentication (MFA)? -Requests made to Turnkey's public API are required to be authenticated using [an API key or WebAuthn stamp](/developer-reference/api-overview/stamps). MFA adds an additional layer of security by requiring users to provide multiple forms of authentication before they can perform specific activities. +Requests made to Turnkey's public API are required to be authenticated using [an API key or WebAuthn stamp](/developer-reference/api-overview/stamps). 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](../sessions/session-profiles). -MFA is highly customizable and can be configured to require any combination of supported authentication methods for different activities. +Turnkey's MFA policies can be configured to require any combination of supported authentication methods. ## MFA policies -MFA policies are a unique resource type in Turnkey that allows configuation of authentication requirements, scoped to a specific condition. +MFA policies are a unique resource type in Turnkey that allows configuration of authentication requirements, scoped to a specific condition. -MFA policies can be created by parent, or sub-organization users and are tied to the targetted user. +MFA policies can be configured in both parent and sub-organizations, and are enforced at the user level rather than the organization level. Once an MFA policy is created, it is immediately enforced for the user. @@ -23,7 +23,7 @@ An MFA policy can be created using the `CreateMfaPolicy` activity and passing in - `userId`: The ID of the user the policy applies to - `mfaPolicyName`: The name of the MFA policy - `condition`: A string of [policy language](/concepts/policies/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 array of authentication methods that are required when the policy condition is met. Methods can be logically "OR-ed" together by grouping them in nested "ANY" blocks. +- `requiredAuthenticationMethods`: An array of authentication methods that are required when the policy condition is met. Grouping methods within nested ANY blocks enables logical OR behavior. - `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 @@ -97,7 +97,9 @@ Each `AuthenticationMethodParams` accepts: 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**. -When a request is made, each MFA policy is checked in order. The first policy whose condition evaluates to `true` determines the authentication requirements for that request. This allows you to create specific policies for sensitive operations with lower order values, and broader catch-all policies with higher order values. +When a request is made, Turnkey evaluates a user's MFA policies in order (from lowest `order` to highest). **The first policy whose condition evaluates to `true` is the one that applies**: its `requiredAuthenticationMethods` define the authentication requirements for that request, and later policies are not considered. + +This lets you place narrowly-scoped, high-sensitivity policies first, and keep broader “catch-all” policies with higher order values as a fallback. ```json // Order 1: Strict MFA for signing - evaluated first diff --git a/authentication/mfa/satsifying-mfa.mdx b/authentication/mfa/satsifying-mfa.mdx index 4b0d054b..aed6892f 100644 --- a/authentication/mfa/satsifying-mfa.mdx +++ b/authentication/mfa/satsifying-mfa.mdx @@ -1,11 +1,20 @@ --- title: "Satisfying MFA" -description: "Learn how to satisfy multi-factor authentication (MFA) policies in Turnkey by proving ownership of required authentication methods." +description: "Learn how a user can satisfy multi-factor authentication (MFA) policies by proving control of the required authentication methods." --- -When a user submits an activity and one of their MFA policies evaluates to `true`, the activity is placed in `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` status. The activity will not execute until the user proves they have the required authentication methods. +When a user submits an activity and an MFA policy evaluates to `true`, the activity enters `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` status. The activity will not execute until the user satisfies the authentication challenges. -To satisfy MFA, the user must call the `APPROVE_ACTIVITY` activity, passing in the `fingerprint` of the original activity. The way the user stamps this approval request determines which authentication method is proven. +The user must call the `APPROVE_ACTIVITY` activity, passing in the `fingerprint` of the original activity: + +```ts +// This endpoint can be found in any of Turnkey's SDKs, within a client access point or provider. +approveActivity({ + fingerprint: "", +}); +``` + +The credential used to stamp this approval request determines which authentication method is being proven. You can learn more about stamps [here](/developer-reference/api-overview/stamps). @@ -39,9 +48,9 @@ MFA works alongside Turnkey's [consensus](/concepts/users/root-quorum) system fo When an activity requires both MFA and consensus: -1. **The proposing user must satisfy their own MFA first.** If the proposer has an MFA policy that matches the activity, the activity is returned with `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED`. The proposer must satisfy their MFA requirements before the activity can proceed to consensus. +1. **The activity initiator must satisfy their own MFA requirements first.** If the proposer has an MFA policy that matches the activity, the activity is returned with `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED`. The proposer must satisfy their MFA requirements before the activity can proceed to consensus. 2. **Subsequent approvers vote on the activity as normal.** Once the proposer's MFA is satisfied, other users in the quorum can approve or reject the activity. -3. **Approving users must also satisfy their own MFA.** If an approving user has an MFA policy that matches the activity, they will receive `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED` when they attempt to vote. They must satisfy their own MFA requirements before their vote is recorded. +3. **Approving users must also satisfy their own MFA requirements.** If an approver has an MFA policy that matches the activity, their vote will return `ACTIVITY_STATUS_AUTHENTICATORS_NEEDED`. The approver must satisfy their MFA requirements before their vote is recorded. 4. **The activity executes only after all required users have satisfied MFA and consensus is met.** This ensures that every user involved in an activity is individually held to their own MFA requirements, regardless of whether they are the proposer or an approver. diff --git a/authentication/sessions/session-profiles.mdx b/authentication/sessions/session-profiles.mdx index cfc97377..e5d715cb 100644 --- a/authentication/sessions/session-profiles.mdx +++ b/authentication/sessions/session-profiles.mdx @@ -7,7 +7,7 @@ description: "Learn how to use session profiles to issue sessions with limited c Session profiles are resources created by the parent organization that allow sessions to be issued with limited capabilities. When a session is issued with a session profile, the profile's capability is evaluated on every request made with that session. If the capability evaluates to `false`, the request is denied. -Session profiles are **immutable** - once created, they cannot be edited or deleted. Make sure the configuration is correct before creating a session profile. +Session profiles are **immutable** - once created, they cannot be edited or deleted. If you need to change a session profile, you must create a new one and update your login flows to use the new profile. Session profiles can be created using the `CreateSessionProfile` activity, via the public API or the [Turnkey dashboard](https://app.turnkey.com). From 3c392a704c41e97895b0740d22d54958caac71d6 Mon Sep 17 00:00:00 2001 From: Amir Cheikh Date: Tue, 21 Apr 2026 15:20:46 -0400 Subject: [PATCH 4/4] Legal feedback --- authentication/mfa/enforcement-and-recovery.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/authentication/mfa/enforcement-and-recovery.mdx b/authentication/mfa/enforcement-and-recovery.mdx index 76bab258..1a935b8d 100644 --- a/authentication/mfa/enforcement-and-recovery.mdx +++ b/authentication/mfa/enforcement-and-recovery.mdx @@ -37,9 +37,9 @@ Once this is in place, the delegated access user (controlled by the parent org) ## MFA recovery -If an end user loses access to one of their authentication methods, they may be unable to complete activities that require MFA. Because Turnkey cannot write to organizations directly, Turnkey is unable to recover users on their behalf. **You must set up a recovery mechanism in advance.** +If an end user loses access to one of their authentication methods, they may be unable to complete activities that require MFA. Because Turnkey cannot write to organizations directly, Turnkey is unable to recover access for end-users. **Organizations must set up a recovery mechanism in advance.** -The recommended approach is to use [delegated access users](/concepts/policies/delegated-access-overview) to delete the MFA policy that is locking the user out. The delegated access user must have permission to delete MFA policies: +One approach is to use [delegated access users](/concepts/policies/delegated-access-overview) to delete the MFA policy that is locking the user out. The delegated access user must have permission to delete MFA policies: ``` ts // Policy condition: only allow deleting MFA policies @@ -48,7 +48,7 @@ activity.resource == 'MFA_POLICY' && activity.action == 'DELETE' ### Quorum-based recovery -It is strongly recommended that you set up **two or more delegated access users** for MFA recovery, with a consensus policy requiring both to approve before an MFA policy can be deleted. This prevents any single party from removing a user's MFA protections. +It is strongly recommended that your Organization considers setting up **two or more delegated access users** for MFA recovery, with a consensus policy requiring both or more to approve before an MFA policy can be deleted. This prevents any single party from removing a user's MFA protections. To set this up: