From 907c2cdad105cfdb94fa4e7b70a457a748e6bb93 Mon Sep 17 00:00:00 2001 From: Dhruv Pareek Date: Thu, 21 May 2026 13:51:40 -0700 Subject: [PATCH] docs: explain realistic OIDC sandbox tokens --- mintlify/global-accounts/index.mdx | 2 +- .../platform-tools/sandbox-testing.mdx | 4 +- mintlify/openapi.yaml | 12 ++--- .../global-accounts/authentication.mdx | 6 ++- .../snippets/sandbox-global-account-magic.mdx | 50 ++++++++++++++++--- openapi.yaml | 12 ++--- .../OauthCredentialCreateRequestFields.yaml | 10 ++-- .../OauthCredentialVerifyRequestFields.yaml | 9 ++-- openapi/paths/auth/auth_credentials.yaml | 16 +++--- .../auth/auth_credentials_{id}_verify.yaml | 11 ++-- 10 files changed, 90 insertions(+), 42 deletions(-) diff --git a/mintlify/global-accounts/index.mdx b/mintlify/global-accounts/index.mdx index 2e4cca94..921d1379 100644 --- a/mintlify/global-accounts/index.mdx +++ b/mintlify/global-accounts/index.mdx @@ -73,6 +73,6 @@ Some Global Accounts capabilities require platform enablement before you can bui Generate the P-256 key pair, decrypt the session signing key, and sign payloads on Web, iOS, and Android. - Magic values for OTP, signatures, and OAuth tokens that exercise the full request shape without standing up real auth providers. + Magic values for OTP and signatures, plus sandbox OIDC token rules that exercise the full request shape without standing up real auth providers. diff --git a/mintlify/global-accounts/platform-tools/sandbox-testing.mdx b/mintlify/global-accounts/platform-tools/sandbox-testing.mdx index 0006ef8a..b6b9be96 100644 --- a/mintlify/global-accounts/platform-tools/sandbox-testing.mdx +++ b/mintlify/global-accounts/platform-tools/sandbox-testing.mdx @@ -7,7 +7,7 @@ icon: "/images/icons/hammer.svg" import SandboxGlobalAccountMagic from '/snippets/sandbox-global-account-magic.mdx'; -The Grid sandbox lets you exercise the full Global Accounts integration — customer creation, account lookup, credential registration, funding, and signed withdrawals — without moving real money or standing up real auth providers. All API endpoints work the same way as in production, but money movements are simulated and real auth checks are bypassed via a small set of magic values. +The Grid sandbox lets you exercise the full Global Accounts integration — customer creation, account lookup, credential registration, funding, and signed withdrawals — without moving real money or standing up real auth providers. All API endpoints work the same way as in production, but money movements are simulated. OTP, passkey, and wallet signatures use sandbox-only magic values, while OAuth uses JWT-shaped sandbox OIDC tokens with claim, freshness, identity, and nonce checks. ## Sandbox setup @@ -52,7 +52,7 @@ All webhook events fire normally in sandbox. Configure your webhook URL in the d When you're ready to go live: 1. Generate production API tokens in the dashboard and swap them for the sandbox credentials in your environment. -2. Remove any sandbox magic values from your client and server code — production runs the real OTP, HPKE, WebAuthn, and ECDSA flows. +2. Remove sandbox magic values and unsigned sandbox OIDC tokens from your client and server code — production runs the real OTP, HPKE, WebAuthn, OIDC signature, and ECDSA flows. 3. Configure production webhook endpoints. 4. Test with small amounts first. diff --git a/mintlify/openapi.yaml b/mintlify/openapi.yaml index 6835912f..d288fb73 100644 --- a/mintlify/openapi.yaml +++ b/mintlify/openapi.yaml @@ -4058,13 +4058,13 @@ paths: requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 expiresAt: '2026-04-08T15:35:00Z' '400': - description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` or `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a credential type that already exists on the internal account. Only one email OTP credential and one passkey credential are supported per internal account at this time. + description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer. content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration. content: application/json: schema: @@ -4231,7 +4231,7 @@ paths: description: | Complete the verification step for a previously created authentication credential and issue a session signing key. - For `EMAIL_OTP` credentials, supply the one-time password that was emailed to the user along with a client-generated public key. For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. + For `EMAIL_OTP` credentials, supply the one-time password that was emailed to the user along with a client-generated public key. For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. On success, the response contains an `encryptedSessionSigningKey` that is encrypted to the supplied `clientPublicKey`, along with an `expiresAt` timestamp marking when the session expires. The `clientPublicKey` is ephemeral and one-time-use per verification request. operationId: verifyAuthCredential @@ -4295,7 +4295,7 @@ paths: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP`), for an OIDC token whose signature, issuer, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. + description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. content: application/json: schema: @@ -16276,7 +16276,7 @@ components: description: Discriminator value identifying this as an OAuth credential. oidcToken: type: string - description: OIDC ID token issued by the identity provider (e.g. Google, Apple). Grid fetches the issuer's signing key from the `iss` claim's `.well-known` OpenID configuration and verifies the token signature. The token's `iat` claim must be less than 60 seconds before the request timestamp. + description: OIDC ID token issued by the identity provider (e.g. Google, Apple). The token's `iss`, `aud`, and `sub` claims define the OAuth identity registered to this credential. In production, the provider signature is verified against the issuer's JWKS. In sandbox, the token must still be JWT-shaped with supported `iss`, non-empty `aud` and `sub`, numeric `iat` and `exp`, and `iat` less than 60 seconds before the request timestamp, but the signature segment may be a dummy value. example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.signature OauthCredentialCreateRequest: title: OAuth Credential Create Request @@ -16420,7 +16420,7 @@ components: description: Discriminator value identifying this as an OAuth verification. oidcToken: type: string - description: OIDC ID token issued by the identity provider. For reauthentication after a prior session expired, supply a fresh token — the token's `iat` claim must be less than 60 seconds before the request timestamp. Grid fetches the issuer's signing key from the `iss` claim's `.well-known` OpenID configuration and verifies the token signature. + description: OIDC ID token issued by the identity provider. For reauthentication after a prior session expired, supply a fresh token — the token's `iat` claim must be less than 60 seconds before the request timestamp. The token identity (`iss`, `aud`, and `sub`) must match the registered OAuth credential. In production, the provider signature is verified against the issuer's JWKS. In sandbox, the token must still be JWT-shaped with supported `iss`, non-empty `aud` and `sub`, numeric `iat` and `exp`, and a `nonce` equal to `sha256(clientPublicKey)`, but the signature segment may be a dummy value. example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.signature clientPublicKey: type: string diff --git a/mintlify/snippets/global-accounts/authentication.mdx b/mintlify/snippets/global-accounts/authentication.mdx index 0f2bd3ee..b13dd015 100644 --- a/mintlify/snippets/global-accounts/authentication.mdx +++ b/mintlify/snippets/global-accounts/authentication.mdx @@ -384,7 +384,11 @@ sequenceDiagram IB-->>C: { encryptedSessionSigningKey, expiresAt } ``` -Grid validates the OIDC token signature against the issuer's JWKS on every call and requires `iat` to be no more than **60 seconds** older than the request. Use a fresh token for each `verify` call; cached tokens will fail. +Grid validates the OIDC token signature against the issuer's JWKS on every call and requires `iat` to be no more than **60 seconds** older than the request. Use a fresh token for each `verify` call; cached tokens will fail. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. + + + In sandbox, OAuth still uses JWT-shaped OIDC tokens. The sandbox skips real IdP signature verification, but it validates the same identity and freshness claims. For `verify`, include `nonce` equal to `sha256(clientPublicKey)`. See [Sandbox testing](/global-accounts/platform-tools/sandbox-testing#oauth-oidc-token). + ```bash curl -X POST "$GRID_BASE_URL/auth/credentials" \ diff --git a/mintlify/snippets/sandbox-global-account-magic.mdx b/mintlify/snippets/sandbox-global-account-magic.mdx index 5f36332d..cbd2aff1 100644 --- a/mintlify/snippets/sandbox-global-account-magic.mdx +++ b/mintlify/snippets/sandbox-global-account-magic.mdx @@ -1,6 +1,6 @@ -The Grid sandbox accepts a small set of magic values that bypass real auth and credential checks for Global Account flows, so you can exercise the full request shape without standing up Turnkey, WebAuthn, or an OIDC provider. These values are sandbox-only — production enforces real signature verification, WebAuthn assertion, and OIDC nonce binding. +The Grid sandbox accepts a small set of magic values for Global Account flows, so you can exercise the full request shape without standing up Turnkey, WebAuthn, or an OIDC provider. OTP, passkey, and wallet signatures use fixed sandbox-only values. OAuth uses JWT-shaped sandbox OIDC tokens: sandbox skips real IdP signature verification, but still validates the token claims, freshness, credential identity, and verify-time nonce binding. -A wrong magic value (or any other value) returns `401 UNAUTHORIZED` with a `reason` field that names the specific check that failed. +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. ### Email OTP code @@ -55,24 +55,58 @@ Any other signature returns `401 UNAUTHORIZED` with `reason: "Invalid passkey si ### OAuth (OIDC) token -Pass `sandbox-valid-oidc-token` as the body `oidcToken` on both `POST /auth/credentials` (OAUTH create) and `POST /auth/credentials/{id}/verify` (OAUTH). +OAuth does not use a fixed magic token in sandbox. Pass a JWT-shaped OIDC token as `oidcToken`. The JWT signature segment can be a dummy value, but the payload must look like a real ID token. + +For `POST /auth/credentials` with `type: "OAUTH"`, the sandbox token must include: + +- `iss`: a supported issuer, such as `https://accounts.google.com`, `accounts.google.com`, or `https://appleid.apple.com` +- `aud`: a non-empty string, or a single-element string array +- `sub`: a non-empty subject identifier for the user +- `iat`: a numeric issued-at timestamp no more than 60 seconds before the request, with 5 seconds of clock skew allowed +- `exp`: a numeric expiration timestamp later than the request time + +Grid stores the OAuth credential's registered identity from `iss`, `aud`, and `sub`. On `POST /auth/credentials/{id}/verify`, the fresh `oidcToken` must carry the same `iss`, `aud`, and `sub` as the credential being verified. It must also include `nonce` equal to `sha256(clientPublicKey)`, where `clientPublicKey` is the exact hex public key sent in the verify request. ```bash +export PUBLIC_KEY="04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2" +OIDC_TOKEN=$(node - <<'NODE' +const crypto = require("crypto"); + +const publicKey = process.env.PUBLIC_KEY || "04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2"; +const now = Math.floor(Date.now() / 1000); +const b64url = (value) => + Buffer.from(JSON.stringify(value)).toString("base64url"); + +const payload = { + iss: "https://accounts.google.com", + sub: "sandbox-user-123", + aud: "grid-sandbox-oauth-client-id", + iat: now, + exp: now + 300, + nonce: crypto.createHash("sha256").update(publicKey).digest("hex"), + email: "sandbox-user-123@example.com", + email_verified: true +}; + +console.log( + `${b64url({ alg: "RS256", typ: "JWT" })}.${b64url(payload)}.sandbox-signature` +); +NODE +) + curl -X POST https://api.lightspark.com/grid/2025-10-13/auth/credentials/AuthMethod:abc123/verify \ -u "$GRID_CLIENT_ID:$GRID_CLIENT_SECRET" \ -H "Content-Type: application/json" \ -H "Request-Id: 7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21" \ -d '{ "type": "OAUTH", - "oidcToken": "sandbox-valid-oidc-token", - "clientPublicKey": "04f45f2a..." + "oidcToken": "'"$OIDC_TOKEN"'", + "clientPublicKey": "'"$PUBLIC_KEY"'" }' ``` -Any other token returns `401 UNAUTHORIZED` with `reason: "Invalid OIDC token"`. - - **OAUTH create still requires a JWT-shaped token.** On the initial `POST /auth/credentials` (OAUTH create), the `oidcToken` must be a structurally valid JWT (`header.payload.signature`) so Grid can decode the `iss` claim and resolve the provider name. The literal `sandbox-valid-oidc-token` works on `verify` but not on `create` — for `create`, sign your own dummy JWT with any payload that includes a recognized `iss` claim. The sandbox bypasses signature verification, not JWT structure parsing. + The old literal `sandbox-valid-oidc-token` is no longer accepted. Use a freshly generated sandbox JWT for both OAuth credential registration and OAuth verification. Production requires a real ID token from your provider and verifies the provider signature. ### Wallet signature header diff --git a/openapi.yaml b/openapi.yaml index 6835912f..d288fb73 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4058,13 +4058,13 @@ paths: requestId: Request:7c4a8d09-ca37-4e3e-9e0d-8c2b3e9a1f21 expiresAt: '2026-04-08T15:35:00Z' '400': - description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` or `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a credential type that already exists on the internal account. Only one email OTP credential and one passkey credential are supported per internal account at this time. + description: Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` when registering an email OTP credential while one already exists, `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose WebAuthn credentialId is already attached to the internal account, or `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an unsupported issuer. content: application/json: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, or when the `Request-Id` does not match an unexpired pending challenge. + description: Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for an additional credential on the target internal account, when the `Request-Id` does not match an unexpired pending challenge, or when OAuth token authentication fails during credential registration. content: application/json: schema: @@ -4231,7 +4231,7 @@ paths: description: | Complete the verification step for a previously created authentication credential and issue a session signing key. - For `EMAIL_OTP` credentials, supply the one-time password that was emailed to the user along with a client-generated public key. For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. + For `EMAIL_OTP` credentials, supply the one-time password that was emailed to the user along with a client-generated public key. For `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior session expired. The token identity (`iss`, `aud`, and `sub`) must match the OAuth credential being verified. In sandbox, the token's `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting `assertion` with the `Request-Id` header. The `clientPublicKey` for `PASSKEY` credentials is supplied on the challenge call, where it is bound into the pending session-creation request. On success, the response contains an `encryptedSessionSigningKey` that is encrypted to the supplied `clientPublicKey`, along with an `expiresAt` timestamp marking when the session expires. The `clientPublicKey` is ephemeral and one-time-use per verification request. operationId: verifyAuthCredential @@ -4295,7 +4295,7 @@ paths: schema: $ref: '#/components/schemas/Error400' '401': - description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP`), for an OIDC token whose signature, issuer, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. + description: Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP`), for an OIDC token whose signature, issuer, identity, nonce, or `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed. content: application/json: schema: @@ -16276,7 +16276,7 @@ components: description: Discriminator value identifying this as an OAuth credential. oidcToken: type: string - description: OIDC ID token issued by the identity provider (e.g. Google, Apple). Grid fetches the issuer's signing key from the `iss` claim's `.well-known` OpenID configuration and verifies the token signature. The token's `iat` claim must be less than 60 seconds before the request timestamp. + description: OIDC ID token issued by the identity provider (e.g. Google, Apple). The token's `iss`, `aud`, and `sub` claims define the OAuth identity registered to this credential. In production, the provider signature is verified against the issuer's JWKS. In sandbox, the token must still be JWT-shaped with supported `iss`, non-empty `aud` and `sub`, numeric `iat` and `exp`, and `iat` less than 60 seconds before the request timestamp, but the signature segment may be a dummy value. example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.signature OauthCredentialCreateRequest: title: OAuth Credential Create Request @@ -16420,7 +16420,7 @@ components: description: Discriminator value identifying this as an OAuth verification. oidcToken: type: string - description: OIDC ID token issued by the identity provider. For reauthentication after a prior session expired, supply a fresh token — the token's `iat` claim must be less than 60 seconds before the request timestamp. Grid fetches the issuer's signing key from the `iss` claim's `.well-known` OpenID configuration and verifies the token signature. + description: OIDC ID token issued by the identity provider. For reauthentication after a prior session expired, supply a fresh token — the token's `iat` claim must be less than 60 seconds before the request timestamp. The token identity (`iss`, `aud`, and `sub`) must match the registered OAuth credential. In production, the provider signature is verified against the issuer's JWKS. In sandbox, the token must still be JWT-shaped with supported `iss`, non-empty `aud` and `sub`, numeric `iat` and `exp`, and a `nonce` equal to `sha256(clientPublicKey)`, but the signature segment may be a dummy value. example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.signature clientPublicKey: type: string diff --git a/openapi/components/schemas/auth/OauthCredentialCreateRequestFields.yaml b/openapi/components/schemas/auth/OauthCredentialCreateRequestFields.yaml index 3660fc22..afb0d472 100644 --- a/openapi/components/schemas/auth/OauthCredentialCreateRequestFields.yaml +++ b/openapi/components/schemas/auth/OauthCredentialCreateRequestFields.yaml @@ -12,8 +12,10 @@ properties: type: string description: >- OIDC ID token issued by the identity provider (e.g. Google, Apple). - Grid fetches the issuer's signing key from the `iss` claim's - `.well-known` OpenID configuration and verifies the token signature. - The token's `iat` claim must be less than 60 seconds before the - request timestamp. + The token's `iss`, `aud`, and `sub` claims define the OAuth identity + registered to this credential. In production, the provider signature is + verified against the issuer's JWKS. In sandbox, the token must still be + JWT-shaped with supported `iss`, non-empty `aud` and `sub`, numeric `iat` + and `exp`, and `iat` less than 60 seconds before the request timestamp, + but the signature segment may be a dummy value. example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.signature diff --git a/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml b/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml index 688b7338..7783c14c 100644 --- a/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml +++ b/openapi/components/schemas/auth/OauthCredentialVerifyRequestFields.yaml @@ -15,9 +15,12 @@ properties: OIDC ID token issued by the identity provider. For reauthentication after a prior session expired, supply a fresh token — the token's `iat` claim must be less than 60 seconds before the request - timestamp. Grid fetches the issuer's signing key from the `iss` - claim's `.well-known` OpenID configuration and verifies the token - signature. + timestamp. The token identity (`iss`, `aud`, and `sub`) must match + the registered OAuth credential. In production, the provider signature + is verified against the issuer's JWKS. In sandbox, the token must still + be JWT-shaped with supported `iss`, non-empty `aud` and `sub`, numeric + `iat` and `exp`, and a `nonce` equal to `sha256(clientPublicKey)`, but + the signature segment may be a dummy value. example: eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJzdWIiOiIxMTIyMzM0NDU1IiwiYXVkIjoiMTIzNDU2Ny5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoidXNlckBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjczNjUwOSwiZXhwIjoxNzQ2NzQwMTA5fQ.signature clientPublicKey: type: string diff --git a/openapi/paths/auth/auth_credentials.yaml b/openapi/paths/auth/auth_credentials.yaml index b1f0714c..48eadb8f 100644 --- a/openapi/paths/auth/auth_credentials.yaml +++ b/openapi/paths/auth/auth_credentials.yaml @@ -160,11 +160,12 @@ post: expiresAt: '2026-04-08T15:35:00Z' '400': description: >- - Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` or - `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a credential type - that already exists on the internal account. Only one email OTP - credential and one passkey credential are supported per internal - account at this time. + Bad request. Returned with `EMAIL_OTP_CREDENTIAL_ALREADY_EXISTS` + when registering an email OTP credential while one already exists, + `PASSKEY_CREDENTIAL_ALREADY_EXISTS` when registering a passkey whose + WebAuthn credentialId is already attached to the internal account, or + `INVALID_INPUT` when an OAuth `oidcToken` is malformed or has an + unsupported issuer. content: application/json: schema: @@ -173,8 +174,9 @@ post: description: >- Unauthorized. Returned when the provided `Grid-Wallet-Signature` is missing, malformed, or does not match a pending challenge for - an additional credential on the target internal account, or when - the `Request-Id` does not match an unexpired pending challenge. + an additional credential on the target internal account, when + the `Request-Id` does not match an unexpired pending challenge, or + when OAuth token authentication fails during credential registration. content: application/json: schema: diff --git a/openapi/paths/auth/auth_credentials_{id}_verify.yaml b/openapi/paths/auth/auth_credentials_{id}_verify.yaml index f0ffd525..21d46dc5 100644 --- a/openapi/paths/auth/auth_credentials_{id}_verify.yaml +++ b/openapi/paths/auth/auth_credentials_{id}_verify.yaml @@ -10,7 +10,10 @@ post: `OAUTH` credentials, supply a fresh OIDC token (`iat` must be less than 60 seconds before the request) along with the client-generated public key; this is also the reauthentication path after a prior - session expired. For `PASSKEY` credentials, the client completes a + session expired. The token identity (`iss`, `aud`, and `sub`) must + match the OAuth credential being verified. In sandbox, the token's + `nonce` must equal `sha256(clientPublicKey)`. For `PASSKEY` credentials, + the client completes a WebAuthn assertion (`navigator.credentials.get()`) against the Grid-issued `challenge` returned from `POST /auth/credentials/{id}/challenge`, and submits the resulting @@ -92,9 +95,9 @@ post: '401': description: >- Unauthorized. Returned for an invalid or expired OTP (`EMAIL_OTP`), - for an OIDC token whose signature, issuer, or `iat` freshness - check failed (`OAUTH`), or for a WebAuthn assertion whose - signature, challenge, or credential match failed (`PASSKEY`). + for an OIDC token whose signature, issuer, identity, nonce, or + `iat` freshness check failed (`OAUTH`), or for a WebAuthn assertion + whose signature, challenge, or credential match failed (`PASSKEY`). Also returned for `PASSKEY` when `Request-Id` is missing, does not match an unexpired pending challenge for this credential, or was already consumed.