Skip to content

Add encrypted OTP flow#506

Merged
carsonp6 merged 1 commit into
mainfrom
05-26-add_encrypted_otp_flow
Jun 4, 2026
Merged

Add encrypted OTP flow#506
carsonp6 merged 1 commit into
mainfrom
05-26-add_encrypted_otp_flow

Conversation

@carsonp6
Copy link
Copy Markdown
Contributor

@carsonp6 carsonp6 commented May 26, 2026

TL;DR

Introduces a secure V3 HPKE-based EMAIL_OTP verification flow and deprecates the legacy plaintext OTP flow.

What changed?

POST /auth/credentials (registration)

  • The 201 response for EMAIL_OTP credentials now includes otpEncryptionTargetBundle — a one-time HPKE target bundle the client uses to encrypt the OTP attempt before sending it to the server.

POST /auth/credentials/{id}/challenge (re-issue)

  • The EMAIL_OTP response now returns a fresh otpEncryptionTargetBundle alongside the AuthMethod, replacing the previous description that said there was no challenge body to surface.

POST /auth/credentials/{id}/verify (verification)

  • Adds a new V3 secure flow for EMAIL_OTP: the client submits an encryptedOtpBundle (HPKE-encrypted payload containing the TEK public key and OTP code attempt). The server responds with 202 carrying a payloadToSign (verificationToken). The client signs the token with the TEK private key and retries with Grid-Wallet-Signature and Request-Id headers to receive the issued AuthSession. The TEK public key becomes the session API key on completion.
  • The previous EMAIL_OTP flow (plaintext otp + clientPublicKey) is now marked deprecated and will be removed in a future release.
  • Adds the Grid-Wallet-Signature request header, required on the signed retry leg of the V3 EMAIL_OTP flow.
  • Updates Request-Id header description to cover both the EMAIL_OTP signed retry and PASSKEY assertion correlation use cases.
  • Adds a 202 response schema (AuthSignedRequestChallenge) to the verify endpoint.
  • Updates 401 error conditions to cover EMAIL_OTP signed retry failures (missing/malformed signature, key mismatch, expired challenge).
  • Sandbox behavior documented: V3 flow runs real HPKE end-to-end; the only shortcut is the magic OTP code "000000".

Schema changes

  • AuthMethodResponse: adds otpEncryptionTargetBundle property.
  • AuthSignedRequestChallenge: extended to cover the EMAIL_OTP verify retry use case; documents that the TEK keypair (not the session API keypair) is used to sign the stamp for this operation.
  • EmailOtpCredentialVerifyRequestFields: otp and clientPublicKey marked deprecated; encryptedOtpBundle added; otp and clientPublicKey removed from required.

How to test?

V3 flow:

  1. Register an EMAIL_OTP credential via POST /auth/credentials and capture otpEncryptionTargetBundle from the response.
  2. Generate an ephemeral P-256 TEK keypair on the client.
  3. HPKE-encrypt {clientPublicKey, otpCodeAttempt} under otpEncryptionTargetBundle to produce encryptedOtpBundle.
  4. Submit POST /auth/credentials/{id}/verify with encryptedOtpBundle and expect a 202 response containing payloadToSign and requestId.
  5. Sign payloadToSign with the TEK private key and resubmit with Grid-Wallet-Signature and Request-Id headers; expect a 200 AuthSession.
  6. In sandbox, use OTP code "000000" as the magic value.

Legacy flow (still functional, deprecated):

  1. Submit POST /auth/credentials/{id}/verify with plaintext otp and clientPublicKey; expect a 200 AuthSession with encryptedSessionSigningKey.

Why make this change?

The legacy EMAIL_OTP flow transmits the plaintext OTP code to the server, creating an unnecessary exposure surface. The V3 flow uses HPKE to encrypt the OTP code and the client's public key together so the plaintext code never transits the server. The TEK keypair generated by the client for encryption also becomes the session API key, binding authentication and session establishment into a single cryptographic operation.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
grid-flow-builder Ignored Ignored Preview Jun 4, 2026 6:54pm

Request Review

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

✱ Stainless preview builds for grid

This PR will update the grid SDKs with the following commit messages.

cli

feat: Add encrypted OTP flow

csharp

feat: Add encrypted OTP flow

go

feat: Add encrypted OTP flow

kotlin

feat: Add encrypted OTP flow

openapi

