I've recently had the misfortune of having to integrate web-based payment software with Visa's Cybersource REST API using their Microforms integration. Unfortunately, the documentation is somewhat lacking and support is virtually non-existent.
Fortunately I'm a tenacious thing and with much trial and error managed to get it working. I'm posting this information here in the hope it saves someone else the hair-tearing it cost me.
First, let me explain what Microforms are: they are iframes hosted at Cybersource themselves but embedded on your site to collect the sensitive credit card data without you having to worry about PCI compliance. You request a "Server Side Capture Context" from the API and use this to initialise a javascript object which creates the fields on your page. When the data has been entered, a token is requested that contains the encrypted card data and then when all user processing has been completed, this can be sent on to Cybersource to confirm the payment.
The first step was to get an account on their Sandbox at https://developer.cybersource.com/hello-world/sandbox.html
Even this step was painful, as my first five attempts at registration failed to send me a confirmation email with my account credentials, so I was unable to log in. And yes, I did check spam. Eventually, though, I did get an email and was able to log in and create a Shared Key.
The next problem I had was that the instructions for how to create the authentication header for messages to the API were vague, contradictory and incomplete. The documentation says that the message data need only contains a targetOrigins property, thus:
{ "targetOrigins": &["http://localhost"&] }
Only this isn't the case. If you only send this you get an error back from the API. It turns out you also need to request the fields you want to display in the microform too:
{
"fields\": {
"paymentInformation": {
"card": {
"number": { },
"securityCode": {
"required": false
}
}
}
},
"targetOrigins": &["https://localhost"&]
}
So that's the payload sorted out, now for the hard part: the Authentication header.
Cybersource gives you the choice of a JWT certificate or an HTTP signature. I'm using the latter because it seemed easier to set up. The signature is a hashed copy of all the headers being included in the message and a few additional bits. Deciding exactly what to include took me ages as when it didn't work there was no indication of what part I'd got wrong: the header, the signature, the payload, or something else? For brevity, I'm going to just post the PHP code that got this working (with my login credentials replaced, of course):
<?php
$url = 'https://apitest.cybersource.com/microform/v2/sessions';
$merchantId = 'testres';
$keyId = '08c94330-f618-42a3-b09d-e1e43be5efda';
$sharedKey = 'yBJxy6LjM2TmcPGu+GaJrHtkke25fPpUX+UY6/L/1tE=';
$host = 'apitest.cybersource.com';
$data =
"{
\"fields\": {
\"paymentInformation\": {
\"card\": {
\"number\": { },
\"securityCode\": {
\"required\": false
}
}
}
},
\"targetOrigins\": &[\"https://localhost\"&]
}
";
$hash = hash("sha256", $data, true);
$digest = base64_encode($hash);
$date = date("D, d M Y G:i:s")." GMT";
$sigstring = "host: apitest.cybersource.com\ndate: $date\n(request-target): post /microform/v2/sessions\ndigest: SHA-256=$digest\nv-c-merchant-id: $merchantId";
$sigstringbytes = utf8_encode($sigstring);
echo "$sigstring\n";
$rawkey = base64_decode($sharedKey);
$hashval = hash_hmac("sha256", $sigstringbytes, $rawkey, true);
$sighash = base64_encode($hashval);
$headers = array(
"Accept-Language: en-GB,en;q=0.5",
"Accept-Encoding: gzip, deflate, br",
"digest: SHA-256=".$digest,
"v-c-merchant-id: ".$merchantId,
"v-c-date: ".$date,
"date: ".$date,
"signature: keyid=\"".$keyId."\", algorithm=\"HmacSHA256\", headers=\"host date (request-target) digest v-c-merchant-id\", signature=\"".$sighash."\"",
"host: apitest.cybersource.com",
"Content-Type: application/json"
);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
//curl_setopt($curl, CURLOPT_PROXY, "127.0.0.1:8888"); // Uncomment to see traffic in a traffic analyser like Fiddler
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$resp = curl_exec($curl);
curl_close($curl);
echo $resp."\n";
?>
The tricky part was working out which headers to include and in what order. The headers here are all necessary (in my experience) to get anything other than a 401 error back from the API.
The digest is easy: it's just a base64 encoded version of a SHA256 hash of the packet data (the JSON request).
The order of the headers in the signature is also crucial, it would seem. Once all of this is set up correctly, a call to the endpoint should return a capture context in the form of a public key:
header.payload.signature
The whole thing should be passed to the javascript side of things to initialise the Flex() object and create the microform. From there, the card number and CVV (and expiry month and year) can be collected and then either the normal, non-3D Secure, endpoint can be contacted or the Payer Authentication endpoint can be used to perform proper security checks on the card. I'll cover that in the next post.