Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions api-reference/overview/stamps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,40 @@ To create a valid, WebAuthn authenticator stamped request follow these steps:
</Step>
</Steps>

### Attested

Attested stamps allow authenticators that cannot directly sign requests (Email OTP, SMS OTP, and OAuth) to prove identity. Instead of the public key itself being a stored credential, the public key is _attested_ by a token that proves ownership of an identity (an email address, phone number, or OAuth provider).

To create a valid attested stamp:
<Steps>
<Step>
Generate a P256 key pair on the client
</Step>
<Step>
Obtain a token that attests this key pair belongs to the identity. The token must contain the client public key, cryptographically binding the key to the identity:

* For **Email OTP / SMS OTP**: a `verificationToken` issued by Turnkey after a successful OTP verification. The token embeds the client `public_key` alongside the contact (email or phone).
* For **OAuth**: an `oidcToken` from the OAuth provider (e.g., Google). The token's `nonce` must be set to the hash of the client public key.
</Step>
<Step>
Sign the JSON-encoded POST body with the client private key to produce a `signature`
</Step>
<Step>
Create a JSON-encoded stamp:

* `publicKey`: the client public key (hex-encoded)
* `signature`: the signature over the request body (hex-encoded)
* `publicKeyAttestation`: the token (verification token or OIDC token) that ties the public key to the identity
* `scheme`: the attestation scheme. `STAMP_ATTESTED_SCHEME_P256_VERIFICATION_TOKEN` for Email/SMS OTP, or `STAMP_ATTESTED_SCHEME_P256_OIDC` for OAuth
</Step>
<Step>
Attach the JSON-encoded stamp to your request as a `X-Stamp-Attested` header
</Step>
<Step>
Submit the stamped request to Turnkey's API
</Step>
</Steps>

### Stampers

Our [JS SDK](https://github.com/tkhq/sdk) and [CLI](https://github.com/tkhq/tkcli) abstract request stamping for you. If you choose to use an independent client, you will need to implement this yourself. For reference, check out our implementations:
Expand Down
26 changes: 23 additions & 3 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,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": [
Expand Down Expand Up @@ -1056,7 +1071,7 @@
"redirects": [
{
"source": "/users/sessions",
"destination": "/features/authentication/sessions",
"destination": "/features/authentication/sessions/overview",
"permanent": true
},
{
Expand Down Expand Up @@ -1851,7 +1866,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
},
{
Expand Down
49 changes: 49 additions & 0 deletions features/authentication/mfa/enforcement-and-recovery.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
title: "MFA Recovery"
description: "Learn how to set up recovery mechanisms for MFA policies in case users lose access to their authentication methods."
sidebarTitle: "Recovery"
---

## MFA recovery

If a user can no longer satisfy their MFA requirements (e.g., they lose access to their passkey or phone number), they will be unable to perform any activities that trigger that MFA policy. There is no way to bypass MFA once it is enforced.

Most applications that support MFA offer a recovery path through customer support (e.g., "contact support to reset your 2FA"). Turnkey cannot do this because Turnkey has no write access to your organizations. Only authenticators within the organization can modify its state.

By default, there is no MFA recovery mechanism. If your security posture allows it and you want to protect against users locking themselves out, you can set one up using [delegated access users](/features/policies/delegated-access/overview) that have permission to delete MFA policies on behalf of locked-out users.

The delegated access user's policy should be scoped to only allow MFA policy deletion:

``` ts
// Policy condition: only allow deleting MFA policies
activity.resource == 'MFA_POLICY' && activity.action == 'DELETE'
```

With this setup, when a user is locked out, you can verify their identity through your own process and then delete the MFA policy using the DA user to restore access.

### Quorum-based recovery

A single DA user with the ability to delete MFA policies is a single point of failure. If that DA user's API key is compromised, an attacker could remove a user's MFA protections entirely.

To prevent this, you can set up **multiple delegated access users** for MFA recovery with a consensus policy requiring multiple approvals before an MFA policy can be deleted. This way, no single party can unilaterally remove a user's MFA.

<Note>
We recommend having more DA users than required approvals (e.g., 3-4 DA users requiring 2 approvals). If you require all DA users to approve and one loses access, recovery is permanently blocked.
</Note>

To set this up:

1. Create multiple 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: at least 2 delegated users must approve
approvers.count() >= 2
```

With this configuration, deleting an MFA policy requires at least two delegated access users to approve the `DeleteMfaPolicy` activity. Even if one DA user's key is compromised, the attacker cannot remove MFA protections alone. And if one DA user loses access, the remaining users can still reach the approval threshold.
Loading