feat: Add encrypted OTP flow

php

feat: Add encrypted OTP flow

python

feat: Add encrypted OTP flow

ruby

feat: Add encrypted OTP flow

typescript

feat: Add encrypted OTP flow
grid-openapi studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅

grid-ruby studio · code

Your SDK build had at least one note diagnostic.
generate ✅build ✅lint ✅test ✅

grid-kotlin studio · code

Your SDK build had at least one "note" diagnostic.
generate ✅build ✅lint ✅test ✅

⚠️ grid-python studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

pip install https://pkg.stainless.com/s/grid-python/b31fbed31aab4fdc2428921a2736dfbe0912b5b4/grid-0.0.1-py3-none-any.whl
⚠️ grid-csharp studio · code

Your SDK build had a failure in the build CI job, which is a regression from the base state.
generate ⚠️build ❗lint ✅test ❗

grid-typescript studio · code

Your SDK build had at least one note diagnostic.
generate ✅build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/grid-typescript/948a93b3665eab90f89e3cf4c6dc9bb53daa2daf/dist.tar.gz
grid-php studio · code

Your SDK build had at least one note diagnostic.
generate ✅lint ✅test ✅

⚠️ grid-go studio · code

Your SDK build had a failure in the lint CI job, which is a regression from the base state.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@da1c1086d1336cee69ef62909084c7211c641fe0
⚠️ grid-cli studio · code

Your SDK build had a failure in the build CI job, which is a regression from the base state.
generate ⚠️build ❗lint ❗test ❗


This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-06-04 20:14:06 UTC

@carsonp6 carsonp6 marked this pull request as ready for review May 26, 2026 21:33
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 26, 2026

Greptile Summary

This PR introduces a V3 HPKE-based EMAIL_OTP verification flow where the OTP code and client public key are encrypted client-side and never transmitted in plaintext, and deprecates the legacy plaintext OTP flow.

  • New V3 two-leg flow: Registration (POST /auth/credentials) and challenge re-issue now return otpEncryptionTargetBundle; POST /auth/credentials/{id}/verify accepts encryptedOtpBundle, returns 202 with payloadToSign, and completes on a signed retry carrying Grid-Wallet-Signature and Request-Id.
  • Schema updates: AuthMethodResponse gains otpEncryptionTargetBundle; AuthSignedRequestChallenge is extended to cover the EMAIL_OTP verify retry case; EmailOtpCredentialVerifyRequestFields is replaced with the new V3-only shape.
  • Documentation: client-keys.mdx adds an "Encrypt the OTP code" section with a worked HPKE example, but section 2 ("Verify the credential") still describes a single-call flow that returns encryptedSessionSigningKey, which is incorrect for the new two-leg EMAIL_OTP flow.

Confidence Score: 4/5

Safe to merge with one schema fix — the legacy EMAIL_OTP fields are completely absent from the new schema rather than marked deprecated, which will break schema validation for any existing integrators still on the plaintext flow.

The core V3 HPKE flow is well-specified across the path, schema, and example files. The one concrete defect is in EmailOtpCredentialVerifyRequestFields.yaml: the PR description promises deprecated-but-functional legacy fields, but the schema removes them entirely, leaving legacy callers with no documented path and breaking schema validators.

openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml needs the legacy otp/clientPublicKey fields added back as optional deprecated properties if the server still accepts them. mintlify/snippets/global-accounts/client-keys.mdx section 2 needs updating to describe the EMAIL_OTP two-leg flow.

Important Files Changed

