Skip to content
Draft
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
16 changes: 8 additions & 8 deletions mintlify/openapi.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 6 additions & 6 deletions mintlify/snippets/global-accounts/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -480,7 +480,7 @@ The lowest-friction credential type — works on any device with email access an

Grid creates the first `EMAIL_OTP` credential when the Global Account is provisioned. The credential uses the customer email on file for the internal account. To authenticate with it, send an OTP challenge, then verify using the secure encrypted OTP flow.

The client never sends the plaintext OTP code. Instead, it HPKE-encrypts the code (together with a fresh public key) to an enclave bundle returned from the challenge. The server is a pass-through and never sees the plaintext.
The client never sends the plaintext OTP code. Instead, it HPKE-encrypts the code (together with a fresh public key) to an encryption target bundle returned from the challenge. The server is a pass-through and never sees the plaintext.

```mermaid
sequenceDiagram
Expand All @@ -495,7 +495,7 @@ sequenceDiagram
G-->>IB: 200 AuthMethod + otpEncryptionTargetBundle
IB-->>C: { otpEncryptionTargetBundle }
E-->>C: OTP code
C->>C: generateClientKeyPair() (TEK)
C->>C: generateClientKeyPair()
C->>C: HPKE-encrypt { otp_code, public_key } → encryptedOtpBundle
C->>IB: POST /my-backend/otp/verify { encryptedOtpBundle }
IB->>G: POST /auth/credentials/{id}/verify { type: EMAIL_OTP, encryptedOtpBundle }
Expand Down Expand Up @@ -527,7 +527,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000
}
```

The client generates a fresh P-256 key pair (the TEK — Target Encryption Key), HPKE-encrypts `{otp_code, public_key}` under `otpEncryptionTargetBundle`, and submits the encrypted payload. See <a href="client-keys#encrypt-the-otp-code-email_otp-only">Encrypt the OTP code</a> for implementation details.
The client generates a fresh ephemeral P-256 key pair, HPKE-encrypts `{otp_code, public_key}` under `otpEncryptionTargetBundle`, and submits the encrypted payload. See <a href="client-keys#encrypt-the-otp-code-email_otp-only">Encrypt the OTP code</a> for implementation details.

Then verify with the encrypted OTP bundle:

Expand All @@ -552,7 +552,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000
}
```

The client signs `payloadToSign` with the TEK private key (the same key whose public key was encrypted in the bundle), then retries with the stamp:
The client signs `payloadToSign` with the private key it generated (the same key whose public key was encrypted in the bundle), then retries with the stamp:

```bash
curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000-000000000004/verify" \
Expand Down Expand Up @@ -580,7 +580,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000
}
```

The TEK public key becomes the session API key. Unlike `OAUTH` and `PASSKEY` flows, `EMAIL_OTP` does **not** return `encryptedSessionSigningKey` — the client already holds the session signing key (the TEK private key it generated).
The client's public key becomes the session API key. Unlike `OAUTH` and `PASSKEY` flows, `EMAIL_OTP` does **not** return `encryptedSessionSigningKey` — the client already holds the session signing key (the private key it generated).

`verify` shares its terminal status code with the still-processing response: a `200` here is only a session if the body isn't `{ "status": "PROCESSING" }`. See <a href="#handling-a-still-processing-response">handling a still-processing response</a> — if you get `PROCESSING`, re-send the identical signed retry (same `encryptedOtpBundle`, `Grid-Wallet-Signature`, and `Request-Id`) until you get the session shown above.

Expand All @@ -603,7 +603,7 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:019542f5-b3e7-1d02-0000

### Email OTP reauthentication

Same pattern as the first activation: call `/challenge` to send a new OTP and receive a fresh `otpEncryptionTargetBundle`, generate a new TEK key pair, build the `encryptedOtpBundle`, and complete the two-step verify flow.
Same pattern as the first activation: call `/challenge` to send a new OTP and receive a fresh `otpEncryptionTargetBundle`, generate a new ephemeral P-256 key pair, build the `encryptedOtpBundle`, and complete the two-step verify flow.

### Changing the email OTP address

Expand Down
8 changes: 4 additions & 4 deletions mintlify/snippets/global-accounts/client-keys.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ func generateClientKeyPair() -> ClientKeyPair {

## Encrypt the OTP code (`EMAIL_OTP` only)

`EMAIL_OTP` credentials never send the OTP code in plaintext. Instead, the client HPKE-encrypts the code (together with its `publicKeyHex`) to an enclave key, so the code is unreadable in transit and Grid is only a pass-through.
`EMAIL_OTP` credentials never send the OTP code in plaintext. Instead, the client HPKE-encrypts the code (together with its `publicKeyHex`) to an encryption target key, so the code is unreadable in transit and Grid is only a pass-through.

Grid returns an `otpEncryptionTargetBundle` whenever it initiates or reissues an OTP challenge, including `POST /auth/credentials/{id}/challenge` and add-EMAIL_OTP signed-retry responses. First-time EMAIL_OTP wallet bootstrap registration can omit it; if the registration response has no bundle, call `POST /auth/credentials/{id}/challenge` for that credential before verifying. The bundle is a signed enclave bundle whose `data` field is hex-encoded JSON carrying the enclave's HPKE target key as `targetPublic`. Pull out `targetPublic`, HPKE-encrypt `{ otp_code, public_key }` to it, and submit the library's `{ encappedPublic, ciphertext }` output as `encryptedOtpBundle` on `POST /auth/credentials/{id}/verify`.
Grid returns an `otpEncryptionTargetBundle` whenever it initiates or reissues an OTP challenge, including `POST /auth/credentials/{id}/challenge` and add-EMAIL_OTP signed-retry responses. First-time EMAIL_OTP wallet bootstrap registration can omit it; if the registration response has no bundle, call `POST /auth/credentials/{id}/challenge` for that credential before verifying. The bundle is a signed bundle whose `data` field is hex-encoded JSON carrying the HPKE target key as `targetPublic`. Pull out `targetPublic`, HPKE-encrypt `{ otp_code, public_key }` to it, and submit the library's `{ encappedPublic, ciphertext }` output as `encryptedOtpBundle` on `POST /auth/credentials/{id}/verify`.

Use an HPKE library so you don't hand-roll the suite, `info`, or AAD. The
helper below assumes your crypto layer returns the JSON string Grid expects for
Expand All @@ -151,13 +151,13 @@ function buildEncryptedOtpBundle(
clientPublicKeyHex: string,
otp: string,
): string {
// Pull the enclave's target key out of the signed bundle.
// Pull the target key out of the signed bundle.
const { data } = JSON.parse(otpEncryptionTargetBundle) as { data: string };
const { targetPublic } = JSON.parse(
new TextDecoder().decode(hexToBytes(data)),
) as { targetPublic: string };

// Note the snake_case { otp_code, public_key } — that's what the enclave expects.
// Note the snake_case { otp_code, public_key } — that's the format Grid expects.
const plainTextBuf = new TextEncoder().encode(
JSON.stringify({ otp_code: otp, public_key: clientPublicKeyHex }),
);
Expand Down
Loading
Loading