feat(connector): [STRIPE] Google Pay decryption, and route predecrypted wallets through /v1/tokens - #2120
Open
pixincreate wants to merge 10 commits into
Open
feat(connector): [STRIPE] Google Pay decryption, and route predecrypted wallets through /v1/tokens#2120pixincreate wants to merge 10 commits into
pixincreate wants to merge 10 commits into
Conversation
…token Stripe only accepts a Google Pay credential for later merchant-initiated transactions when it is presented as a network token (PAN + cryptogram). The connector previously read only the encrypted wallet token and errored on GpayTokenizationData::Decrypted, so a Google Pay payment with setup_future_usage could not be set up at all. Add StripeGooglePayPredecrypt and route the decrypted token to it, falling back to a plain card when the token carries no cryptogram/ECI. auth_type is threaded through the wallet conversions so that fallback can set request_three_d_secure. Apple Pay predecrypt already worked and is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Record why this arm builds the non-generic card variant where hyperswitch builds StripeCardData, and that the two serialize identically, so the divergence does not read as an unexplained drift from OSS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Predecrypted Apple Pay and Google Pay have never worked on Stripe in UCS. `StripeApplePayPredecrypt` / `StripeGooglePayPredecrypt` serialize a top-level `card[number]` + `card[cryptogram]` + `card[eci]` + `card[tokenization_method]` block, and Authorize flattened that straight into `/v1/payment_intents`, which answers `parameter_unknown` on `card`. Replaying the exact bytes UCS produces against the sandbox: `/v1/tokens` 200s and returns a `tok_` with `card.tokenization_method` set, `/v1/payment_methods` 400s on `tokenization_method`/`eci`/`cryptogram`, `/v1/payment_intents` 400s on `card`. Apple Pay predecrypt was broken the same way and predates the Google Pay work. Mirror hyperswitch OSS: * The `PaymentMethodToken` flow now posts wallets to `/v1/tokens` and keeps raw cards on `/v1/payment_methods`. The two endpoints mint different object families (`tok_` vs `pm_`) that Authorize spends on different parameters, so the split has to be per payment method rather than a blanket switch. Card tokenization keeps the `pm_` contract UCS already ships. * Authorize now recognises a `tok_` and sends it as `payment_method_data[card][token]` (the shape `GooglePayToken` / `ApplepayPayment` already used) instead of `payment_method=`, which only accepts a PaymentMethod id. * Authorize refuses a decrypted wallet credential passed directly, with a message pointing at `PaymentMethodService/Tokenize`, rather than posting a request Stripe is guaranteed to reject. Tokenization stays caller-driven — Authorize does not mint a token inline. Same call we made on Checkout: Stripe tokens are single-use and short-lived, so an inline second network call turns every Authorize retry into a fresh token mint and leaks a token per attempt. UCS also has no connector-level flow chaining; composite Authorize orchestrates access-token/customer/order/authenticate but has no tokenize step, so inline minting would need proto and composite-service changes well outside the connector. Verified against the Stripe sandbox: card Authorize CHARGED (unchanged), Google Pay decrypted with cryptogram+ECI now CHARGED via `tok_`, Google Pay PAN_ONLY still CHARGED through the plain-card fallback, Apple Pay decrypted now CHARGED via `tok_`, Google Pay decrypted with `setup_future_usage=off_session` still emits `setup_future_usage=off_session` and CHARGED, and card tokenize -> pm_ -> Authorize still CHARGED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
pixincreate
marked this pull request as ready for review
August 14, 2026 06:24
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
deepanshu-iiitu
previously approved these changes
Aug 14, 2026
…e flow
`should_do_payment_method_token` excluded Google Pay outright, so a decrypted
Google Pay payload never reached Tokenize: the composite flow sent it straight
to Authorize, where the `is_tokens_endpoint_only` guard refused it. The Google
Pay decryption support was therefore unreachable through the normal flow.
Deleting the exclusion would have been wrong in the other direction. An
*encrypted* Google Pay payload is Google's own token and Stripe consumes it
inline on `/v1/payment_intents` as `payment_method_data[card][token]`; only the
*decrypted* credential (PAN + cryptogram + ECI) is restricted to `/v1/tokens`.
The two shapes collapse onto the same `Wallet` + `GooglePay` pair, which is why
the old signature could not express the rule.
`should_do_payment_method_token` now also receives whether the wallet payload
arrived pre-decrypted, computed at the composite call site from the request's
`google_pay_sdk` / `apple_pay_sdk` oneof. Stripe answers wallet AND (not Google
Pay OR pre-decrypted), which is what hyperswitch's config expresses as
`payment_method = "wallet"` + `payment_method_type = { list = "google_pay",
type = "disable_only" }` + `google_pay_pre_decrypt_flow =
"connector_tokenization"`. Every other implementor ignores the new parameter and
is behaviourally unchanged; the default impl still returns false.
Verified against the Stripe sandbox through `/composite/payment_methods/get`:
decrypted Google Pay and Apple Pay now mint a `tok_…` on `/v1/tokens` and the
follow-up Authorize charges it, encrypted Google Pay performs no tokenization
call at all and charges directly on `/v1/payment_intents`, and a decrypted
credential posted straight to Authorize is still refused locally without any
outbound request.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…erences Addresses review feedback on #2120. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Shubhodip900
previously approved these changes
Aug 14, 2026
…guard every flow
Three fixes to the decrypted-wallet routing added in this branch.
1. A cryptogram-less decrypted wallet is no longer treated as tokenizable.
`is_wallet_payload_pre_decrypted` reported `true` for any decrypted wallet
payload, so Stripe's `should_do_payment_method_token` demanded Tokenize for a
PAN_ONLY Google Pay credential. The Tokenize builder can only produce the
PaymentIntent card block (`payment_method_data[card][…]` /
`payment_method_options[card][…]`) for it, and `/v1/tokens` accepts only
`card[number]` / `card[exp_month]` / `card[exp_year]` / `card[cvc]` — a
guaranteed 400 on exactly the path the flag routed traffic onto. The flag now
reports the narrower, honest fact — the payload carries a decrypted *network
token*, i.e. a PAN with a cryptogram — and is renamed
`is_wallet_payload_decrypted_network_token` so that "pre-decrypted" is no
longer conflated with "tokenizable". A PAN_ONLY credential stays on the inline
Authorize path it already worked on.
2. The `/v1/tokens`-only guard now covers every flow that flattens a
`StripePaymentMethodData` into a non-token request body, not just Authorize.
The same `card[cryptogram]` + `card[eci]` block was still reachable through
the SetupMandate wallet arm (`/v1/setup_intents`) and RepeatPayment's
`NetworkTokenWithNTI` arm (`/v1/payment_intents`). Both now fail fast via a
shared `reject_if_tokens_endpoint_only`, naming the endpoint and the tokenize
step, instead of shipping a request Stripe answers with `parameter_unknown`.
3. `StripeGooglePayPredecrypt::eci` becomes `Option`, matching
`StripeApplePayPredecrypt`. A cryptogram-bearing token with no ECI used to
fall into the PAN-only arm and charge as a plain unauthenticated card,
silently discarding the cryptogram and the liability shift. The cryptogram
alone now decides the shape. This intentionally diverges from OSS, which
matches `(Some(cryptogram), Some(eci))` and drops the cryptogram otherwise.
Verified against the Stripe sandbox, wallet cases driven through
CompositePaymentMethodService/Get so `should_do_payment_method_token` is actually
consulted:
card Authorize -> /v1/payment_intents, CHARGED
GPay decrypted, cryptogram + ECI -> /v1/tokens then /v1/payment_intents,
CHARGED, charge wallet.type=google_pay
GPay decrypted, PAN only -> no Tokenize call at all, inline
/v1/payment_intents card, CHARGED
GPay decrypted, cryptogram no ECI -> /v1/tokens with card[cryptogram] and no
card[eci], CHARGED, wallet.type=google_pay
Apple Pay decrypted -> unchanged, /v1/tokens, CHARGED
GPay encrypted -> no Tokenize, inline
payment_method_data[card][token]
SetupMandate w/ decrypted wallet -> local refusal, zero HTTP emitted
RepeatPayment NetworkTokenWithNTI -> local refusal, zero HTTP emitted
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Auto-applied by CI: - cargo +nightly fmt --all - make -C sdk generate (if applicable) - make docs (if applicable) This commit was automatically generated by GitHub Actions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two things: adding Google Pay decryption support for Stripe, and fixing a pre-existing bug that meant no predecrypted wallet ever worked on this connector.
1. Google Pay decryption was missing
Stripe only accepts a Google Pay credential for later merchant-initiated transactions as a network token (PAN + cryptogram).
TryFrom<&GooglePayWalletData>read only the encrypted token and errored onGpayTokenizationData::Decrypted, so Google Pay withsetup_future_usagecould not be set up at all.Adds
StripeWallet::GooglePayPredecryptToken/StripeGooglePayPredecrypt(tokenization_method = android_pay). A PAN_ONLY token — no cryptogram or ECI — falls back to a plain card.auth_typeis threaded through the wallet conversions so that fallback can setrequest_three_d_secure.2. Predecrypted wallets were posted to the wrong endpoint
Found while testing the above. Apple Pay predecrypt was equally broken and predates this branch.
The predecrypt structs serialize a top-level
card[number]/card[cryptogram]/card[eci]/card[tokenization_method]block, which is only legal on/v1/tokens. Authorize flattened it straight into/v1/payment_intents. Replaying the exact bytes:POST /v1/tokenstok_...,card.tokenization_method: "android_pay"POST /v1/payment_methodstokenization_method, eci, cryptogramPOST /v1/payment_intents(what this connector did)parameter_unknown,param: "card"Fix:
PaymentMethodTokenflow'sget_urlsplits by payment method:Wallet→v1/tokens, everything else staysv1/payment_methods. Card tokenization already round-trips coherently (/v1/payment_methods→pm_→payment_method=pm_) and is left alone.StripeCardTokenPayment(payment_method_data[card][token]). Authorize splits an incomingPaymentMethodTokenon thetok_prefix — a token goes topayment_method_data[card][token], apm_still goes topayment_method. The prefix determines the Stripe object type, which determines the legal parameter.3. Predecrypted Google Pay was excluded from tokenization
should_do_payment_method_tokenreturnedfalsefor Google Pay unconditionally, so the composite service skipped tokenization and the decrypted payload hit the guard above — the feature was unreachable through the normal flow. Apple Pay was unaffected.Removing the exclusion outright would break encrypted Google Pay, which must go direct to Authorize as
GooglepayToken. The trait signature could not express the difference, soshould_do_payment_method_tokengains anis_wallet_pre_decryptedflag (defaultfalse, so the nine other implementors are behaviourally untouched), and the composite service derives it from the request payload's wallet oneof. Stripe's rule becomes: wallet and (not Google Pay or the payload is predecrypted).Net routing, which matches the connector's documented contract:
GooglepayToken/v1/tokensthenpayment_method_data[card][token]No inline tokenization from Authorize. Stripe tokens are single-use and short-lived, so an inline mint would leak a token on every Authorize retry, and the composite Authorize has no tokenize step to chain through.
Testing — real Stripe sandbox
Driven through the composite path (the one that consults
should_do_payment_method_token), not by calling Tokenize by hand — calling it manually is what masked bug 3 the first time./v1/payment_intents,payment_method_data[card][number]pi_3U4Gj8D5R7gDAGff1MD8kEfX/v1/tokens→tok_, thenpayment_method_data[card][token]pi_3U4Gk2D5R7gDAGff0Y7Ey1yj— was 400GooglepayTokenpi_3U4GknD5R7gDAGff0zLUkNMQ/v1/tokens→tok_, thenpayment_method_data[card][token]pi_3U4Gl6D5R7gDAGff0fho2ZIv— was 400setup_future_usage=OFF_SESSIONsetup_future_usage=off_sessionpi_3U4GlgD5R7gDAGff1ypGbKez, mandatepm_1U4GlgD5R7gDAGffNEmYd3Ow/v1/payment_methods→pm_,payment_method=pm_pi_3U4CDrD5R7gDAGff0r5570pxCase (c) is the regression risk of change 3 and is clean in both directions.
Outgoing body for (b),
POST /v1/tokens:then
POST /v1/payment_intents:cargo clippy -p connector-integration --all-targets,-p composite-service -p interfaces -p grpc-server,cargo check --workspace --all-targetsandcargo fmt --checkall clean.Known adjacent issue, deliberately not changed
RepeatPaymenthas the samepayment_method = tokenassignment and would mishandle atok_. Unreachable in practice — MIT uses a stored mandate id — so it was left rather than changing an untested path, but it is the next place this bug class surfaces.🤖 Generated with Claude Code