Filename Overview
openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml Replaces legacy otp+clientPublicKey fields with encryptedOtpBundle for V3 HPKE flow; legacy fields are completely removed (not marked deprecated as the PR description states), breaking schema validation for existing legacy callers.
mintlify/snippets/global-accounts/client-keys.mdx Adds the new 'Encrypt the OTP code' section for EMAIL_OTP V3; section 2 still implies a single-call flow returning encryptedSessionSigningKey, which is incorrect for the two-leg EMAIL_OTP V3 flow; line 12 clientPublicKey routing description is now stale.
openapi/components/schemas/auth/AuthMethodResponse.yaml Adds otpEncryptionTargetBundle property to carry the HPKE target bundle on EMAIL_OTP registration and challenge responses; well-documented with clear usage guidance.
openapi/paths/auth/auth_credentials_{id}_verify.yaml Adds Grid-Wallet-Signature header, 202 response schema, updated 401 conditions, and emailOtp/emailOtpSignedRetry examples; the two-leg flow is clearly described and examples are well-structured.
openapi/components/schemas/auth/AuthSignedRequestChallenge.yaml Extends description to cover the EMAIL_OTP verify retry use case; correctly documents that the TEK keypair (not session API keypair) is used for signing in this context.
openapi/paths/auth/auth_credentials_{id}_challenge.yaml Updated to return otpEncryptionTargetBundle in the EMAIL_OTP challenge response; example value and description updated accordingly.
openapi/components/schemas/auth/AuthSession.yaml Adds clarification that EMAIL_OTP sessions omit encryptedSessionSigningKey; accurately reflects the new V3 flow where the client retains the TEK private key.
openapi/paths/auth/auth_credentials.yaml 201 response description updated to mention otpEncryptionTargetBundle for EMAIL_OTP; example value updated; no issues.

Sequence Diagram

sequenceDiagram
    participant C as Client
    participant B as Integrator Backend
    participant G as Grid API

    Note over C,G: Registration
    B->>G: POST /auth/credentials
    G-->>B: 201 AuthMethodResponse with otpEncryptionTargetBundle
    G-)C: OTP email delivered

    Note over C,G: V3 EMAIL_OTP Verification
    C->>C: Generate TEK keypair
    C->>C: HPKE-encrypt otp_code+clientPublicKey
    B->>G: POST /auth/credentials/id/verify with encryptedOtpBundle
    G-->>B: 202 with payloadToSign and requestId
    C->>C: Sign payloadToSign with TEK private key
    B->>G: Retry with Grid-Wallet-Signature and Request-Id
    G-->>B: 200 AuthSession
    Note over C: TEK private key becomes session signing key
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml:1-4
**Legacy `otp`/`clientPublicKey` fields silently removed instead of deprecated**

The PR description states these fields are "marked deprecated" and "will be removed in a future release," but the new schema completely omits them — they no longer appear as properties at all. Any legacy client request with `{"type":"EMAIL_OTP","otp":"123456","clientPublicKey":"04..."}` (without `encryptedOtpBundle`) will fail schema validation because `encryptedOtpBundle` is now listed as required, and validators won't understand the payload shape. SDK generators and contract-testing tools will see no hint that a deprecated alternative ever existed.

If the server-side legacy flow is still intentionally functional (as the PR description implies), the schema should retain `otp` and `clientPublicKey` as optional properties with `deprecated: true` so integrators have a documented migration path rather than a silent break.

### Issue 2 of 3
mintlify/snippets/global-accounts/client-keys.mdx:158-160
**Section 2 description is inaccurate for the new EMAIL_OTP V3 two-leg flow**

"Your backend calls `POST /auth/credentials/{id}/verify` and returns the `encryptedSessionSigningKey` from Grid's response to the client." For EMAIL_OTP in the V3 flow, the first call returns `202` with `payloadToSign` (not an `AuthSession`), and the signed retry returns a `200` `AuthSession` that omits `encryptedSessionSigningKey` entirely. A developer following this section linearly for EMAIL_OTP would expect a single call and an `encryptedSessionSigningKey` they need to decrypt — neither is true. The note at line 156 hints at the omission but doesn't redirect the reader through the correct two-leg retry before arriving at section 3.

### Issue 3 of 3
mintlify/snippets/global-accounts/client-keys.mdx:12
**Stale `clientPublicKey` routing description for EMAIL_OTP**

"for `EMAIL_OTP` and `OAUTH` it happens on `POST /auth/credentials/{id}/verify`" refers to where `clientPublicKey` is sent. In the V3 EMAIL_OTP flow, `clientPublicKey` (the TEK public key) is no longer a standalone field on that endpoint — it's embedded inside the HPKE-encrypted `encryptedOtpBundle`. A developer reading this sentence might try to pass `clientPublicKey` as a top-level request field for EMAIL_OTP, which the new schema doesn't include. The sentence should note that for EMAIL_OTP the public key is sealed inside `encryptedOtpBundle`.

Reviews (3): Last reviewed commit: "Add encrypted OTP flow" | Re-trigger Greptile

