diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index a73b3fd7..5095e7a7 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -4467,7 +4467,7 @@ paths: `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `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. 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 `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. + 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 `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: ` to receive a session. operationId: challengeAuthCredential tags: - Embedded Wallet Auth @@ -4523,7 +4523,7 @@ paths: 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': @@ -17818,7 +17818,7 @@ components: example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 PasskeyAuthChallenge: title: Passkey Auth Challenge - description: Extended `AuthMethod` shape returned for `PASSKEY` credentials from `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`. + description: Extended `AuthMethod` shape returned for `PASSKEY` credentials from `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 `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: '#/components/schemas/AuthMethod' - type: object @@ -17834,8 +17834,8 @@ components: example: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew 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 `POST /auth/credentials/{id}/challenge`. - example: VjZ6o8KfE9V3q3LkR2nH5eZ6dM8yA1xW + description: 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: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729 requestId: type: string description: Grid-issued `Request:` identifier for this pending passkey authentication request. Echo this value exactly as the `Request-Id` header on the subsequent `POST /auth/credentials/{id}/verify` call so Grid can correlate the assertion with the issued challenge. diff --git a/mintlify/snippets/global-accounts/authentication.mdx b/mintlify/snippets/global-accounts/authentication.mdx index a5486832..fe5835b6 100644 --- a/mintlify/snippets/global-accounts/authentication.mdx +++ b/mintlify/snippets/global-accounts/authentication.mdx @@ -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`. @@ -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) }], @@ -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 diff --git a/openapi.yaml b/openapi.yaml index a73b3fd7..5095e7a7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4467,7 +4467,7 @@ paths: `OAUTH` credentials do not have a challenge step. To authenticate or reauthenticate an OAuth credential, call `POST /auth/credentials/{id}/verify` with a fresh OIDC token and a `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. 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 `navigator.credentials.get()`, then submits the resulting assertion to `POST /auth/credentials/{id}/verify` with `Request-Id: ` to receive a session. + 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 `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: ` to receive a session. operationId: challengeAuthCredential tags: - Embedded Wallet Auth @@ -4523,7 +4523,7 @@ paths: 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': @@ -17818,7 +17818,7 @@ components: example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2 PasskeyAuthChallenge: title: Passkey Auth Challenge - description: Extended `AuthMethod` shape returned for `PASSKEY` credentials from `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`. + description: Extended `AuthMethod` shape returned for `PASSKEY` credentials from `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 `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: '#/components/schemas/AuthMethod' - type: object @@ -17834,8 +17834,8 @@ components: example: KEbWNCc7NgaYnUyrNeFGX9_3Y-8oJ3KwzjnaiD1d1LVTxR7v3CaKfCz2Vy_g_MHSh7yJ8yL0Pxg6jo_o0hYiew 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 `POST /auth/credentials/{id}/challenge`. - example: VjZ6o8KfE9V3q3LkR2nH5eZ6dM8yA1xW + description: 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: 6b35a4c41d9aa7a2a0e742f9f9e7a1c2d65a2db33a3fb748f6d4f1ce78d9a729 requestId: type: string description: Grid-issued `Request:` identifier for this pending passkey authentication request. Echo this value exactly as the `Request-Id` header on the subsequent `POST /auth/credentials/{id}/verify` call so Grid can correlate the assertion with the issued challenge. diff --git a/openapi/components/schemas/auth/PasskeyAuthChallenge.yaml b/openapi/components/schemas/auth/PasskeyAuthChallenge.yaml index 99576234..1bb7a2c4 100644 --- a/openapi/components/schemas/auth/PasskeyAuthChallenge.yaml +++ b/openapi/components/schemas/auth/PasskeyAuthChallenge.yaml @@ -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 @@ -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: >- diff --git a/openapi/paths/auth/auth_credentials_{id}_challenge.yaml b/openapi/paths/auth/auth_credentials_{id}_challenge.yaml index b044905c..36d9b7b1 100644 --- a/openapi/paths/auth/auth_credentials_{id}_challenge.yaml +++ b/openapi/paths/auth/auth_credentials_{id}_challenge.yaml @@ -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: ` to receive a session. @@ -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':