3D Secure in Rails with Active Merchant and Sage Pay (formally ProTX)
A detailed guide for adding 3D Secure (Verified by Visa, etc) support to your Rails application’s checkout process using the Active Merchant gem.
The Pain That is 3D Secure
3D Secure is like the project manager of online payments: it’s meant to be helpful, but in reality it’s nothing but a pain in the ass. On the surface it provides a further level of authentication for consumers making online payments. Visa and Mastercard present it as a security enhancement that shifts liability away from merchants whilst also providing added safety to customers against fraud. Unfortunately it has been very poorly promoted, and manages to both confuse consumers and make the checkout process more complicated at the same time. As a result, merchant uptake has been slow, and despite regular threats from the card companies to make it compulsory, at the time of writing you can still take online payments without it. Well, almost — if you want to accept Maestro payments here in the UK, then chances are you’ll need to get them 3D Secured.
3D Secure Enabled Active Merchant for Sage Pay
To this end, I’ve forked Active Merchant and added 3D Secure Support to the ProTX gateway. Adding 3D Secure support to other gateways shouldn’t be too difficult. For some info on potential differences between how Sage Pay and other gateways do 3D Secure, see the end of this post.
Warning and Caveats
What follows is a rough guide on how you might perform 3D Secure authentication with Sage Pay. How you actually perform the authentication process will be application specific so don’t expect things to work if you blindly copy and paste the following code into your app. The aim is to give you enough of an overview of how the process works and how my fork provides the means to complete the steps required.
As part of that, I’m also assuming that you already understand and use Active Merchant for payment processing with Sage Pay and also have an understanding of the 3D Secure protocol itself. If not, there is plenty of information out there.
Finally, make sure you have good test coverage for your 3D Secure enabled code as this is a complex process with lots of potential pitfalls. To help you with this, the Bogus gateway has been updated with 3D Secure support too.
On to the Code
Firstly, once you have my fork from github, you need to enable 3D Secure on your Sage Pay test accounts administrator page:
You will need to pass in :enable_3d_secure => true
when you instantiate your gateway:
gateway = ActiveMerchant::Billing::ProtxGateway.new(
{
:login => 'test',
:password => 'password',
:enable_3d_secure => true
}
)
Changes to the Response and Gateway
The Response</span> object now behaves subtly
differently. In standard Active Merchant, a
Response</span> will either be successful or not:
response = gateway.purchase(100, credit_card)
if response.success?
puts "Purchase successfully made"
else
puts "Purchase not authorised, try again"
end
With 3D Secure enabled, an unsuccessful transaction hasn’t necessarily failed, it might simply require additional 3D Authentication:
response = gateway.purchase(100, credit_card)
if response.success?
puts "Purchase successfully made"
elsif response.three_d_secure?
puts "Purchase requires additional 3D Authentication"
else
puts "Purchase not authorised, try again"
end
In this case, the Response
object will contain
the additional parameters you need to perform 3D Authentication: the
PaReq, MD and ACS Url.
You use these parameters to redirect the user to their issuing bank
where they are asked to authenticate themselves by providing their
password. Once they have completed authentication, the user is
redirected back to your site with the results of the authentication,
which you then have to send back to the gateway to ‘complete’ the
transaction. You perform this with the gateway’s brand new
three_d_complete
method:
response = gateway.three_d_complete(pa_res, md)
if response.success?
puts "Authentication complete and purchase successfully made"
else
puts "Purchase not authorised or authentication failed, try again"
end
Makes sense so far? Don’t worry, it only gets more confusing…
Performing the 3D Authentication in Your App
Assuming you are doing things RESTfully, your 3D Secure enabled
PaymentsController
may end up looking
something like this:
def create
@credit_card = ActiveMerchant::Billing::CreditCard.new(params[:credit_card])
@billing_address = Address.new(params[:address])
@payment = @order.payments.create(:credit_card => @credit_card, :address => @billing_address)
if @payment.success?
redirect_to complete_order_url(@order)
elsif @payment.requires_authentication?
# A view with an iframe from which the user is redirected to the authentication page
render :action => 'three_d_iframe'
else
flash[:notice] = 'Your payment was unsuccessful'
render :action => 'new'
end
end
# the redirect form that loads into the iframe
def three_d_form
render :layout => false
end
# the action where users are redirected to once they have completed authentication
def three_d_complete
@response = @order.complete_three_d_secure(params[:PaRes], params[:MD])
if @response.success?
render :action => 'verification_complete', layout => false
else
render :action => 'verification_failed', :layout => false
end
end
Here, my Payment
model encapsulates the call
to the gateway and processes the response to see if it was successful or
requires further 3D authentication. If authentication is required, the
controller renders an iframe, into which the
three_d_form
action is loaded which
redirects the user to their issuing bank for authentication:
Here, the orange section is the iFrame with the bank’s authentication page loaded. The three_d_iframe view code looks something like this:
<h2>Card Verification and Authorisation</h2>
<p>A message explaining what is happening and what is required of the user.</p>
<iframe src="<%= three_d_form_order_payment_path(@order, :acs_url => @payment.acs_url, :md => @payment.md, :pa_req => @payment.pa_req) %>" name="3diframe" width="350" height="500" frameborder="0">
<p>Your Browser does not support iframes. To verify your card and complete this transaction, please use a browser that does.</p>
</iframe>
It generates a form that redirects the user to their issuing bank for
authentication. As well as the PaReq and MD, the TermUrl is sent as the
three_d_complete
action where we want to
receive the callback with the result:
<% form_tag(params[:acs_url], :id => '3dform') do %>
<%= hidden_field_tag :PaReq, params[:pa_req] %>
<%= hidden_field_tag :MD, params[:md] %>
<%= hidden_field_tag :TermUrl, three_d_complete_order_payment_url(@order) %>
<noscript>
<p>Click the button below to continue with verification.</p>
<%= submit_tag 'Continue with Card Verfication' %>
</noscript>
<% end %>
<% javascript_tag do %>
window.onload=function(){
document.getElementById('3dform').submit();
}
<% end %>
Once/if the user completes authentication, they are redirected back to
the three_d_complete
action and we make the
three_d_complete
call back to the gateway to
see if the transaction has been authorised or not (see the
three_d_complete
action in the controller
code above).
Finally, we break out of the iframe by rendering either
verification_complete
or
verification_failed
, depending on the result
of the three_d_complete
call:
<% form_tag complete_order_path(@order), :id => 'reload_frame', :target => '_top', :method => 'get' do %>
<noscript>
<p>Verification complete, click the button below to continue.</p>
<%= submit_tag 'Continue to order confirmation %>
</noscript>
<% end %>
<% javascript_tag do %>
window.onload=function(){
document.getElementById('reload_frame').submit();
}
<% end %>
And:
<% form_tag new_order_payment_path(@order), :id => 'reload_frame', :target => '_top', :method => 'get' do %>
<%= hidden_field_tag :verification_failed, true %>
<noscript>
<p>Verification failed, click the button below to try again.</p>
<%= submit_tag 'Try again' %>
</noscript>
<% end %>
<% javascript_tag do %>
window.onload=function(){
document.getElementById('reload_frame').submit();
}
<% end %>
When you want to skip 3D Secure
You can also force 3D Secure off on a per-transaction basis, simply pass
:skip_3d_secure => true
with your options
when you call authorize
or
purchase
. I use this on some sites to turn 3D
Secure off for all except Maestro payments so as to minimise the risk of
order abandonment.
Warnings and Caveats Again
Not to sound like a broken record, but let me re-iterate that although the above code demonstrates the basics of the authentication process, it is very much sudo-code and omits much of the app specific business logic that you will need in your models. Therefore, don’t expect a copy/paste job to work, and don’t forget to test, test and more test.
Implementing 3D Secure on Other Gateways
I was hoping that this work would form the basis for a generic implementation of 3D Secure in Active Merchant. Unfortunately it would appear that it’s not as straight forward as I’d imagined. It seems that Sage Pay has a slightly different approach to 3D Authentication compared to other gateways. Sage Pay requires you make a standard authorisation attempt and sends you a special response if 3D Authentication is required. You then authenticate the user and send the authentication result back to Sage Pay to ‘complete’ your original authorisation attempt.
Other gateways (Payflow Pro for example) have a slightly different approach, and require a call to an authenticate method before you attempt an authorisation to check if 3D Authentication is required. So you perform the authentication first, then make an authorisation call, sending the authentication results as well as the payment details. So unlike with Sage Pay, you need to send the users payment details twice, once for authenticate, then again for the authorisation.
These differences make a common API for 3D Authentication in Active
Merchant tricky. One approach might be to encapsulate the authenticate
call into the respective authorize
and
purchase
methods, mirroring Sage Pays
behaviour, and have the three_d_complete
method make the authorisation calls as necessary. This would require
some potentially messy conditional logic in the
authorize
and
purchase
methods and may not be the best
approach.
I do have some ideas about how you could potentially make Sage Pay replicate the API of gateways like Payflow Pro. Unfortunately I haven’t managed to acquire a 3D Secure enabled Payflow Pro test account to try out this theory. If you can help out with this, drop me a line and who knows, maybe we can finally get a 3D Secure API into Active Merchant.