Strong Customer Authentication - Sagepay Direct v4.00

07 July 2019

Online card payments within the EEA are changing this summer with the introduction of new Strong Customer Authentication (SCA) for transactions over €30. The growth in online card fraud means that from September 14th this year individuals paying online may be challenged for additional identification. The process will be similar to the existing 3D Secure challenge but will make use of additional forms of identification. The detail has been written about many times elsewhere but for owners and developers of sites with online payments the changes may require code updates depending on the nature of the checkout.

Note that as I write the UK regulator has granted an 18-month phase in period too allow banks and business more time to accommodate the new requirements. This means that full enforcement for UK cards may not occur until 2021, however I'm not sure what this means for European card customers to UK businesses.

I have a number of sites to update using a mixture of Stripe and Sagepay checkouts. Stripe updated their API a few months ago which meant I was able to complete all of those integrations in good time. Sagepay however are only recently rolling out their changes in support of SCA which means time may be short for busy developers. With that in mind I thought I would document my approach to moving from the existing Sagepay Direct Protocol v3.00 to v4.00 in case it is of assistance to others.

In essence the change is quite simple, in addition to some minor changes to the content of the 3D secure requests and callbacks, there are nine new fields that must be sent with the initial call to the API. Eight of these may be optional depending on whether or not the client browser has javascript enabled; in reality you may as well consider all nine as mandatory.

Truth be told I am a little baffled about the requirement for some of these fields. As I understand it they are primarily required for the rendering of the challenge window, but why they are required in the context of a modern responsive web, and why the burden is placed on the developer/client to provide these explicitly is not known; rather like the existing requirement to explicitly send the card type with the initial api call. Card type can easily be derived from card number so why the burden is placed on the client to explicitly  choose a card at checkout, or on the developer to do some card type detection, again is unknown when it could easily be done on receipt by Sagepay. Sagepay Direct has been around a while however so I suspect there may be a large amount of legacy code involved. I would welcome being corrected on this.

Happily the changes are easy to implement.

Payment Form Changes

Much of requirement can be captured by dynamically appending additional fields to the client payment form with some simple javascript and looking for those fields in your payment controller:

var paymentform = document.getElementById('payment_form');

	//has javascript
	var a = document.createElement("INPUT");
	a.setAttribute("type", "hidden");
	a.setAttribute("value", "1");
	a.setAttribute("name", "BrowserJavascriptEnabled");

	var b = document.createElement("INPUT");
	b.setAttribute("type", "hidden");
	b.setAttribute("name", "BrowserJavaEnabled");
		b.setAttribute("value","1" );
		b.setAttribute("value", "0");

	var c = document.createElement("INPUT");
	c.setAttribute("type", "hidden");
	c.setAttribute("value", window.screen.colorDepth);
	c.setAttribute("name", "BrowserColorDepth");

	var d = document.createElement("INPUT");
	d.setAttribute("type", "hidden");
	d.setAttribute("value", window.screen.height);
	d.setAttribute("name", "BrowserScreenHeight");

	var e = document.createElement("INPUT");
	e.setAttribute("type", "hidden");
	e.setAttribute("value", window.screen.width);
	e.setAttribute("name", "BrowserScreenWidth");

	var tzoffset = new Date().getTimezoneOffset();
	var f = document.createElement("INPUT");
	f.setAttribute("type", "hidden");
	f.setAttribute("value", tzoffset);
	f.setAttribute("name", "BrowserTZ");

var g = document.createElement("INPUT");
	g.setAttribute("type", "hidden");
	g.setAttribute("value", window.navigator.language);
	g.setAttribute("name", "BrowserLanguage");


That's all for the front end changes.


Transaction Controller

In your transaction controller it's a matter of simply appending the additional fields to your post to the Sagepay API. I incorporated the updated by capturing the extra fields as a new array of additional data and then merging it with the existing prior to posting. It was just clearer for me.

$additional = array();

if(isset($_POST['BrowserJavascriptEnabled']) {
	$additional['BrowserJavascriptEnabled'] = filter_var(1,FILTER_VALIDATE_BOOLEAN);
	$additional['BrowserJavaEnabled'] = filter_var($_POST['BrowserJavaEnabled'],FILTER_VALIDATE_BOOLEAN);
	$additional['BrowserColorDepth'] = $_POST['BrowserColorDepth'];
	$additional['BrowserScreenHeight'] = $_POST['BrowserScreenHeight'];
	$additional['BrowserScreenWidth'] = $_POST['BrowserScreenWidth'];
	$additional['BrowserTZ'] = $_POST['BrowserTZ'];
	$additional['BrowserLanguage'] = $_POST['BrowserLanguage'];
	$additional['BrowserJavascriptEnabled'] = filter_var(0,FILTER_VALIDATE_BOOLEAN);
	$additional['BrowserLanguage'] = "en-GB";

$additional['BrowserAcceptHeader'] = !empty($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : 'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8';
$additional['BrowserUserAgent']  = $_SERVER['HTTP_USER_AGENT'];

It is also necessary to provide the challenge window size as one of five choices, 01 through 05, each corresponding to the following:

I decided just one of 3 would cover my requirements adequately so...

$BrowserScreenWidth = $_POST['BrowserScreenWidth');

switch (true) {
    case $BrowserScreenWidth >= 1200:
        $ChallengeWindowSize = '04'; //600x400;
        $iframe_width = '600';
        $iframe_height = '400';

    case $BrowserScreenWidth >= 1000:
        $ChallengeWindowSize = '02'; //390 x 400;
        $iframe_width = '390';
        $iframe_height = '400';

        $ChallengeWindowSize = '01'; //250 x 400;
        $iframe_width = '250';
        $iframe_height = '400';

$additional['ChallengeWindowSize'] = $ChallengeWindowSize;

Note also that with the old v3.00 protocol the 3D callback URL was provided in the 3D secure post itself, with protocol v4.00 you need to provide this with the initial post:

$additional['ThreeDSNotificationURL'] = your_3D_secure_callback_url;

For the post itself,

$postdata = array_merge($protocol_3_postdata, $additional);


3D Secure Handling

If the returned result from Sagepay indicates that 3D Secure is required then there are two possibilities, that an updated SCA ( 3DS2) challenge is required, or that a fallback to the current 3DS1 standard is required.

in the event that 3DS2 is mandated then Sagepay will return a CReq field in the response object, if a fallback is being used then a PAReq and MD fields will be returned instead. Note also that only with 3DS2 will Sagepay return a VPSTxId in the response object.

In my controller then,

if(!empty($result['response']['CReq'])){  //3DS2
	$tdata = array(
		'ACSURL' =>	$result['response']['ACSURL'],
		'CReq' =>	$result['response']['CReq'],
		'VPSTxId' =>	$result['response']['VPSTxId']
	$tdata = array(
		'ACSURL' =>	$result['response']['ACSURL'],
		'PAReq' =>	$result['response']['PAReq'],
		'callback_url' =>	$three_d_callback_url,
		'MD' =>	$result['response']['MD']

The tdata array is stored in the session for use by the script that provides the source for the challenge iframe, which is loaded with the width and height parameters determined earlier.

The content for the self-submitting form differs slightly depending on whether 3DS2 is being used, or the fallback, in which case the parameters to be the sent are no different to protocol v3.0. For 3DS2 send the CReq and VPSTxId in fields name 'cres' and 'threeDSSessionData' respectively, noting lower case for 'cres' field name. For the 3DS1 fallback send PAReq and MD as before.


3D Callback Script

The paremeters sent back to your 3D Callback URL vary slightly dependng on whether 3DS2 or the 3DS1 fallback is being used. In the latter case nothing has changed from protocol v3.0, you post the returned PARes and MD parameters as before. In the case of 3DS2 you'll have received a CRes parameter instead to be posted together with the VPSTxId. Note that in the post you receive from Sagepay the CRes parameter will be returned in an all lower case field name, cres, but you must post it as CRes.


That's pretty much it. After some initial teething problems the Sagepay Test server supports the new protocol but there is no date yet for the changes to be migrated to the live server.

You can download the integration guidelines for Direct Protocol v4.0 from this page.