Repository navigation
CyberSource 3DS: authorization loses the ECI because the connector never reads eci / eciRaw #15108
Replies: 3 comments
|
Nice catch, and thanks for the detail. I checked this against
One correction to how I first framed the fix: the ECI → commerce indicator mapping already exists ( Disclosure: I work on self-hosted Hyperswitch deployments at BetterSwitch (betterswitch.io). Happy to help review or test if you open a PR. |
|
Thanks, that is a fair correction and it is the right shape for the upstream fix. Two things I want to add, one of which is a snag in "no new mapping needed". Agreed: the mapper already exists, and the double use of
|
eciRaw |
Visa | Mastercard | Amex |
|---|---|---|---|
05 |
vbv |
spa |
aesk |
02 |
— | spa |
— |
06 |
vbv_attempted |
spa |
aesk_attempted |
So no new table. And you are right that indicator is doing two incompatible jobs: as a literal
commerce-indicator string in the complete-authorize path, and as an ECI in the UCS conversion. It
was never populated by CyberSource either way, so both usages were dead.
Snag: the mapper needs the card network, and complete-authorize does not have it
The external-authentication path calls it as get_commerce_indicator_for_external_authentication(network, eci)
because the ProcessingInformation TryFrom for PaymentsAuthorizeRouterData receives network
as a tuple element:
impl TryFrom<(&CybersourceRouterData<&PaymentsAuthorizeRouterData>, Option<PaymentSolution>, Option<String>)>
for ProcessingInformation
{
fn try_from((item, solution, network): (...)) -> Result<Self, Self::Error> {But the complete-authorize conversion — the one that actually holds
CybersourceConsumerAuthValidateResponse — receives (item, solution, three_ds_data) with no
network:
impl TryFrom<(&CybersourceRouterData<&PaymentsCompleteAuthorizeRouterData>, Option<PaymentSolution>, &CybersourceConsumerAuthValidateResponse)>
for ProcessingInformationSo reusing the mapper there means the network has to be derived in that conversion (from
item.router_data.request.payment_method_data, which is where the authorize path takes it from
when it builds the tuple). Small change, but it is not a one-line swap.
What we do today
We patch eci / eciRaw into the struct and prefer them, falling back to ecommerceIndicator
and then to internet. I would keep ecommerceIndicator as a fallback even after switching to the
mapper, because CyberSource sends it brand-correct on every response while the mapper's wildcard
arms are lossy: unknown network with ECI 05 falls through to internet, and any ECI outside
00-07 falls to vbv_failure. The field is authoritative; the mapper is the portable path.
One result worth keeping in the PR: eciRaw in the authorization is not what makes CyberSource
honour the authentication. An authorization carrying only commerceIndicator (vbv / spa /
aesk) plus cavv, xid, specificationVersion and directoryServerTransactionId already
returns processorInformation.consumerAuthenticationResponse.code = 2. Sending eciRaw is still
correct for the record, but the essential mapping is ecommerceIndicator -> commerceIndicator.
Fixtures
Happy to hand these over. All three ran the full flow (device data collection in a real browser,
then enrollment, then authorization) against the CyberSource sandbox, 3DS 2.2.0:
| Brand | Card | ecommerceIndicator |
eciRaw |
Result |
|---|---|---|---|---|
| Visa | 4000000000002701 |
vbv |
05 |
AUTHORIZED, code 2 |
| Mastercard | 5200000000002235 |
spa |
02 |
AUTHORIZED, code 2 |
| Amex | 340000002708 |
aesk |
05 |
AUTHORIZED, code 2 |
Visa response, trimmed to the fields under discussion:
"consumerAuthenticationInformation": {
"authenticationTransactionId": "zz1DGLmEmxR8ectF0nf1",
"eci": "05",
"eciRaw": "05",
"ecommerceIndicator": "vbv",
"cavv": "AJkBBkhgQQAAAE4gSEJydQAAAAA=",
"xid": "AJkBBkhgQQAAAE4gSEJydQAAAAA=",
"paresStatus": "Y",
"veresEnrolled": "Y",
"specificationVersion": "2.2.0",
"directoryServerTransactionId": "39ab3818-b27c-4c8e-9af2-3b24271a2bdb"
}Mastercard returns eciRaw: "02", ecommerceIndicator: "spa" and
ucafAuthenticationData instead of cavv, so it exercises the UCAF branch rather than the
CAVV one — worth a separate fixture.
I can turn these into connector request/response fixtures and open the PR with the struct change,
the network threaded into the complete-authorize conversion, and the mapper call. If you would
rather do the PR yourself I will hand over the JSON as-is, just say which.
Why this matters here
We are mid-certification with our acquirer (BAC Credomatic), and they validate the ECI on the
authorization. Before the fix the flow reported commerceIndicator: "internet" and the acquirer
read it as ECI 07, which is exactly the symptom you confirmed on main.
|
Hi @MileniumTick @Narayanbhat166 @neerajkumar-n and team! 👋 I have submitted an upstream fix addressing this issue in PR #15194: Summary of What Was Implemented in PR #15194:
Please feel free to review the PR: #15194. Happy to incorporate any feedback or additional test vectors! |
Uh oh!
There was an error while loading. Please reload this page.
CyberSource 3DS: authorization loses the ECI because the connector never reads
eci/eciRawSummary
With CyberSource as the payment connector, a card payment that completes a successful 3-D
Secure authentication is authorized without the commerce indicator, so CyberSource and the
issuer record it as ECI 07 (unauthenticated). The authentication itself works: PARes
Y,CAVV, XID and
directoryServerTransactionIdare all present. Only the value that carries theminto the authorization is lost.
The connector deserialises a field named
indicatorfrom the payer-authentication response.CyberSource does not send a field with that name; it sends
eci,eciRawandecommerceIndicator.indicatoris therefore alwaysNone.Affected versions
Checked on
v1.125.0,v1.126.0,v1.127.0,v1.128.0andmain: the struct and themapping are identical in all of them.
crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rsDownstream,
ProcessingInformationfor the follow-up authorization falls back to the literal"internet", which is the value that means "not authenticated":and
get_commerce_indicator_for_external_authentication()expects the numeric ECI (05,06,07), so even a non-emptyindicator(a string such asvbv) would take the default branch.What CyberSource actually returns
Captured from
POST /risk/v1/authenticationsagainst the CyberSource sandbox, Visa test card4000000000002701, 3DS 2.2.0, frictionless:Mastercard returns
eciRaw: "02",ecommerceIndicator: "spa"anducafAuthenticationDatainstead of
cavv.The authorization as this router sends it today
data.is_three_ds()is true, so the 3DS flow did run. The authorization is simply sent as if ithad not.
Minimal fix (18 lines, one file)
Add the three fields and use them:
sent eciRawis not actually required for CyberSource to honour the authentication: a minimalauthorization carrying only
commerceIndicator(vbv/spa/aesk) pluscavv,xid,specificationVersionanddirectoryServerTransactionIdalready returnsprocessorInformation.consumerAuthenticationResponse.code = 2. So the essential part isecommerceIndicator→commerceIndicator.Verification
With that mapping, three test cards through the full flow (device data collection in a real
browser, then enrollment, then authorization) produced:
commerceIndicatorsenteciRawconsumerAuthenticationResponse.code4000000000002701vbv05AUTHORIZED25200000000002235spa02AUTHORIZED2340000002708aesk05AUTHORIZED2Before the change the same flow produced
commerceIndicator: "internet", which is ECI 07.Also worth noting
hyperswitch-prism(the Unified Connector Service) carries the same mapping up to tag2026.09.30.2and releasev0.6.0; the fallback toecommerceIndicatorexists only on itsmain(commitbf871e23). A router pinned to those tags behaves the same way whether itroutes through UCS or directly.
#13527touched this area but for a different case: an externally authenticatedtransaction where the merchant supplies
three_ds_data. The native CyberSource 3DS flow stillloses the ECI.
Why it matters
Without liability shift the merchant absorbs chargebacks on transactions the cardholder did
authenticate. In our case the acquirer (BAC Credomatic) validates a sandbox certification by
looking at the ECI on the authorization, and rejected the integration for exactly this reason.
Happy to open a PR with the change and the connector tests if that is the preferred route; the
patch is small and self-contained.
All reactions