Skip to content
Merged
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
10 changes: 5 additions & 5 deletions mintlify/openapi.yaml

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

9 changes: 5 additions & 4 deletions mintlify/snippets/global-accounts/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Global Accounts are initialized with an `EMAIL_OTP` credential tied to the custo
To produce a session:

- **`EMAIL_OTP`** and **`OAUTH`** — call **`POST /auth/credentials/{id}/verify`** with the OTP value (or a fresh OIDC token) plus a `clientPublicKey`. The response carries the `encryptedSessionSigningKey`.
- **`PASSKEY`** — call **`POST /auth/credentials/{id}/challenge`** with your `clientPublicKey` to receive a Grid-issued WebAuthn `challenge` and `requestId`, run `navigator.credentials.get()` against that challenge, then call **`POST /auth/credentials/{id}/verify`** with the resulting assertion and the `Request-Id` header. Grid bakes the `clientPublicKey` from the `/challenge` call into the session-creation payload, so it does **not** appear on the `/verify` body.
- **`PASSKEY`** — call **`POST /auth/credentials/{id}/challenge`** with your `clientPublicKey` to receive a Grid-issued `challenge` and `requestId`, UTF-8 encode the challenge string for `navigator.credentials.get()`, then call **`POST /auth/credentials/{id}/verify`** with the resulting assertion and the `Request-Id` header. Grid bakes the `clientPublicKey` from the `/challenge` call into the session-creation payload, so it does **not** appear on the `/verify` body.

To add another credential, call **`POST /auth/credentials`** with the new credential details. Because the account already has an `EMAIL_OTP` credential, Grid returns a `202` signed-retry challenge. Stamp that `payloadToSign` with an active session signing key, then retry the same request with `Grid-Wallet-Signature` and `Request-Id`. The signed retry returns the new `AuthMethod`.

Expand Down Expand Up @@ -142,10 +142,11 @@ const challengeRes = await fetch("/my-backend/passkey/challenge", {
});
const { challenge: gridChallenge, requestId: authRequestId } = await challengeRes.json();

// 6. Run the WebAuthn assertion against the Grid-issued challenge.
// 6. Run the WebAuthn assertion against the Grid-issued challenge. This
// challenge is a lowercase hex string; UTF-8 encode it exactly as returned.
const assertion = (await navigator.credentials.get({
publicKey: {
challenge: base64urlToBytes(gridChallenge),
challenge: new TextEncoder().encode(gridChallenge),
rpId,
userVerification: "required",
allowCredentials: [{ type: "public-key", id: base64urlToBytes(credentialId) }],
Expand Down Expand Up @@ -372,7 +373,7 @@ These are the fields you need to pass through on each hop.

### Passkey reauthentication

When a session expires the client re-verifies without recreating the credential. Reauthentication uses the same `/challenge` → `/verify` shape as the first authentication: generate a fresh client key pair, call `POST /auth/credentials/{id}/challenge` with the new `clientPublicKey`, run `navigator.credentials.get()` against the returned challenge, then call `/verify` with the assertion and the matching `Request-Id` header.
When a session expires the client re-verifies without recreating the credential. Reauthentication uses the same `/challenge` → `/verify` shape as the first authentication: generate a fresh client key pair, call `POST /auth/credentials/{id}/challenge` with the new `clientPublicKey`, UTF-8 encode the returned challenge string for `navigator.credentials.get()`, then call `/verify` with the assertion and the matching `Request-Id` header.

```mermaid
sequenceDiagram
Expand Down
10 changes: 5 additions & 5 deletions openapi.yaml

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

21 changes: 12 additions & 9 deletions openapi/components/schemas/auth/PasskeyAuthChallenge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ description: >-
`POST /auth/credentials/{id}/challenge`. Includes the WebAuthn
`credentialId` needed to target the passkey, plus the Grid-issued
`challenge`, corresponding `requestId`, and challenge `expiresAt`. The
client signs the challenge with the passkey to produce the assertion
submitted to `POST /auth/credentials/{id}/verify`.
`challenge` value is the lowercase hex-encoded SHA-256 digest of the
canonical Turnkey session-creation request body, not a base64url string.
The client UTF-8 encodes this string as the WebAuthn challenge and signs it
with the passkey to produce the assertion submitted to
`POST /auth/credentials/{id}/verify`.
allOf:
- $ref: ./AuthMethod.yaml
- type: object
Expand All @@ -26,14 +29,14 @@ allOf:
challenge:
type: string
description: >-
Base64url-encoded challenge issued by Grid for the pending
passkey authentication. The client passes it into
`navigator.credentials.get()` as the WebAuthn challenge; the
resulting assertion is submitted to
`POST /auth/credentials/{id}/verify`. Single-use; a new
challenge is issued on the next call to
Lowercase hex-encoded SHA-256 digest of the canonical Turnkey
session-creation request body for the pending passkey
authentication. Do not base64url-decode this field; pass UTF-8 bytes
of the string (for example, `new TextEncoder().encode(challenge)`) as
the WebAuthn challenge to `navigator.credentials.get()`. Single-use;
a new challenge is issued on the next call to
`POST /auth/credentials/{id}/challenge`.
example: VjZ6o8KfE9V3q3LkR2nH5eZ6dM8yA1xW
example: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729
requestId:
type: string
description: >-
Expand Down
19 changes: 11 additions & 8 deletions openapi/paths/auth/auth_credentials_{id}_challenge.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,18 @@ post:
`clientPublicKey`.


For `PASSKEY` credentials, this issues a fresh Grid-generated WebAuthn
challenge for reauthentication. The request body must carry the
client's ephemeral `clientPublicKey` so Grid can bake it into the
Turnkey session-creation payload the returned challenge is computed
from — this seals the resulting session signing key to the client.
For `PASSKEY` credentials, this issues a fresh Grid reauthentication
challenge. The request body must carry the client's ephemeral
`clientPublicKey` so Grid can bake it into the Turnkey session-creation
payload the returned challenge is computed from — this seals the
resulting session signing key to the client.
The response is a `PasskeyAuthChallenge` — the passkey auth method
fields plus the WebAuthn `credentialId`, new `challenge`, `requestId`,
and `expiresAt`. The client passes `credentialId` as
`allowCredentials[].id` and `challenge` as the WebAuthn challenge in
and `expiresAt`. The `challenge` value is the lowercase hex-encoded
SHA-256 digest of the canonical Turnkey session-creation body, not a
base64url string. The client base64url-decodes `credentialId` for
`allowCredentials[].id` and UTF-8 encodes `challenge` (for example,
`new TextEncoder().encode(challenge)`) as the WebAuthn challenge in
`navigator.credentials.get()`, then submits the resulting assertion to
`POST /auth/credentials/{id}/verify` with `Request-Id: <requestId>` to
receive a session.
Expand Down Expand Up @@ -98,7 +101,7 @@ post:
nickname: iPhone Face-ID
createdAt: '2026-04-08T15:30:01Z'
updatedAt: '2026-04-08T15:35:00Z'
challenge: VjZ6o8KfE9V3q3LkR2nH5eZ6dM8yA1xW
challenge: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729
requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21
expiresAt: '2026-04-08T15:35:00Z'
'400':
Expand Down
Loading