tekin.co.uk

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:

Enable 3D Secure on your Sage Pay Account 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:

3D Authentication loaded into iframe

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.

Get more fab content like this straight to your inbox

You'll get an email whenever I have a fresh insight or tip to share. Zero spam, and you can unsubscribe whenever you like with a single click.

More articles on Ruby & Rails

Authored by Published by