One-Time Password

  • Uses a 6-9 digit or bech32 alphanumeric one-time password sent via email
  • Simple, and familiar user experience

One-Time Password Sandbox Environment

To test OTP codes in our sandbox environment you can use the following:
  • alphanumeric must be set to false
  • otpLength must be set to 6
  • Email: user@example.com
  • OTP Code: 000000
Credential Bundle
  • Sends an encrypted API key credential directly via email
  • Alternative method for specific use cases
  • More secure, but requires copying the full credential to the client
Both methods provide users with an expiring API key for authentication or account recovery.

Core mechanism

Email Authentication is built with expiring API keys as the foundation. The two delivery mechanisms are:

OTP-based method

The authentication process happens in two steps:
1

A 6-9 digit or alphanumeric OTP code is sent to the user’s verified email address
2

Upon verification of the correct code, an API key credential is generated and encrypted for the client

Credential bundle method

Note: on web, this method is only supported with the legacy iframe-based flow, if no hard requirement to use encrypted bundles, we suggest to use the IndexedDB-based OTP flow instead. The API key credential is encrypted and delivered directly through email to the user. Once the credential is live on the client side (within the context of an iframe), it is readily available to stamp (authenticate) requests. See the enclave to end-user secure channel for more info on how we achieve secure delivery.

Prerequisites

Make sure you have set up your primary Turnkey organization with at least one API user that can programmatically initiate email auth and create suborganizations. Check out our Quickstart guide if you need help getting started. To allow an API user to initiate email auth, 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 == 'AUTH' && activity.action == 'CREATE') || (activity.resource == 'ORGANIZATION' && activity.action == 'CREATE')"
}
For OTP Auth signup and login flows you will need a user with the following policy
{
  "effect": "EFFECT_ALLOW",
  "consensus": "approvers.any(user, user.id == '<YOUR_API_USER_ID>')",
  "condition": "(activity.resource == 'AUTH' && activity.action == 'CREATE') || (activity.resource == 'OTP' && activity.action == 'CREATE') || (activity.resource == 'OTP' && activity.action == 'VERIFY') || (activity.resource == 'ORGANIZATION' && activity.action == 'CREATE')"
}

User experience

OTP-based authentication flow

The flow begins with a new activity of type ACTIVITY_TYPE_INIT_OTP using the parent organization id with these parameters:
  • otpType: specify "OTP_TYPE_EMAIL"
  • contact: user’s email address
  • emailCustomization: optional parameters for customizing emails
  • userIdentifier: optional parameter for rate limiting SMS OTP requests per user. We recommend generating this server-side based on the user’s IP address or public key. See the OTP Rate Limits section below for more details.
  • alphanumeric: optional parameter for making this code bech32 alphanumeric or not. default: true
  • otpLength: optional parameter for selecting the length of the OTP. default: 9
  • expirationSeconds: optional validity window (defaults to 5 minutes)
  • sendFromEmailAddress : optional custom email address from which to send the OTP email
  • sendFromEmailSenderName : optional custom sender name for use with sendFromEmailAddress; if left empty, will default to ‘Notifications’
  • replyToEmailAddress : optional custom email address to use as reply-to
After receiving the OTP, users complete OTP verification with ACTIVITY_TYPE_VERIFY_OTP using the parent organization id which returns a verificationToken JWT:
  • otpId: ID from the init activity
  • otpCode: the 6-9 digit or alphanumeric code received via email
  • expirationSeconds: optional validity window (defaults to 1 hour)
After receiving the verification token, users complete OTP authentication flow with with ACTIVITY_TYPE_OTP_LOGIN using the sub orgazanition ID associated with the contact from the first step:
  • publicKey: public key to add to organization data associated with the signing key in IndexedDB or SecureStorage.
  • verificationToken: JWT returned from successfull VERIFY_OTP activity
  • expirationSeconds: optional validity window (defaults to 15 minutes)
  • invalidateExisting: optional boolean to invalidate previous login sessions
auth otp email

OTP rate limits

In order to safeguard users, Turnkey enforces rate limits for OTP auth activities. If a userIdentifier parameter is provided, the following limits are enforced:
  • 3 requests per 3 minutes per unique userIdentifier
  • 3 retries max per code, after which point that code will be locked
  • 3 active codes per user, each with a 5 minute TTL

Credential bundle authentication flow

This alternative method uses ACTIVITY_TYPE_EMAIL_AUTH with these parameters:
  • email: user’s email address (must match their registered email)
  • targetPublicKey: public key for credential encryption
  • apiKeyName: optional name (defaults to Email Auth - <Timestamp>)
  • expirationSeconds: optional validity window (defaults to 15 minutes)
  • emailCustomization: optional parameters for customizing emails
  • invalidateExisting: optional boolean to invalidate previous Email Auth API keys
auth email

Email customization

We offer customization for the following:
  • appName: the name of the application. This will be used in the email’s subject, e.g. Sign in to ${appName}
  • logoUrl: a link to a PNG with a max width of 340px and max height of 124px
  • magicLinkTemplate: a template for the URL to be used in the magic link button, e.g. https://dapp.xyz/%s. The auth bundle will be interpolated into the %s
// Sign and submits the EMAIL_AUTH activity
const response = await client.emailAuth({
  type: "ACTIVITY_TYPE_EMAIL_AUTH",
  timestampMs: String(Date.now()),
  organizationId: <sub-organization-id>,
  parameters: {
    email: <user-email>,
    targetPublicKey: <iframe-public-key>,
    apiKeyName: <optional-api-key-name>,
    expirationSeconds: <optional-api-key-expiration-in-seconds>,
    emailCustomization: {
      appName: <optional-your-app-name>,
      logoUrl: <optional-your-logo-png>,
      magicLinkTemplate: <optional-magic-link>
    }
  },
});

Email templates

We also support custom HTML email templates for Enterprise clients on the Scale tier. This allows you to inject arbitrary data from a JSON string containing key-value pairs. In this case, the emailCustomization variable may look like:
...
emailCustomization: {
  templateId: <HTML-template-stored-in-turnkey>,
  templateVariables: "{\"username\": \"alice and bob\"}"
}
...
In this specific example, the value alice and bob can be interpolated into the email template using the key username. The use of such template variables is purely optional. Here’s an example of a custom HTML email containing an email auth bundle:
dynamic email auth
example
Bespoke HTML templates and custom email sender domains are available to Enterprise clients on the Scale tier or higher.
If you are interested in implementing bespoke, fully-customized email templates, please reach out to hello@turnkey.com.

Custom email sender domain

Enterprise clients can also customize the email sender domain. To get set up, please reach out to your Turnkey rep to get started but here is what you’ll be able to configure:
// Optional custom email address from which to send the OTP email
"sendFromEmailAddress": "notifs@mail.domain.com"

// Optional custom sender name
"sendFromEmailSenderName": "MyApp Notifications"

// Optional reply-to email address
"replyToEmailAddress": "reply@mail.domain.com"
Please keep in mind that:
  • Email has to be from a pre-whitelisted domain
  • If there is no sendFromEmailAddress or it’s invalid, the other two fields are ignored
  • If sendFromEmailSenderName is absent, it defaults to “Notifications” (again, ONLY if sendFromEmailAddress is present and valid)
  • If replyToEmailAddress is absent, then there is no reply-to added. If it is present, it must ALSO be from a valid, whitelisted domain, but it doesn’t have to be the same email address as the sendFromEmailAddress one (though once again, this first one MUST be present, or the other two feature are ignored)
If you are interested in implementing bespoke, fully-customized email templates and sender domain, please reach out to hello@turnkey.com.

Authorization

Authorization is managed through our policy engine:

Authentication

Both OTP-based and credential bundle authentication activities:
  • Can be performed by root users and users with proper policy authorization
  • Require the respective feature to be enabled in the organization and sub-organization
  • Can target any user in the organization or sub-organizations
Specifically:
  • For OTP-based auth: ACTIVITY_TYPE_INIT_OTP, ACTIVITY_TYPE_VERIFY_OTP and ACTIVITY_TYPE_OTP_LOGIN
  • For credential bundle auth: ACTIVITY_TYPE_EMAIL_AUTH
email auth authorization

Example implementations

Implementation in organizations

For organizations accessed via dashboard:
  1. Ensure the required features are enabled:
    • FEATURE_NAME_OTP_EMAIL_AUTH for OTP-based authentication
    • FEATURE_NAME_EMAIL_AUTH for credential bundle authentication
  2. Users initiating the request must have appropriate permissions

Opting out

Organizations can disable email-based features if their security model requires it: Use ACTIVITY_TYPE_REMOVE_ORGANIZATION_FEATURE to disable:
  • FEATURE_NAME_OTP_EMAIL_AUTH for OTP-based authentication
  • FEATURE_NAME_EMAIL_AUTH for credential bundle authentication
When creating sub-organizations, use:
  • disableOtpEmailAuth parameter for OTP-based authentication
  • disableEmailAuth parameter for credential bundle authentication

Implementation notes

  • Users are limited to:
    • 10 long-lived API keys
    • 10 expiring API keys (oldest are discarded when limit is reached)

For top-level organizations

  • Both authentication methods are disabled by default
  • Must be enabled via ACTIVITY_TYPE_SET_ORGANIZATION_FEATURE

For sub-organizations

  • Both authentication methods are enabled by default
  • Can be disabled during creation using CreateSubOrganizationIntentV7 activity parameters
Example of enabling OTP-based Email Auth:
turnkey request --host api.turnkey.com --path /public/v1/submit/set_organization_feature --body '{
        "timestampMs": "'"$(date +%s)"'000",
        "type": "ACTIVITY_TYPE_SET_ORGANIZATION_FEATURE",
        "organizationId": "<YOUR-ORG-ID>",
        "parameters": {
                "name": "FEATURE_NAME_OTP_EMAIL_AUTH"
        }
}' --organization <YOUR-ORG-ID>
Example of enabling credential bundle Email Auth:
turnkey request --host api.turnkey.com --path /public/v1/submit/set_organization_feature --body '{
        "timestampMs": "'"$(date +%s)"'000",
        "type": "ACTIVITY_TYPE_SET_ORGANIZATION_FEATURE",
        "organizationId": "<YOUR-ORG-ID>",
        "parameters": {
                "name": "FEATURE_NAME_EMAIL_AUTH"
        }
}' --organization <YOUR-ORG-ID>