Comment thread openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml Outdated
Comment thread openapi/paths/auth/auth_credentials_{id}_verify.yaml
@carsonp6 carsonp6 force-pushed the 05-26-add_encrypted_otp_flow branch from 36a042d to 20444ac Compare May 26, 2026 23:16
@carsonp6 carsonp6 force-pushed the 05-26-add_encrypted_otp_flow branch from 20444ac to 2fbd5ec Compare May 29, 2026 20:20
@github-actions github-actions Bot added the breaking-change Introduces a breaking change to the OpenAPI spec label May 29, 2026
@github-actions
Copy link
Copy Markdown
Contributor

⚠️ Breaking OpenAPI changes detected

This PR introduces breaking changes to openapi.yaml:

API Changelog 2025-10-13 vs. 2025-10-13

API Changes

POST /auth/credentials/{id}/verify

  • ⚠️ added the new required request property oneOf[subschema #1: Email OTP Credential Verify Request]/allOf[#/components/schemas/EmailOtpCredentialVerifyRequestFields]/encryptedOtpBundle
  • ⚠️ removed the request property oneOf[subschema #1: Email OTP Credential Verify Request]/allOf[#/components/schemas/EmailOtpCredentialVerifyRequestFields]/clientPublicKey
  • ⚠️ removed the request property oneOf[subschema #1: Email OTP Credential Verify Request]/allOf[#/components/schemas/EmailOtpCredentialVerifyRequestFields]/otp

Detected by oasdiff. This PR will need approval from an API reviewer before merge.

@carsonp6 carsonp6 force-pushed the 05-26-add_encrypted_otp_flow branch from 2fbd5ec to f9f2bec Compare May 29, 2026 20:37
@carsonp6
Copy link
Copy Markdown
Contributor Author

@greptileai

returned in the response to this public key. The key is ephemeral
and one-time-use per verification request.
example: 04f45f2a22c908b9ce09a7150e514afd24627c401c38a4afc164e1ea783adaaa31d4245acfb88c2ebd42b47628d63ecabf345484f0a9f665b63c54c897d5578be2
HPKE-encrypted payload binding the client's ephemeral public key
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there something that describes the format of this bundle or how to create it?

@carsonp6 carsonp6 force-pushed the 05-26-add_encrypted_otp_flow branch 2 times, most recently from 50e731f to 2c892e7 Compare May 29, 2026 23:22
@carsonp6 carsonp6 force-pushed the 05-26-add_encrypted_otp_flow branch from 2c892e7 to 261efa2 Compare June 4, 2026 18:54
@carsonp6 carsonp6 merged commit 2ae1c42 into main Jun 4, 2026
9 of 10 checks passed
@carsonp6 carsonp6 deleted the 05-26-add_encrypted_otp_flow branch June 4, 2026 20:07
pengying pushed a commit that referenced this pull request Jun 6, 2026
## Summary

Updates documentation to match the V3 secure EMAIL_OTP flow introduced in #506. The OpenAPI schema now requires `encryptedOtpBundle` instead of the deprecated plaintext `otp` + `clientPublicKey` fields.

**Changes:**

### Mintlify Documentation
- **authentication.mdx**: Updated EMAIL_OTP section with new mermaid diagram, challenge response showing `otpEncryptionTargetBundle`, and two-step verify flow (202 → signed retry → 200)
- **walkthrough.mdx**: Updated "Authenticate and sign" section to show the encrypted OTP and signed retry pattern
- **sandbox-global-account-magic.mdx**: Updated EMAIL_OTP sandbox docs to show encrypted flow (sandbox runs real HPKE end-to-end)

### Scripts
- **scripts/README.md**: Updated offramp guide to use `encrypt-otp` command and two-step verify
- **scripts/embedded-wallet-sign.js**: Added `encrypt-otp` command for HPKE-encrypting OTP attempts

### Key behavior changes documented:
- The TEK (Target Encryption Key) private key generated by the client becomes the session signing key
- EMAIL_OTP no longer returns `encryptedSessionSigningKey` in the response (no decryption step needed)
- `/challenge` now returns `otpEncryptionTargetBundle` for EMAIL_OTP
- `/verify` is now a two-step flow: first call returns 202 with `payloadToSign`, signed retry returns 200 with session

## Test plan

- [ ] Verify mermaid diagrams render correctly in Mintlify dev server
- [ ] Test `scripts/embedded-wallet-sign.js encrypt-otp` command works
- [ ] Review code samples match the OpenAPI schema examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Introduces a breaking change to the OpenAPI spec

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants