Zebra Codes

3D Secure v2 Payments Failing Randomly

1st of September, 2021

The Payment Services Directive 2 (PSD2) is a piece of EU and UK legislation that, among other things, mandates the use of a security protocol such as 3D Secure. 3D Secure until this point has been optional, but now it will be required for all online transactions. As well as now becoming mandatory, the protocol has been completely revised into version 2.

Upon adding 3D Secure v2 capability to my direct integration with Opayo (and upgrading from VPS Protocol v3 to v4, as is required), I found that certain customer transactions were being rejected by their bank, despite the majority going through successfully. No error message is given, just a failure response, and the session data threeDSSessionData field being blank.

The request to the customer’s bank is passed in a form, like this:

<form action="ACSURL" method="post">
    <input type="hidden" name="threeDSSessionData" value="TXID">
    <input type="hidden" name="creq" value="CREQ">
    <input type="hidden" name="TermUrl" value="TERMURL">
</form>

The values in the fields come from Opayo when it returns a response with Status=3DAUTH:

  • ACSURL: The bank’s 3D Secure endpoint.
  • TXID: The VPSTxId given by Opayo, for finding the transaction when the response comes back.
  • CREQ: The 3D Secure v2 data blob.
  • TERMURL: The URL to which the 3D Secure response is posted.

When this form is submitted, the customer is taken to their bank’s page. They input their password, and then get taken back to your own website at TERMURL. TERMURL receives some POST data: The bank’s response in cres and the transaction ID in threeDSSessionData.

The cres response is an opaque blob that you pass back to Opayo in order to complete the transaction. Opayo then response with either success or failure. In this case, a small number of transactions were indicated as failing 3D Secure, when they should have passed.

There is no further information available, so I did some digging. Looking at the creq and cres fields it appeared that they were base 64 encoded, and sure enough, they were actually JSON:

{
  "messageType": "CReq",
  "messageVersion": "2.1.0",
  "threeDSServerTransID": "ff67b49e-47ab-4a7f-9e1a-e16199387782",
  "acsTransID": "fb154a3f-5b5a-4890-8614-a818b20113fa",
  "challengeWindowSize": "03"
}
{
  "messageType": "Erro",
  "messageVersion": "2.1.0",
  "acsTransID": "fb154a3f-5b5a-4890-8614-a818b20113fa",
  "errorCode": "203",
  "errorComponent":"A",
  "errorDescription": "Data element not in the required format or value is invalid as defined in Table A.1.",
  "errorDetail": "threeDSSessionData",
  "errorMessageType": "CReq"
}

The response here provides some valuable insight: The problem lies with the threeDSSessionData, and its value does not match the required format. I managed to find the 3D Secure Protocol Specification v2.1.0 and the table A.1 mentioned in the error message. This leads to table A.5.4, “Browser CReq and CRes POST”. This specifies that the threeDSSessionData field must be base64-url encoded, something that the Opayo documentation does not mention.

Opayo recommends that the transaction ID VPSTxId is used as the threeDSSessionData, and this is of the format {a-b-c-d} – the braces are not part of the base64 character set, and hence the message is rejected as invalid by some banks.

The solution is simple: base64-url encode the value placed into the threeDSSessionData field before sending it to the bank, and then base64-url decode the value that you receive back.

Note that base64-url encoding is not standard base64. The URL-safe version of base64 (RFC4648) replaces ‘+/’ with ‘-_’, and removes the trailing ‘=’ padding, because these characters have special meaning in URLs.