Skip to content

fix: preserve original 3DS authentication results - #14294

Open
Longsueynin wants to merge 1 commit into
juspay:mainfrom
Longsueynin:fix/preserve-three-ds-results
Open

Longsueynin wants to merge 1 commit into
juspay:mainfrom
Longsueynin:fix/preserve-three-ds-results

Conversation

@Longsueynin

@Longsueynin Longsueynin commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Expose stored EMV trans_status and preserve Worldpay XML 3DS results for cards, Apple Pay and Google Pay across confirm/retrieve updates. Adds optional API fields; no database migration.

Closes #14293.

@Longsueynin
Longsueynin requested review from a team as code owners September 17, 2026 15:03

@abhisheksharma2411 abhisheksharma2411 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The regression this fixes is the kind that only shows up in production — an authentication result that exists at confirm and is gone by the time anyone reads the payment back. The test is the right shape for it:

// Sync/capture can report an auth code without repeating the 3DS result.
let connector = json!({method: {"auth_code": "123456"}});
let updated = update_additional_payment_data_with_connector_response_pm_data(Some(stored), Some(connector))

asserting the stored authentication_data survives a later connector response that doesn't mention it, across Card, Apple Pay and Google Pay. That's exactly the failure mode — the second response wins and silently drops a field it never carried — and covering all three methods matters because they store under different pointers.

if auth_code.is_none() && authentication_data.is_none() returning early is a good detail too: an authentication failure legitimately has no auth code, so the alternative would have been emitting an empty container on every such response.

I checked what actually lands in the field, since it's untyped, and on this connector it's narrow:

let authentication_data = payment_data.three_d_secure_result
    .map(|description| serde_json::json!({ "three_d_secure_result": description }));

— a human-readable description ("Authentication offered but not used", "Authentication unavailable", "Authentication rejected"). No CAVV, no ECI, no DS transaction id. So nothing sensitive is being newly exposed here, which was my first question given this ends up on the payment response.

That's worth writing down somewhere, because the type won't enforce it. authentication_data: Option<serde_json::Value> on a response-exposed struct is an open container, and the next connector to implement this will copy Worldpay's shape as the precedent. A 3DS result is exactly the place where a cardholder authentication value could plausibly get added "because the connector returned it" — and CAVV/ECI on a payment-retrieve response is a different conversation from a status description. A doc comment on the field saying what belongs in it (connector-reported authentication outcome, not authentication credentials) costs one line and outlives the reviewer.

One wire-format confirmation worth making explicit in the PR body: WalletAdditionalDataForCard carries no skip_serializing_if on any field, so the new key will appear as "authentication_data": null on every response for that struct, not only when populated. That's consistent with its existing siblings (auth_code, issuer_name, issuer_country all behave the same), so it's house style rather than a new problem — but it is a visible response change for integrators diffing payloads, and "no database migration" in the description may read as "no client-visible change" when the response body does gain a key.

trans_status on ExternalAuthenticationDetailsResponse being an enum rather than a string is the right call — that one has a fixed EMV domain and deserves the type.

@XyneSpaces

Copy link
Copy Markdown
Contributor

[should-fix] The new authentication_data field in crates/api_models/src/payments.rs and crates/api_models/src/payments/additional_info.rs holds raw connector 3DS/auth payloads as serde_json::Value. These payloads can contain sensitive authentication details; wrap the field in Secret<serde_json::Value> or a typed PII wrapper to keep it out of logs and serialized diagnostics.

pub authentication_data: Option<Secret<serde_json::Value>>,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Payment responses lose original 3DS authentication results

3 participants