Skip to content

Commit

Permalink
Merge pull-request #66
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewkmin committed Dec 5, 2023
2 parents e95a810 + cd7ee61 commit cbc26f0
Show file tree
Hide file tree
Showing 11 changed files with 381 additions and 67 deletions.
129 changes: 127 additions & 2 deletions api/public_api.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -897,6 +897,32 @@
"tags": ["Policies"]
}
},
"/public/v1/submit/email_auth": {
"post": {
"summary": "Email Auth",
"description": "Authenticate a user via Email",
"operationId": "EmailAuth",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/ActivityResponse"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/EmailAuthRequest"
}
}
],
"tags": ["Email Auth"]
}
},
"/public/v1/submit/export_private_key": {
"post": {
"summary": "Export Private Key",
Expand Down Expand Up @@ -1482,7 +1508,8 @@
"ACTIVITY_TYPE_SIGN_TRANSACTION_V2",
"ACTIVITY_TYPE_EXPORT_PRIVATE_KEY",
"ACTIVITY_TYPE_EXPORT_WALLET",
"ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V4"
"ACTIVITY_TYPE_CREATE_SUB_ORGANIZATION_V4",
"ACTIVITY_TYPE_EMAIL_AUTH"
]
},
"AddressFormat": {
Expand Down Expand Up @@ -1515,6 +1542,11 @@
},
"updatedAt": {
"$ref": "#/definitions/external.data.v1.Timestamp"
},
"expirationSeconds": {
"type": "string",
"format": "uint64",
"description": "Optional window (in seconds) indicating how long the API Key should last."
}
},
"required": [
Expand All @@ -1535,6 +1567,10 @@
"publicKey": {
"type": "string",
"description": "The public component of a cryptographic key pair used to sign messages and transactions."
},
"expirationSeconds": {
"type": "string",
"description": "Optional window (in seconds) indicating how long the API Key should last."
}
},
"required": ["apiKeyName", "publicKey"]
Expand Down Expand Up @@ -2346,6 +2382,10 @@
"disableEmailRecovery": {
"type": "boolean",
"description": "Disable email recovery for the sub-organization"
},
"disableEmailAuth": {
"type": "boolean",
"description": "Disable email auth for the sub-organization"
}
},
"required": ["subOrganizationName", "rootUsers", "rootQuorumThreshold"]
Expand Down Expand Up @@ -2995,6 +3035,84 @@
"type": "string",
"enum": ["EFFECT_ALLOW", "EFFECT_DENY"]
},
"EmailAuthIntent": {
"type": "object",
"properties": {
"email": {
"type": "string",
"description": "Email of the authenticating user."
},
"targetPublicKey": {
"type": "string",
"description": "Client-side public key generated by the user, to which the email auth bundle (credentials) will be encrypted."
},
"apiKeyName": {
"type": "string",
"description": "Optional human-readable name for an API Key. If none provided, default to Email Auth - \u003cTimestamp\u003e"
},
"expirationSeconds": {
"type": "string",
"description": "Optional window (in seconds) indicating how long the API Key should last. Default to 30 minutes."
},
"emailCustomization": {
"$ref": "#/definitions/EmailCustomization",
"description": "Optional parameters for customizing emails. If not provided, use defaults."
}
},
"required": ["email", "targetPublicKey"]
},
"EmailAuthRequest": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["ACTIVITY_TYPE_EMAIL_AUTH"]
},
"timestampMs": {
"type": "string",
"description": "Timestamp (in milliseconds) of the request, used to verify liveness of user requests."
},
"organizationId": {
"type": "string",
"description": "Unique identifier for a given Organization."
},
"parameters": {
"$ref": "#/definitions/EmailAuthIntent"
}
},
"required": ["type", "timestampMs", "organizationId", "parameters"]
},
"EmailAuthResult": {
"type": "object",
"properties": {
"userId": {
"type": "string",
"description": "Unique identifier for the authenticating User."
},
"apiKeyId": {
"type": "string",
"description": "Unique identifier for the created API key."
}
},
"required": ["userId", "apiKeyId"]
},
"EmailCustomization": {
"type": "object",
"properties": {
"subject": {
"type": "string"
},
"body": {
"type": "string"
},
"styling": {
"type": "string"
},
"urlPrefix": {
"type": "string"
}
}
},
"ExportPrivateKeyIntent": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -3112,7 +3230,8 @@
"type": "string",
"enum": [
"FEATURE_NAME_ROOT_USER_EMAIL_RECOVERY",
"FEATURE_NAME_WEBAUTHN_ORIGINS"
"FEATURE_NAME_WEBAUTHN_ORIGINS",
"FEATURE_NAME_EMAIL_AUTH"
]
},
"GetActivitiesRequest": {
Expand Down Expand Up @@ -3696,6 +3815,9 @@
},
"createSubOrganizationIntentV4": {
"$ref": "#/definitions/CreateSubOrganizationIntentV4"
},
"emailAuthIntent": {
"$ref": "#/definitions/EmailAuthIntent"
}
},
"required": ["createOrganizationIntent"]
Expand Down Expand Up @@ -4270,6 +4392,9 @@
},
"createSubOrganizationResultV4": {
"$ref": "#/definitions/CreateSubOrganizationResultV4"
},
"emailAuthResult": {
"$ref": "#/definitions/EmailAuthResult"
}
}
},
Expand Down
39 changes: 21 additions & 18 deletions docs/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
sidebar_position: 8
slug: /faq
---

