Skip to content

Commit c761855

Browse files
committed
docs: document realistic sandbox passkeys
1 parent 6b3905e commit c761855

5 files changed

Lines changed: 39 additions & 18 deletions

File tree

mintlify/openapi.yaml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mintlify/snippets/sandbox-global-account-magic.mdx

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
The Grid sandbox accepts a small set of test helpers for Global Account flows, so you can exercise the full request shape without real OTP delivery, WebAuthn ceremony, or wallet signatures. Email OTP uses a sandbox test inbox. Passkey and wallet signatures use fixed sandbox-only values. OAuth accepts OIDC ID tokens from supported providers; for isolated sandbox tests, you can also pass a JWT-shaped test token. Sandbox skips real IdP signature verification, but still validates token claims, freshness, credential identity, and verify-time nonce binding.
1+
The Grid sandbox lets you exercise Global Account auth flows without moving real money. Email OTP uses a sandbox test inbox. Passkey auth can use the same browser WebAuthn ceremony as production, and signed wallet actions can use the same decrypted session signing key and `Grid-Wallet-Signature` stamp as production. OAuth accepts OIDC ID tokens from supported providers; for isolated sandbox tests, you can also pass a JWT-shaped test token. Sandbox skips real IdP signature verification, but still validates token claims, freshness, credential identity, and verify-time nonce binding.
22

3-
A wrong magic value or sandbox OIDC authentication failure returns `401 UNAUTHORIZED` with a `reason` field that names the specific check that failed. A malformed OIDC JWT can return `400 INVALID_INPUT` before authentication starts.
3+
Sandbox-only compatibility values are still available for some flows, but they do not exercise the production-shaped client implementation. Authentication failures return `401 UNAUTHORIZED` with a `reason` field that names the specific check that failed. A malformed OIDC JWT can return `400 INVALID_INPUT` before authentication starts.
44

55
### Email OTP code
66

@@ -24,11 +24,17 @@ curl -X POST "$GRID_BASE_URL/auth/credentials/AuthMethod:abc123/verify" \
2424

2525
The sandbox validates the code against the pending OTP state. A wrong, expired, consumed, or locked-out code returns `401 UNAUTHORIZED`.
2626

27-
### Passkey assertion signature
27+
### Passkey WebAuthn ceremony
2828

29-
Pass `sandbox-valid-passkey-signature` as `assertion.signature` on `POST /auth/credentials/{id}/verify` when the credential type is `PASSKEY`. The sandbox accepts the rest of the assertion as-is and skips the WebAuthn signature check.
29+
For new sandbox integrations, use the same WebAuthn calls you plan to use in production.
3030

31-
Passkey reauthentication is a two-step `/challenge``/verify` flow. The `clientPublicKey` is sent on `/challenge` (so Grid can seal the session signing key to your device) — the magic value bypasses the credential check, not the HPKE plumbing, so the public key is still required.
31+
1. Generate your own WebAuthn registration challenge and call `navigator.credentials.create()`.
32+
2. Register the passkey with `POST /auth/credentials`, passing the challenge and attestation returned by the browser.
33+
3. Reauthenticate with `POST /auth/credentials/{id}/challenge`, passing the P-256 `clientPublicKey` that Grid should seal the session signing key to.
34+
4. Pass the returned `challenge` into `navigator.credentials.get()` using the returned `credentialId` in `allowCredentials`.
35+
5. Verify with `POST /auth/credentials/{id}/verify`, passing the browser assertion and echoing `Request-Id` from the challenge response.
36+
37+
The sandbox validates the registered credential ID, WebAuthn challenge, origin/RP binding, user-presence bit, assertion signature, and signature counter. A successful verify response includes `encryptedSessionSigningKey`, sealed to the `clientPublicKey`, just like production.
3238

3339
```bash
3440
# 1. /challenge with clientPublicKey
@@ -39,7 +45,7 @@ curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMet
3945
"clientPublicKey": "04f45f2a..."
4046
}'
4147

42-
# 2. /verify with the magic signature, no clientPublicKey
48+
# 2. /verify with the browser assertion returned by navigator.credentials.get()
4349
curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/verify \
4450
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
4551
-H "Content-Type: application/json" \
@@ -50,12 +56,14 @@ curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMet
5056
"credentialId": "...",
5157
"clientDataJson": "...",
5258
"authenticatorData": "...",
53-
"signature": "sandbox-valid-passkey-signature"
59+
"signature": "..."
5460
}
5561
}'
5662
```
5763

