Email Recovery
Email recovery shines if you are leveraging sub-organizations to create embedded wallets for your users. This allows your users to recover their Turnkey account if something goes wrong with their passkeys, and keeps you out of the loop: we engineered this feature to ensure your organization is unable to take over sub-organizations even if it wanted to.
A simple example demonstrating email recovery end-to-end can be found here.
Prerequisites
Make sure you have set up your primary Turnkey organization with at least one API user that can programmatically initiate email recovery on behalf of suborgs. Check out our Quickstart guide if you need help getting started. To allow an API user to initiate email recovery, you'll need the following policy in your main organization:
{
"effect": "EFFECT_ALLOW",
"consensus": "approvers.any(user, user.id == '<YOUR_API_USER_ID>')",
"condition": "activity.resource == 'RECOVERY' && activity.action == 'CREATE'"
}
Helper packages
- We have released open-source code to create target encryption keys, decrypt recovery credentials, and sign Turnkey activities. We've deployed this a static HTML page hosted on
recovery.turnkey.com
meant to be embedded as an iframe element (see the code here). This ensures the recovery credentials are encrypted to keys that your organization doesn't have access to (because they live in the iframe, on a separate domain) - We have also built a package to help you insert this iframe and interact with it in the context of email recovery:
@turnkey/iframe-stamper
In the rest of this guide we'll assume you are using these helpers.
Email Recovery step-by-step
Here's a diagram summarizing the email recovery flow step-by-step (direct link):
Let's review these steps in detail:
User on
yoursite.xyz
clicks "recovery", and a new recovery UI is shown. We recommend this recovery UI be a new hosted page of your site or application, which contains language explaining to the user what steps they will need to take next to complete recovery. While the UI is in a loading state your frontend uses@turnkey/iframe-stamper
to insert a new iframe element:const iframeStamper = new IframeStamper({
iframeUrl: "https://recovery.turnkey.com",
// Configure how the iframe element is inserted on the page
iframeContainerId: "your-container",
iframeElementId: "turnkey-iframe",
});
// Inserts the iframe in the DOM. This creates the new encryption target key
const publicKey = await iframeStamper.init();Your code receives the iframe public key and shows the recovery form, and the user enters their email address.
Your app can now create and sign a new
INIT_USER_EMAIL_RECOVERY
activity with the user email and the iframe public key in the parameters. Note: you'll need to retrieve the sub-organization ID based on the user email.Email is received by the user.
User copies and pastes their recovery code into your app. Remember: this code is an encrypted credential which can only be decrypted within the iframe.
Your app injects the recovery code into the iframe for decryption:
await iframeStamper.injectCredentialBundle(code);
Your app prompts the user to create a new passkey (using our SDK functionality):
// Creates a new passkey
let attestation = await getWebAuthnAttestation(...params...)Your app uses
@turnkey/iframe-stamper
to sign a newRECOVER_USER
activity:// New client instantiated with our iframe stamper
const client = new TurnkeyClient(
{ baseUrl: "https://api.turnkey.com" },
iframeStamper,
);
// Sign and submits the RECOVER_USER activity
const response = await client.recoverUser({
type: "ACTIVITY_TYPE_RECOVER_USER",
timestampMs: String(Date.now()),
organizationId: initRecoveryResponse.organizationId,
parameters: {
userId: initRecoveryResponse.userId,
authenticator: {
authenticatorName: data.authenticatorName,
challenge: base64UrlEncode(challenge),
attestation: attestation,
},
},
});
Once the RECOVER_USER
activity is successfully posted, the recovery is complete! If this activity succeeds, your frontend can redirect to login/sign-in or perform crypto signing with the new passkey.