Skip to content

Add encrypted OTP flow#506

Open
carsonp6 wants to merge 1 commit into
mainfrom
05-26-add_encrypted_otp_flow
Open

Add encrypted OTP flow#506
carsonp6 wants to merge 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 May 29, 2026 11:22pm

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

Edit this comment to update them. They will appear in their respective SDK's changelogs.

grid-openapi studio · code · diff

Your SDK build had at least one "note" diagnostic, which is a regression from the base state.
generate ✅

New diagnostics (1 note)

💡 Java/NameNotAllowed: Encountered name `return` that conflicts with a [Kotlin keyword](https://kotlinlang.org/docs/keyword-reference.html#hard-keywords). Renamed to `return_`.

To provide a different name, use a merge transform.

grid-ruby studio · code · diff

Your SDK build was successful.
generate ✅build ✅lint ✅test ✅

grid-kotlin studio · code · diff

Your SDK build had at least one "note" diagnostic, which is a regression from the base state.
generate ✅build ✅lint ✅test ✅

New diagnostics (2 note)

💡 Java/NameNotAllowed: Encountered name `return` that conflicts with a [Kotlin keyword](https://kotlinlang.org/docs/keyword-reference.html#hard-keywords). Renamed to `return_`.

To provide a different name, use a merge transform.

💡 Java/NameNotAllowed: Encountered name `return` that conflicts with a [Kotlin keyword](https://kotlinlang.org/docs/keyword-reference.html#hard-keywords). Renamed to `return_`.

To provide a different name, use a merge transform.

grid-typescript studio · code · diff

Your SDK build was successful.
generate ✅build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/grid-typescript/a3c000df25430a3fe18e51589d0628f93ebe026a/dist.tar.gz
grid-python studio · code · diff

Your SDK build was successful.
generate ✅build ✅lint ❗test ❗

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

Your SDK build had at least one new warning diagnostic, which is a regression from the base state.
generate ⚠️build ❗lint ✅test ❗

New diagnostics (1 warning, 1 note)
⚠️ Schema/CannotInferName: Placeholder name generated for schema.
💡 Name/Renamed: 246 names were renamed due to language constraints, so fallback names will be used instead.
grid-go studio · code · diff

Your SDK build was successful.
generate ✅build ✅lint ❗test ❗

go get github.com/stainless-sdks/grid-go@bf3bff7276fd9961c2c41835c701d0a3ae18ab3b
grid-php studio · code · diff

Your SDK build was successful.
generate ✅lint ✅test ✅

grid-cli studio · code · diff

Your SDK build had at least one "warning" diagnostic, but this did not represent a regression.
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-05-29 23:29:37 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 plaintext OTP code is never transmitted to the server, and deprecates the legacy plaintext flow. The TEK keypair generated by the client for HPKE encryption doubles as the session API keypair, binding authentication and session establishment into a single cryptographic operation.

  • New two-leg EMAIL_OTP flow: registration and challenge endpoints now return otpEncryptionTargetBundle; the verify endpoint accepts encryptedOtpBundle, responds with a 202 challenge carrying a verificationToken, and issues the session after the client signs the token with its TEK private key.
  • Schema updates: AuthMethodResponse gains the optional otpEncryptionTargetBundle field; AuthSession clarifies that encryptedSessionSigningKey is omitted for EMAIL_OTP sessions; AuthSignedRequestChallenge documents the keypair distinction between credential-management retries and the EMAIL_OTP verify retry.
  • Example quality: payloadToSign and oidcToken examples across all modified files are updated to use structurally valid base64url signature segments.

Confidence Score: 4/5

Safe to merge after clarifying whether the legacy otp+clientPublicKey flow is still accepted server-side and updating the schema accordingly.

The PR description explicitly states the legacy EMAIL_OTP flow is "still functional, deprecated," yet the otp and clientPublicKey properties are completely removed from EmailOtpCredentialVerifyRequestFields.yaml rather than retained with deprecated:true. If the server still accepts those fields, the published schema no longer reflects that, breaking SDK generators and contract tests for clients on the old flow.

openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml — the removal of otp and clientPublicKey properties needs to be reconciled with the legacy flow's continued server-side support.

Important Files Changed

Filename Overview
openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml Switches required fields to encryptedOtpBundle for V3 flow, but removes otp and clientPublicKey entirely rather than marking them deprecated:true — contradicts PR description that the legacy flow is still functional.
openapi/paths/auth/auth_credentials_{id}_verify.yaml Adds Grid-Wallet-Signature header, 202 response schema, updated 401 conditions, and two EMAIL_OTP request examples; payloadToSign example now uses a valid base64url signature segment.
openapi/components/schemas/auth/AuthMethodResponse.yaml Adds optional otpEncryptionTargetBundle property for EMAIL_OTP credentials; correctly kept out of required since it only appears for one credential type.
openapi/components/schemas/auth/AuthSession.yaml Clarifies that encryptedSessionSigningKey is omitted for EMAIL_OTP sessions (client holds the TEK private key); clean, accurate update.
openapi/components/schemas/auth/AuthSignedRequestChallenge.yaml Extends description to cover the EMAIL_OTP verify retry use case and documents that the TEK keypair (not the session API keypair) is used for that specific operation.
openapi/paths/auth/auth_credentials.yaml Updates 201 response description and example to include otpEncryptionTargetBundle; oidcToken example updated to a valid base64url signature.
openapi/paths/auth/auth_credentials_{id}_challenge.yaml Updates EMAIL_OTP description to mention otpEncryptionTargetBundle returned on re-issue; adds bundle to the response example.

Sequence Diagram

sequenceDiagram
    participant C as Client
    participant S as Grid Server

    note over C,S: Registration
    C->>S: POST /auth/credentials (EMAIL_OTP)
    S-->>C: 201 AuthMethodResponse (otpEncryptionTargetBundle)

    note over C,S: V3 Verification - Leg 1
    C->>C: Generate ephemeral TEK keypair
    C->>C: HPKE-encrypt clientPublicKey+OTP under otpEncryptionTargetBundle
    C->>S: "POST /auth/credentials/{id}/verify body={encryptedOtpBundle}"
    S->>S: Decrypt bundle, verify OTP code
    S-->>C: 202 AuthSignedRequestChallenge (payloadToSign, requestId, expiresAt)

    note over C,S: V3 Verification - Leg 2 signed retry
    C->>C: Sign verificationToken with TEK private key
    C->>S: "POST /auth/credentials/{id}/verify + Grid-Wallet-Signature + Request-Id"
    S->>S: Verify TEK signature against bound pubkey
    S-->>C: 200 AuthSession (TEK pubkey becomes session API key)
Loading

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
openapi/components/schemas/auth/EmailOtpCredentialVerifyRequestFields.yaml:1-4
**Deprecated fields dropped from schema while legacy flow remains live**

The PR description explicitly states the legacy `otp` + `clientPublicKey` flow is *"still functional, deprecated"*, yet those two properties are completely absent from this schema (not marked `deprecated: true` — removed entirely). Because this is the sole OpenAPI contract, SDK generators and contract-testing tools will have no record that `otp` or `clientPublicKey` are accepted fields. Any existing client still using the legacy flow will appear to be sending invalid request bodies according to the spec. The properties should remain in the schema marked with `deprecated: true` (and removed from `required`) until the server-side removal ships, so the schema accurately reflects what the server actually accepts.

Reviews (2): 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 from f9f2bec to 50e731f Compare May 29, 2026 22:22
@carsonp6 carsonp6 force-pushed the 05-26-add_encrypted_otp_flow branch from 50e731f to 2c892e7 Compare May 29, 2026 23:22
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