58-
Any other signature returns `401 UNAUTHORIZED` with `reason: "Invalid passkey signature"`.
64+
<Note>
65+
The legacy sandbox-only assertion signature `sandbox-valid-passkey-signature` is still accepted for compatibility, but it skips WebAuthn verification and should not be used for production-shaped sandbox tests.
66+
</Note>
5967

6068
### OAuth (OIDC) token
6169

@@ -115,7 +123,7 @@ curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMet
115123

116124
### Wallet signature header
117125

118-
Pass `sandbox-valid-signature` as the `Grid-Wallet-Signature` HTTP header on any signed-retry flow:
126+
After verifying an auth credential, decrypt `encryptedSessionSigningKey` with the private key matching the `clientPublicKey` you supplied on verify or refresh. Use the decrypted session signing key to build a Turnkey API-key stamp over the exact `payloadToSign` string returned by Grid, then pass that full stamp as the `Grid-Wallet-Signature` HTTP header on signed flows:
119127

120128
- `POST /auth/credentials` (add-additional-credential signed retry)
121129
- `DELETE /auth/credentials/{id}` (revoke credential)
@@ -125,11 +133,17 @@ Pass `sandbox-valid-signature` as the `Grid-Wallet-Signature` HTTP header on any
125133
- `POST /quotes/{quoteId}/execute` (when source is an embedded wallet)
126134

127135
```bash
136+
STAMP=$($SIGN stamp "$SESSION_PRIV_HEX" "$PAYLOAD_TO_SIGN")
137+
128138
curl -X POST https://api.lightspark.com/grid/2025-10-13/quotes/Quote:abc123/execute \
129139
-u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \
130140
-H "Content-Type: application/json" \
131141
-H "Idempotency-Key: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \
132-
-H "Grid-Wallet-Signature: sandbox-valid-signature"
142+
-H "Grid-Wallet-Signature: $STAMP"
133143
```
134144

135-
Any other header value returns `401 UNAUTHORIZED` with `reason: "Invalid Grid-Wallet-Signature"`.
145+
Sandbox validates that the stamp is a P-256 Turnkey API-key stamp over the exact pending Turnkey payload and that the public key belongs to an active sandbox session for the wallet.
146+
147+
<Note>
148+
The legacy sandbox-only `Grid-Wallet-Signature: sandbox-valid-signature` value is still accepted for compatibility. Use a real session stamp when you want the client implementation to match production.
149+
</Note>

openapi.yaml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openapi/components/schemas/auth/PasskeyAssertion.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
title: Passkey Assertion
22
type: object
3+
description: >-
4+
WebAuthn assertion returned by `navigator.credentials.get()`. In sandbox,
5+
Grid validates the assertion against the registered passkey credential so the
6+
client-side flow can match production. In production, Turnkey validates the
7+
WebAuthn assertion.
38
required:
49
- credentialId
510
- clientDataJson

scripts/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,9 @@ g -X POST -H 'Content-Type: application/json' -d '{}' \
180180

181181
Read the OTP code from the email and assign to `$OTP`.
182182

183-
> **Sandbox tip**: in sandbox mode, no email is sent — verify with the fixed
184-
> OTP code `000000`.
183+
> **Sandbox tip**: in sandbox mode, no email is sent. Fetch the active code
184+
> from `GET /sandbox/email-otps/latest?authMethodId=$CRED_ID`, then assign
185+
> the returned `otpCode` to `$OTP`.
185186
186187
### 3.3 Verify the OTP and decrypt the session key
187188

@@ -255,11 +256,10 @@ g -X POST -H 'Content-Type: application/json' \
255256
"$GRID_BASE_URL/quotes/$QUOTE_ID/execute" | jq '.status'
256257
```
257258

258-
> **Sandbox tip**: in sandbox mode you can skip the keypair / decrypt /
259-
> stamp dance entirely. Use the fixed value
260-
> `Grid-Wallet-Signature: sandbox-valid-signature`; any other value is
261-
> rejected with the same `INVALID_INPUT` shape a bad real stamp would
262-
> produce.
259+
> **Sandbox tip**: sandbox accepts the same decrypted session signing key and
260+
> `Grid-Wallet-Signature` stamp shape as production. The legacy fixed value
261+
> `Grid-Wallet-Signature: sandbox-valid-signature` is still accepted for
262+
> compatibility, but it does not exercise the production-shaped client path.
263263
264264
```bash
265265
# Poll — typically 60–180s for the full chain to reach COMPLETED.

0 commit comments

Comments
 (0)