# FAQ

### Why do you require a public / private key pair to access Turnkey API?
Expand Down Expand Up @@ -38,26 +39,28 @@ We have limits on the number of resources within a single organization to avoid

Currently, the resource limits within a single organization are as follows:

| Resource | Maximum number allowed |
| :---------------------- | :--------------------- |
| Private keys | 1,000 |
| Wallets | 100 |
| Users | 100 |
| Policies | 100 |
| Invitations | 100 |
| Tags | 100 |
| Authenticators per user | 10 |
| API keys per user | 10 |
| Sub-Organizations | unlimited |
| Resource | Maximum number allowed |
| :----------------------------- | :--------------------- |
| Private keys | 1,000 |
| Wallets | 100 |
| Users | 100 |
| Policies | 100 |
| Invitations | 100 |
| Tags | 100 |
| Authenticators per user | 10 |
| API keys per user (long-lived) | 10 |
| API keys per user (expiring) | 10 |
| Sub-Organizations | unlimited |

If you are approaching any of these limits in your implementation and require support, reach out to the Turnkey team (<[email protected]>).

### Do you have any rate limits in place in your public API?

Our public API currently limits users to a maximum of 60 RPS (Requests Per Second). Specific headers are returned to indicate current quota:
* `ratelimit-limit`: indicates the total quota (60)
* `ratelimit-remaining`: indicates the current quota
* `x-rate-limit-request-forwarded-for` and `x-rate-limit-request-remote-addr`: echo back your remote IP and forwarded-for IP for debugging purposes

- `ratelimit-limit`: indicates the total quota (60)
- `ratelimit-remaining`: indicates the current quota
- `x-rate-limit-request-forwarded-for` and `x-rate-limit-request-remote-addr`: echo back your remote IP and forwarded-for IP for debugging purposes

When rate limits are exceeded, an error with HTTP 429 is returned with the following message: `Too many requests. Please wait and try again in a few seconds`.

Expand Down Expand Up @@ -104,16 +107,16 @@ We require a recent timestamp in the `timestampMs` field for each new activity s

Our secure enclaves have their own, independent, secure source of time. We currently require request timestamps to be **less than an hour old**, and **up to 5 minutes in the future**.

### How do pricing and billing work?
### How do pricing and billing work?

Turnkey is priced per signature, i.e. any transaction or raw payload successfully signed by a private key created on Turnkey. Turnkey offers 25 free signatures each month. To execute more than 25 transactions in a given month, you are required to have a credit card on file or active enterprise plan on your account. To upgrade your plan, navigate to Account Settings from the menu in the top right-hand corner in the Turnkey dashboard and follow the instructions.
Turnkey is priced per signature, i.e. any transaction or raw payload successfully signed by a private key created on Turnkey. Turnkey offers 25 free signatures each month. To execute more than 25 transactions in a given month, you are required to have a credit card on file or active enterprise plan on your account. To upgrade your plan, navigate to Account Settings from the menu in the top right-hand corner in the Turnkey dashboard and follow the instructions.

For more information about pricing and billing, check out the [pricing page](https://www.turnkey.com/pricing).

### Where else can I get help with my Turnkey implementation?

If you get stuck or have a one-off question, post it to our [developer forum](https://github.com/orgs/tkhq/discussions) or reach out directly to [email protected]. Teams that are looking for more in-depth integration support can upgrade to an Enterprise plan via [email protected].
If you get stuck or have a one-off question, post it to our [developer forum](https://github.com/orgs/tkhq/discussions) or reach out directly to [email protected]. Teams that are looking for more in-depth integration support can upgrade to an Enterprise plan via [email protected].

### Is my country supported?

Turnkey is not currently available to users in any countries currently subject to US OFAC sanctions.
Turnkey is not currently available to users in any countries currently subject to US OFAC sanctions.
2 changes: 2 additions & 0 deletions docs/getting-started/Quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ Navigate to your user page by clicking on "User Details" in the user dropdown me

Click on "Create API keys" and follow the prompts to add the generated public API key. You'll be required to authenticate with the same authenticator used during onboarding. After this succeeds, you should be all set to interact with our API.

NOTE: if you would like to manually your locally stored public/private API key files (e.g. `key.public`, `key.private`), you will have to save the files without newlines (which occupy extra bytes). For example, for VIM, use `:set binary noeol` or `:set binary noendofline` before writing.

## Create a Wallet

Wallets are collections of cryptographic key pairs typically used for sending and receiving digital assets. To create one, we need to provide a name:
Expand Down
94 changes: 94 additions & 0 deletions docs/getting-started/email-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
sidebar_position: 8
description: Learn about Email Auth on Turnkey
slug: /getting-started/email-auth
---

# Email Auth

Email Auth enables a user to authenticate their Turnkey account via email. In this process, the user is granted an expiring API key that is held in local storage. This expiring API key can be used by the user to access their wallet, similar to a session key. An example utilizing Email Auth for an organization can be found in our SDK repo [here](https://github.com/tkhq/sdk/tree/main/examples/email-auth).

## User Experience

Email auth starts with a new activity posted to Turnkey. This activity has the type `ACTIVITY_TYPE_EMAIL_AUTH` and takes the following as parameters:

- `email`: the email of the user who would like to authenticate. This email must be the email already attached to the user in organization data (i.e., previously approved by the user). This prevents malicious account takeover. If you try to pass a different email address, the activity will fail.
- `targetPublicKey`: the public key to which the auth credential is encrypted (more on this later)
- `apiKeyName`: an optional name for the API Key. If none is provided, we will default to `Email Auth - <Timestamp>`
- `expirationSeconds`: an optional window (in seconds) indicating how long the API Key should last. Default to 30 minutes.
- `emailCustomization`: optional parameters for customizing emails. If not provided, use defaults. This is currently a WIP. 🚧

This activity generates a new API key pair (an "auth credential"), saves the public key in organization data under the target user, and sends an email with the encrypted auth credential:

<p style={{ textAlign: "center" }}>
<img
src="/img/auth_email.png"
alt="auth email"
style={{ width: 420 }}
/>
</p>

Calling email auth requires proper permissions via policies or being a parent organization. See [Authorization](#authorization) for more details.

## Authorization

Authorization for email auth is based on our usual activity authorization: our [policy engine](../policy-management/Policy-overview.md) controls who can and cannot execute auth-related activities.

- `ACTIVITY_TYPE_EMAIL_AUTH` can be performed by the root user or by any user in an organization if authorized by policy, but **only if the feature is enabled**. The activity can target **any user** in this organization **or any sub-organization user**. The activity will fail if a parent user tries to perform email auth for a sub-organization which has [opted out of this feature](#opting-out-of-email-auth).

<p style={{textAlign: 'center'}}>
<img src="/img/diagrams/email_auth_authorization.png" width="500" height="200"/>
</p>

## Email auth in your sub-organizations

Email auth works well with [sub-organizations](./Sub-Organizations.md).

<!-- TODO: Our Demo Passkey Wallet application (https://wallet.tx.xyz) has auth functionality integrated. We encourage you to try it (and look at [the code](https://github.com/tkhq/demo-passkey-wallet) if you're curious!). -->

If you're looking for a more concrete guide, head to our [Sub-Organization Email Auth implementation guide](../integration-guides/email-auth-for-sub-organizations.md) for more details.

## Email auth in your organization

If you want to use email auth in the context of an organization accessed via our dashboard, first, you must ensure that the organization feature (`FEATURE_NAME_EMAIL_AUTH`) is enabled. Additionally, the user attempting to initiate email auth must have appropriate permissions (via root user status, or via policy).

## Opting out of email auth

Similar to email recovery, depending on your threat model, it may be unacceptable to rely on email as an authentication factor. We envision this to be the case when an organization has a mature set of root users with multiple authenticators, or when a sub-organization "graduates" from one to many redundant passkeys or API keys. When you're ready, you can disable email auth with `ACTIVITY_TYPE_REMOVE_ORGANIZATION_FEATURE` (see Remove [Organization Feature](/api#tag/Features/operation/RemoveOrganizationFeature)). The feature name to remove is `FEATURE_NAME_EMAIL_AUTH`.

If you _never_ want to have email auth enabled for sub-organizations, our `CREATE_SUB_ORGANIZATION` activity takes a `disableEmailAuth` boolean in its parameters. Set it to `true` and the sub-organization will be created without the organization feature.

## Cryptographic details

Note: if the following section looks familiar, it is! It shares the same cryptographic innerworkings as Email Recovery.

Unlike typical email auth functionality, Turnkey's email auth doesn't send unencrypted tokens via emails. This ensures no man-in-the-middle attack can happen: even if the content of the auth email is leaked, an attacker wouldn't be able to decrypt the auth credential. The following diagram summarizes the flow:

<img src="/img/email_auth_cryptography.png" />

Our email auth flow works by anchoring auth in a **target encryption key** (TEK). This target encryption key is a standard P-256 key pair and can be created in many ways: completely offline, or online inside of script using the web crypto APIs.

The public part of this key pair is passed as a parameter inside of a signed `EMAIL_AUTH` activity. The signature on the activity has to come from a user who is [authorized](#authorization) to initiate email auth.

Our enclave creates a fresh P256 key pair ("auth credential") and encrypts the private key to the recovering user's TEK using the **Hybrid Public Key Encryption standard**, also known as **HPKE** or [RFC 9180](https://datatracker.ietf.org/doc/rfc9180/).

Once the encrypted auth credential is received via email, it's decrypted where the target public key was originally created. The auth credential is then ready to be used to sign an activity, which is then submitted to Turnkey.

## Implementation notes

Users currently have a limit of 10 long-lived API keys, and 10 expiring API keys. In the case that the limit of expiring API keys is breached, the oldest (by creation date) will be discarded.

NOTE: feature must be enabled. For top-level orgs, by default, Email Auth is not enabled. It must be enabled via the `ACTIVITY_TYPE_SET_ORGANIZATION_FEATURE` activity. Here's an example, using our CLI:

```
turnkey request --host api.turnkey.com --path /public/v1/submit/email_auth --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>
```

Suborgs have Email Auth enabled as a feature by default. It can be conveniently disabled during creation, using the `CreateSubOrganizationIntentV4` activity parameter `disableEmailAuth`.
2 changes: 1 addition & 1 deletion docs/getting-started/email-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Authorization for email recovery is based on our usual activity authorization: o
</p>


Important note: recovery credentials automatically expire after **30 minutes** and are overridden when multiple `INIT_USER_EMAIL_RECOVERY` activities target the same user. Only the most recent recovery credential is valid.
Important note: recovery credentials automatically expire after **15 minutes** and are overridden when multiple `INIT_USER_EMAIL_RECOVERY` activities target the same user. Only the most recent recovery credential is valid.

## Email recovery in your sub-organizations

Expand Down
Loading

0 comments on commit cbc26f0

Please sign in to comment.