diff --git a/docs.json b/docs.json index 6cf53206..005fbcd6 100644 --- a/docs.json +++ b/docs.json @@ -334,7 +334,22 @@ ] }, "features/authentication/auth-proxy", - "features/authentication/sessions", + { + "group": "Sessions", + "pages": [ + "features/authentication/sessions/overview", + "features/authentication/sessions/session-profiles" + ] + }, + { + "group": "Multi-factor authentication (MFA)", + "pages": [ + "features/authentication/mfa/overview", + "features/authentication/mfa/satisfying-mfa", + "features/authentication/mfa/enforcement-and-recovery", + "features/authentication/mfa/examples" + ] + }, { "group": "Advanced", "pages": [ @@ -1037,7 +1052,7 @@ "redirects": [ { "source": "/users/sessions", - "destination": "/features/authentication/sessions", + "destination": "/features/authentication/sessions/overview", "permanent": true }, { @@ -1832,7 +1847,12 @@ }, { "source": "/authentication/sessions", - "destination": "/features/authentication/sessions", + "destination": "/features/authentication/sessions/overview", + "permanent": true + }, + { + "source": "/features/authentication/sessions", + "destination": "/features/authentication/sessions/overview", "permanent": true }, { diff --git a/features/authentication/mfa/enforcement-and-recovery.mdx b/features/authentication/mfa/enforcement-and-recovery.mdx new file mode 100644 index 00000000..23a153c3 --- /dev/null +++ b/features/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 be done using a [delegated access user](/features/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 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.** + +One approach is to use [delegated access users](/features/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 +activity.resource == 'MFA_POLICY' && activity.action == 'DELETE' +``` + +### Quorum-based recovery + +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: + +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/features/authentication/mfa/examples.mdx b/features/authentication/mfa/examples.mdx new file mode 100644 index 00000000..01b33585 --- /dev/null +++ b/features/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](/features/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](/features/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/features/authentication/mfa/overview.mdx b/features/authentication/mfa/overview.mdx new file mode 100644 index 00000000..e502c469 --- /dev/null +++ b/features/authentication/mfa/overview.mdx @@ -0,0 +1,123 @@ +--- +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](/api-reference/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). + +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 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. + +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](/features/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. 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 + +### Condition + +The `condition` field is a string written in Turnkey's [policy language](/features/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](/features/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, 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 +{ + "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" }] } + ] +} +``` diff --git a/features/authentication/mfa/satisfying-mfa.mdx b/features/authentication/mfa/satisfying-mfa.mdx new file mode 100644 index 00000000..bf5596cb --- /dev/null +++ b/features/authentication/mfa/satisfying-mfa.mdx @@ -0,0 +1,92 @@ +--- +title: "Satisfying MFA" +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 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. + +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](/api-reference/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](/features/users/root-quorum) system for activities that require approval from multiple users. + +When an activity requires both MFA and 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 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. + +```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/features/authentication/overview.mdx b/features/authentication/overview.mdx index 2c6c83fd..f2cc78a6 100644 --- a/features/authentication/overview.mdx +++ b/features/authentication/overview.mdx @@ -38,7 +38,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](/features/authentication/sessions) documentation. +For information about managing authenticated sessions, see our [Sessions](/features/authentication/sessions/overview) documentation. ## Related resources @@ -47,5 +47,6 @@ For information about managing authenticated sessions, see our [Sessions](/featu - + + diff --git a/features/authentication/sessions.mdx b/features/authentication/sessions/overview.mdx similarity index 96% rename from features/authentication/sessions.mdx rename to features/authentication/sessions/overview.mdx index 172085bf..e18a4691 100644 --- a/features/authentication/sessions.mdx +++ b/features/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,10 @@ 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/features/authentication/sessions/session-profiles.mdx b/features/authentication/sessions/session-profiles.mdx new file mode 100644 index 00000000..ebe21024 --- /dev/null +++ b/features/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. 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). + +## 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](/features/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](/features/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/satisfying-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. diff --git a/solutions/embedded-wallets/embedded-business-wallets.mdx b/solutions/embedded-wallets/embedded-business-wallets.mdx index 6b8d2fac..d9f6cc20 100644 --- a/solutions/embedded-wallets/embedded-business-wallets.mdx +++ b/solutions/embedded-wallets/embedded-business-wallets.mdx @@ -32,7 +32,7 @@ Turnkey enables developers to build shared business wallets with role-based cont | **Policies and guardrails** | With roles in place, enforce spending limits, require multi-party approval for high-value transactions, restrict payments to allowlisted addresses, or require quorum for policy changes. Optionally let business customers configure their own guardrails. | [Policies](/features/policies/overview), [Delegated Access](/features/policies/delegated-access/overview) | | **Custody model** | Choose who can authorize the enclave to sign: the user only (non-custodial), your application (custodial), or both with scoped permissions (hybrid). Business wallets commonly use hybrid custody with policy-backed controls. | [Custody models](/solutions/embedded-wallets/overview#custody-models) | | **Authentication methods** | Choose user auth methods: Passkeys, OAuth/email, or SMS. You can use the [Auth Proxy](/features/authentication/auth-proxy) for backend-signed OTP/OAuth/signup without your own backend, or wire auth to your app. | [Authentication Overview](/features/authentication/overview), [Auth Proxy](/features/authentication/auth-proxy) | -| **Session management** | Allow a user to take multiple, contiguous actions in a defined period of time. Actions include: Read-write or read-only. | [Sessions](/features/authentication/sessions) | +| **Session management** | Allow a user to take multiple, contiguous actions in a defined period of time. Actions include: Read-write or read-only. | [Sessions](/features/authentication/sessions/overview) | | **Gas sponsorship** | Integrate a gasless UX via sponsored transactions to cover who pays gas and how transactions are broadcast. | [Transaction Management](/features/transaction-management), [Sending sponsored transactions](/features/transaction-management/sending-sponsored-transactions) | | **Key portability** | Determine whether users can import or export keys. | [Import wallets](/features/wallets/import-wallets), [Export wallets](/features/wallets/export-wallets) | | **Recovery flows** | Define how users regain access if they lose their authenticator. Options include email recovery and backup passkeys. | [Email recovery](/features/authentication/email) | diff --git a/solutions/embedded-wallets/embedded-consumer-wallet.mdx b/solutions/embedded-wallets/embedded-consumer-wallet.mdx index e9c1554b..6f53f9ae 100644 --- a/solutions/embedded-wallets/embedded-consumer-wallet.mdx +++ b/solutions/embedded-wallets/embedded-consumer-wallet.mdx @@ -24,7 +24,7 @@ exact user experience you need. | **Custody model** | Choose who can authorize the enclave to sign: the user only (non-custodial), your application (custodial), or both with scoped permissions (hybrid). | [Custody models](/solutions/embedded-wallets/overview#custody-models) | | **Authentication methods** | Choose user auth methods: Passkeys, OAuth/email, or SMS. You can use the [Auth Proxy](/features/authentication/auth-proxy) for backend-signed OTP/OAuth/signup without your own backend, or wire auth to your app. | [Authentication Overview](/features/authentication/overview), [Auth Proxy](/features/authentication/auth-proxy) | | **Policies and guardrails** | Set guardrails for what your end users can do, such as spending limits, allowed destinations, or multi-party approval. Optionally let users configure their own guardrails within your app. | [Policies](/features/policies/overview), [Delegated Access](/features/policies/delegated-access/overview) | -| **Session management** | Allow a user to take multiple, contiguous actions in a defined period of time. Actions include: Read-write or read-only. | [Sessions](/features/authentication/sessions) | +| **Session management** | Allow a user to take multiple, contiguous actions in a defined period of time. Actions include: Read-write or read-only. | [Sessions](/features/authentication/sessions/overview) | | **Wallet architecture** | Choose between key-based (HD) or smart contract wallets for your users. Turnkey supports both. | [Wallets Concept](/features/wallets), [Transaction Management](/features/transaction-management) | | **Gas sponsorship** | Integrate a gasless UX via sponsored transactions to cover who pays gas and how transactions are broadcast. | [Transaction Management](/features/transaction-management), [Sending sponsored transactions](/features/transaction-management/sending-sponsored-transactions) | | **Key portability** | Determine whether users can import or export keys. | [Import wallets](/features/wallets/import-wallets), [Export wallets](/features/wallets/export-wallets) | diff --git a/solutions/embedded-wallets/embedded-waas.mdx b/solutions/embedded-wallets/embedded-waas.mdx index 0d650751..62651305 100644 --- a/solutions/embedded-wallets/embedded-waas.mdx +++ b/solutions/embedded-wallets/embedded-waas.mdx @@ -164,7 +164,7 @@ See [Embedded Wallet Kit](/solutions/embedded-wallets/integration-guide/react/in Integrate the wallet into your onboarding and runtime flows so every downstream integration inherits a working embedded wallet. - **Onboarding:** Handle Turnkey org setup, auth configuration, and the staged sub-org creation flow as part of user registration. The end user should experience passkey registration as a natural part of sign-up. -- **Client initialization:** Initialize the Turnkey client with the user's sub-org context on each session. Use [sessions](/features/authentication/sessions) for batched signing workflows to reduce authentication friction. +- **Client initialization:** Initialize the Turnkey client with the user's sub-org context on each session. Use [sessions](/features/authentication/sessions/overview) for batched signing workflows to reduce authentication friction. - **Transaction flow:** Surface the approval prompt via EWK components, submit the user's approval to Turnkey, run your backend checks, then co-sign or withhold. - **Recovery:** Expose the export flow in your settings UI so users can self-serve wallet recovery. Turnkey's enclave encrypts the mnemonic to a user-generated target key via HPKE. Neither Turnkey nor your platform can view the exported material. diff --git a/solutions/embedded-wallets/integration-guide/overview.mdx b/solutions/embedded-wallets/integration-guide/overview.mdx index cee28c72..19cef8e3 100644 --- a/solutions/embedded-wallets/integration-guide/overview.mdx +++ b/solutions/embedded-wallets/integration-guide/overview.mdx @@ -130,7 +130,7 @@ and renews it automatically when possible. Default session length is 15 minutes, configurable per flow. If a session expires, the SDK surfaces an unauthenticated state and your app prompts re-auth. -See [Sessions](/features/authentication/sessions) for configuration options. +See [Sessions](/features/authentication/sessions/overview) for configuration options. ## What the platform guides cover diff --git a/solutions/embedded-wallets/integration-guide/react/advanced-backend-authentication.mdx b/solutions/embedded-wallets/integration-guide/react/advanced-backend-authentication.mdx index 50613779..4eb307ac 100644 --- a/solutions/embedded-wallets/integration-guide/react/advanced-backend-authentication.mdx +++ b/solutions/embedded-wallets/integration-guide/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`](/features/authentication/sessions#indexeddb-web-only-%3A) and used for stamping requests to Turnkey after authentication. You can learn more about stamps [here](/api-reference/overview/stamps). +The private key generated from `createApiKeyPair` will be automatically stored in [`indexedDB`](/features/authentication/sessions/overview#indexeddb-web-only-%3A) and used for stamping requests to Turnkey after authentication. You can learn more about stamps [here](/api-reference/overview/stamps). ### Storing the session -Login endpoints like `otpLogin` and `oauthLogin` will [return a session token in JWT format](/features/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](/features/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/solutions/embedded-wallets/integration-guide/typescript/advanced-backend-authentication.mdx b/solutions/embedded-wallets/integration-guide/typescript/advanced-backend-authentication.mdx index 4d4bfd6f..41784f80 100644 --- a/solutions/embedded-wallets/integration-guide/typescript/advanced-backend-authentication.mdx +++ b/solutions/embedded-wallets/integration-guide/typescript/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](/features/authentication/sessions#indexeddb-web-only-%3A) on web environments or [secure storage](/features/authentication/sessions#securestorage-mobile-only) on React Native and used for stamping requests to Turnkey after authentication. You can learn more about stamps [here](/api-reference/overview/stamps). +The private key generated from `createApiKeyPair` will be automatically stored in [indexedDB](/features/authentication/sessions/overview#indexeddb-web-only-%3A) on web environments or [secure storage](/features/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](/api-reference/overview/stamps). ### Storing the session -Login endpoints like `otpLogin` and `oauthLogin` will [return a session token in JWT format](/features/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](/features/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) => { diff --git a/welcome.mdx b/welcome.mdx index fbe6e776..ea494b7e 100644 --- a/welcome.mdx +++ b/welcome.mdx @@ -340,7 +340,7 @@ mode: "custom"

Control access