Sunday, 24 January 2010

3D Secure Implementation With PayPal

In a recent blog post I documented some details for setting up direct payments with PayPal. I've recently had to extend that to implement 3D secure - known also as Verified by Visa or Mastercard SecureCode.

The primary reason I've needed to implement this in a hurry for a client is that as a deadline has been reached that mandates the technology if you wish to accept Maestro cards on the website. Hence, if anyone else like me has been asked to set this up on the site in a hurry, hopefully this post that documents the steps required and techniques will prove useful...

Project References

Paypal have a relationship with a company called Cardinal Commerce who are available to customers using PayPal Direct Payments for support with implementing the 3d secure technology. I found them very helpful in going through the process. The first step is to obtain a dll file called CMPCDotNet.dll (known as the "thin client software"). Note that the version supplied with the initial documentation is for .Net 2.0 - a version for .Net 1.1 is available too and this is what I needed to use.

Lookup Customer's 3d Secure Enrollment

The 3d secure process involves 2 steps - the first one being a look-up to see if the customer's card is enrolled in the scheme or not. To do this you make a web request passing in details of the order and the customer's card details, and receive back a response indicating their enrollment status. If they aren't enrolled, you just continue as normal with the Direct Payments process. Otherwise you need to continue with the steps described below.

Having made a project reference to the CMPCDotNet.dll, you'll be able to call methods provided by it, as illustrated in the code sample at the bottom of this post.

Redirect to 3D secure site

Having checked the response and confirmed the user is enrolled in the scheme, we need them to transfer the customer to the 3d secure site where they will be asked to enter their saved password. Before this happens though, it's necessary to save some information that will be needed again later in the process - and hence what I did here was save this data to the Session.

Once that's done the redirect can happen - but this needs to be via an HTTP Post. Hence a Response.Redirect() can't be used here, and instead the technique I used was to write an HTML form to the response stream, and have that form automatically posted via javascript.

Authentication

Once the customer completes the password entry process at the 3d secure website, control is passed back to your website to a URL passed in the initial form post. At this stage it's necessary to recreate state such as the user's card details in the form from the information saved to session.

Before continuing with processing the order via PayPal though, one last step is required which is to check whether or not the user was successfully authenticated with 3d secure. A second web request is made, passing the "payload" response received in the form post back from 3d secure (note - not the payload from the original look-up call - this caught me out for a while), and the transaction ID that was received in the first look-up request.

Again the response can be checked, and depending on the values received you can either decline the order, or proceed with charging the customer's card via PayPal Direct Payments. You will need to pass some additional details to PayPal, and also make sure you are using version 59.0 or later of the API.

Code Sample

The following code sample illustrates carries out the 3d secure authentication process described above. In order to demonstrate the key points from this blog post, it's not a complete working sample - you'll integrate your own methods for creating and retrieving orders of course - as well as set up the Direct Payments call (see my previous blog post if you need further details on this).

You'll see it also includes some code to log web requests and responses to a text file, which is useful for debugging and auditing purposes.


 1 private void Page_Load(object sender, System.EventArgs e)


 2 {


 3    //Check for post-back from 3D secure


 4    if (!Page.IsPostBack && Request.QueryString["3DSecPostBack"] == "1")


 5        Do3DSecAuthentication();


 6 }


 7 


 8 private void btnMakePayment_Click(object sender, ImageClickEventArgs e)


 9 {


 10    makePayment();


 11 }


 12 


 13 private void makePayment()


 14 {           


 15    //Create order record


 16    Order order = CreatePendingOrder();


 17 


 18    if (Page.IsValid)


 19    {


 20        //Open log file


 21        TextWriter objLogFile;


 22        objLogFile = File.AppendText(ConfigurationSettings.AppSettings["PaypalLogFilePath"]);


 23        try


 24        {


 25            //Get request details from card details form in ASPX file


 26            string cardType = ddlCardType.SelectedItem.Value;


 27            string cardNumber = txtCardNumber.Text;


 28            string expMonth = ddlExpiryDateMonth.SelectedItem.Value;


 29            string expYear = ddlExpiryDateYear.SelectedItem.Value;


 30            string expDate = expMonth + expYear;


 31            string startDate = "";


 32            if (ddlStartDateMonth.SelectedIndex > 0 && ddlStartDateYear.SelectedIndex > 0)


 33                startDate = ddlStartDateMonth.SelectedItem.Value + ddlStartDateYear.SelectedItem.Value;


 34            int issueNumber = 0;


 35            if (txtIssueNumber.Text != "")


 36            {


 37                try { issueNumber = int.Parse(txtIssueNumber.Text); }


 38                catch { }


 39            }


 40            string securityCode = txtSecurityCode.Text;


 41            string firstName = txtFirstName.Text;


 42            string lastName = txtLastName.Text;


 43            string street = txtStreet.Text;


 44            string city = txtCity.Text;


 45            string state = txtState.Text;


 46            string zip = txtZip.Text;


 47            string countryCode = "GB";


 48            string currencyCode = "GBP";


 49            string numericCurrencyCode = "826";


 50 


 51            //Do 3D secure look-up only for Maestro for now


 52            bool do3DSecure = (cardType == "Maestro");


 53 


 54            //Make first 3D secure call for look-up


 55            if (do3DSecure)


 56            {


 57                objLogFile.WriteLine("3D secure look-up commenced: " + DateTime.Now.ToString());


 58 


 59                //Set up request


 60                CentinelRequest centinelRequest = new CentinelRequest();


 61                centinelRequest.add("Version", ConfigurationSettings.AppSettings["CentinelMessageVersion"]);


 62                centinelRequest.add("MsgType", "cmpi_lookup");


 63                centinelRequest.add("ProcessorId", ConfigurationSettings.AppSettings["CentinelProcessorId"]);


 64                centinelRequest.add("MerchantId", ConfigurationSettings.AppSettings["CentinelMerchantId"]);


 65                centinelRequest.add("TransactionPwd", ConfigurationSettings.AppSettings["CentinelTransactionPwd"]);


 66                centinelRequest.add("TransactionType", "C");


 67                centinelRequest.add("Amount", ((int)(order.TotalAmount * 100)).ToString());


 68                centinelRequest.add("CurrencyCode", numericCurrencyCode);


 69                centinelRequest.add("CardNumber", cardNumber);


 70                centinelRequest.add("CardExpMonth", expMonth);


 71                centinelRequest.add("CardExpYear", expYear);


 72                centinelRequest.add("CardCode", securityCode);


 73                centinelRequest.add("OrderNumber", order.ID.ToString());


 74                centinelRequest.add("IPAddress", Request.ServerVariables["REMOTE_ADDR"]);


 75                objLogFile.WriteLine("... request prepared: " + centinelRequest.getUnparsedRequest().Replace(cardNumber, "XXXXXXXXXXXXXXXX"));


 76 


 77                //Set up variable to hold response


 78                string centinelErrorNo, centinelErrorDesc, centinelEnrolled = "U", centinelACSUrl = "",


 79                    centinelTransactionId = "", centinelPayload = "", centinelEciFlag = "", centinelTermUrl = "";


 80 


 81                //Make request and get response


 82                CentinelResponse centinelResponse = new CentinelResponse();


 83                try


 84                {                           


 85                    centinelResponse = centinelRequest.sendHTTP(ConfigurationSettings.AppSettings["CentinelTransactionUrl"], int.Parse(ConfigurationSettings.AppSettings["CentinelTimeout"]));


 86                    objLogFile.WriteLine("... response received: " + centinelResponse.getUnparsedResponse());


 87 


 88                    centinelErrorNo = centinelResponse.getValue("ErrorNo");


 89                    centinelErrorDesc = centinelResponse.getValue("ErrorDesc");


 90                    centinelEnrolled = centinelResponse.getValue("Enrolled");


 91                    centinelACSUrl = centinelResponse.getValue("ACSUrl");


 92                    centinelTransactionId = centinelResponse.getValue("TransactionId");


 93                    centinelPayload = centinelResponse.getValue("Payload");


 94                    centinelEciFlag = centinelResponse.getValue("EciFlag");


 95                    centinelTermUrl = "http" + (bool.Parse(ConfigurationSettings.AppSettings["UseSSL"]) ? "s" : "") + "://" + Request.ServerVariables["SERVER_NAME"] + Request.ServerVariables["URL"] + "?ID=" + order.ID + "&3DSecPostBack=1";


 96                }


 97                catch


 98                {


 99                    centinelErrorNo = "9040";


 100                    centinelErrorDesc = "Communication error";


 101                }


 102 


 103                //Check 3d secure response


 104                if (centinelErrorNo == "0")


 105                {


 106                    //No error, so check enrolled status


 107                    if (centinelEnrolled == "Y")


 108                    {


 109                        //Customer enrolled, so save collected details into session


 110                        Session["PendingOrderCurrencyCode"] = currencyCode;


 111                        Session["PendingOrderCardType"] = cardType;


 112                        Session["PendingOrderCardNumber"] = cardNumber;


 113                        Session["PendingOrderExpDate"] = expDate;


 114                        Session["PendingOrderStartDate"] = startDate;


 115                        Session["PendingOrderIssueNumber"] = issueNumber;


 116                        Session["PendingOrderSecurityCode"] = securityCode;


 117                        Session["PendingOrderFirstName"] = firstName;


 118                        Session["PendingOrderLastName"] = lastName;


 119                        Session["PendingOrderStreet"] = street;


 120                        Session["PendingOrderCity"] = city;


 121                        Session["PendingOrderState"] = state;


 122                        Session["PendingOrderZip"] = zip;


 123                        Session["PendingOrderCountryCode"] = countryCode;


 124                        Session["PendingOrderCentinelTransactionId"] = centinelTransactionId;


 125 


 126                        //Redirect to completion URL via form post


 127                        // - do this by writing out a form to the response stream that will submit automatically


 128                        StringBuilder centinelAuthForm = new StringBuilder();


 129                        centinelAuthForm.Append("<html>");


 130                        centinelAuthForm.Append("<body onload=\"document.auth.submit();\">");


 131                        centinelAuthForm.Append("<form name=\"auth\" action=\"").Append(centinelACSUrl).Append("\" method=\"post\">");


 132                        centinelAuthForm.Append("<input type=\"hidden\" name=\"PaReq\" value=\"").Append(centinelPayload).Append("\">");


 133                        centinelAuthForm.Append("<input type=\"hidden\" name=\"TermUrl\" value=\"").Append(centinelTermUrl).Append("\">");


 134                        centinelAuthForm.Append("<input type=\"hidden\" name=\"MD\" value=\"\">");


 135                        centinelAuthForm.Append("<p>If you are not automatically redirected, please click to proceed with authentication of your card details via 3-D Secure.</p>");


 136                        centinelAuthForm.Append("<p><input type=\"submit\" value=\"SUBMIT\"></p>");


 137                        centinelAuthForm.Append("</form>");


 138                        centinelAuthForm.Append("</body>");


 139                        centinelAuthForm.Append("</html>");


 140                        Response.Clear();


 141                        Response.Write(centinelAuthForm.ToString());


 142                        Response.End();


 143                    }


 144                    else


 145                    {


 146                        //Customer not-enrolled, so just carry on with PayPal process


 147                        DoPayPalRequest(objLogFile, orderId, currencyCode, cardType, cardNumber, expDate,


 148                            startDate, issueNumber, securityCode,


 149                            firstName, lastName, street, city, state, zip, countryCode,


 150                            "", centinelEnrolled, "", centinelEciFlag, "");


 151                    }


 152                }


 153                else


 154                {


 155                    //Error in 3d secure authentication, so exit


 156                    Display3DSecureErrors(centinelErrorNo, centinelErrorDesc);


 157                }


 158            }


 159            else


 160            {


 161                //3D secure not required, so just carry on with PayPal process without 3DS details


 162                DoPayPalRequest(objLogFile, orderId, currencyCode, cardType, cardNumber, expDate,


 163                    startDate, issueNumber, securityCode,


 164                    firstName, lastName, street, city, state, zip, countryCode,


 165                    "", "", "", "", "");


 166            }


 167        }


 168        finally


 169        {


 170            objLogFile.WriteLine("");


 171            objLogFile.Close();


 172        }


 173    }


 174 }


 175 


 176 private void Do3DSecAuthentication()


 177 {


 178    //Open log file


 179    TextWriter objLogFile;


 180    objLogFile = File.AppendText(ConfigurationSettings.AppSettings["PaypalLogFilePath"]);


 181    try


 182    {


 183        //Log and save PaResPayload posted back from 3DS


 184        string centinelPaRes = Request.Form["PaRes"];


 185        objLogFile.WriteLine("... payload posted: " + centinelPaRes);


 186 


 187        //Retrieve posted card and user details from session


 188        int orderId = int.Parse(Request.QueryString["ID"]);


 189        string currencyCode = (string)Session["PendingOrderCurrencyCode"];


 190        string cardType = (string)Session["PendingOrderCardType"];


 191        string cardNumber = (string)Session["PendingOrderCardNumber"];


 192        string expDate = (string)Session["PendingOrderExpDate"];


 193        string startDate = (string)Session["PendingOrderStartDate"];


 194        int issueNumber = (int)Session["PendingOrderIssueNumber"];


 195        string securityCode = (string)Session["PendingOrderSecurityCode"];


 196        string firstName = (string)Session["PendingOrderFirstName"];


 197        string lastName = (string)Session["PendingOrderLastName"];


 198        string street = (string)Session["PendingOrderStreet"];


 199        string city = (string)Session["PendingOrderCity"];


 200        string state = (string)Session["PendingOrderState"];


 201        string zip = (string)Session["PendingOrderZip"];


 202        string countryCode = (string)Session["PendingOrderCountryCode"];


 203 


 204        //Re-fill form


 205        ddlCardType.SelectedIndex = -1;


 206        ddlCardType.Items.FindByValue(cardType).Selected = true;


 207        txtCardNumber.Text = cardNumber;


 208        ddlExpiryDateMonth.SelectedIndex = -1;


 209        ddlExpiryDateMonth.Items.FindByValue(expDate.Substring(0, 2)).Selected = true;


 210        ddlExpiryDateYear.SelectedIndex = -1;


 211        ddlExpiryDateYear.Items.FindByValue(expDate.Substring(2)).Selected = true;


 212        if (issueNumber > 0)


 213            txtIssueNumber.Text = issueNumber.ToString();


 214        if (startDate != "")


 215        {


 216            ddlStartDateMonth.SelectedIndex = -1;


 217            ddlStartDateMonth.Items.FindByValue(startDate.Substring(0, 2)).Selected = true;


 218            ddlStartDateYear.SelectedIndex = -1;


 219            ddlStartDateYear.Items.FindByValue(startDate.Substring(2)).Selected = true;


 220        }


 221        txtSecurityCode.Text = securityCode;


 222 


 223        //Set up second 3D secure call for authentication


 224        objLogFile.WriteLine("3D secure authenticate commenced: " + DateTime.Now.ToString());


 225        CentinelRequest centinelRequest = new CentinelRequest();


 226        centinelRequest.add("Version", ConfigurationSettings.AppSettings["CentinelMessageVersion"]);


 227        centinelRequest.add("MsgType", "cmpi_authenticate");


 228        centinelRequest.add("ProcessorId", ConfigurationSettings.AppSettings["CentinelProcessorId"]);


 229        centinelRequest.add("MerchantId", ConfigurationSettings.AppSettings["CentinelMerchantId"]);


 230        centinelRequest.add("TransactionPwd", ConfigurationSettings.AppSettings["CentinelTransactionPwd"]);


 231        centinelRequest.add("TransactionType", "C");


 232        centinelRequest.add("TransactionId", (string)Session["PendingOrderCentinelTransactionId"]);


 233        centinelRequest.add("PAResPayload", centinelPaRes);


 234        objLogFile.WriteLine("... request prepared: " + centinelRequest.getUnparsedRequest());


 235 


 236        //Make request and check response


 237        CentinelResponse centinelResponse = new CentinelResponse();


 238        string centinelErrorNo, centinelErrorDesc, centinelPAResStatus = "",


 239            centinelSignatureVerification = "", centinelCavv = "", centinelEciFlag = "", centinelXid = "";


 240        try


 241        {


 242            centinelResponse = centinelRequest.sendHTTP(ConfigurationSettings.AppSettings["CentinelTransactionUrl"], int.Parse(ConfigurationSettings.AppSettings["CentinelTimeout"]));


 243            objLogFile.WriteLine("... response received: " + centinelResponse.getUnparsedResponse());


 244 


 245            centinelErrorNo = centinelResponse.getValue("ErrorNo");


 246            centinelErrorDesc = centinelResponse.getValue("ErrorDesc");


 247            centinelPAResStatus = centinelResponse.getValue("PAResStatus");


 248            centinelSignatureVerification = centinelResponse.getValue("SignatureVerification");


 249            centinelCavv = centinelResponse.getValue("Cavv");


 250            centinelEciFlag = centinelResponse.getValue("EciFlag");


 251            centinelXid = centinelResponse.getValue("Xid");


 252        }


 253        catch


 254        {


 255            centinelErrorNo = "9040";


 256            centinelErrorDesc = "Communication error";


 257        }


 258 


 259        //Check 3d secure response


 260        if (centinelErrorNo == "0")


 261        {


 262            //No error, so check response


 263            if ((centinelPAResStatus == "Y" || centinelPAResStatus == "A" || centinelPAResStatus == "U") && centinelSignatureVerification == "Y")


 264            {


 265                //Response OK, so process PayPay payment


 266                DoPayPalRequest(objLogFile, orderId, currencyCode, cardType, cardNumber, expDate,


 267                    startDate, issueNumber, securityCode,


 268                    firstName, lastName, street, city, state, zip, countryCode,


 269                    centinelPAResStatus, "Y", centinelCavv, centinelEciFlag, centinelXid);


 270            }


 271            else


 272            {


 273                //Response not OK, so can't go further


 274                lblResultMessage.Text = "Sorry, but we cannot complete your order as your card has not been authorised with 3D secure";


 275            }


 276        }


 277        else


 278        {


 279            //Error in response


 280            Display3DSecureErrors(centinelErrorNo, centinelErrorDesc);


 281        }


 282    }


 283    finally


 284    {


 285        objLogFile.WriteLine("");


 286        objLogFile.Close();


 287    }


 288 }


 289 


 290 private void DoPayPalRequest(TextWriter objLogFile, int orderId, string currencyCode,


 291    string cardType, string cardNumber, string expDate,


 292    string startDate, int issueNumber, string securityCode,


 293    string firstName, string lastName, string street, string city, string state, string zip, string countryCode,


 294    string centinelPAResStatus, string centinelEnrolled, string centinelCavv, string centinelEciFlag, string centinelXid)


 295 {


 296    //Make payment with PayPal Direct Payments...


 297 }


 298 


 299 private void Display3DSecureErrors(string centinelErrorNo, string centinelErrorDesc)


 300 {


 301    lblResultMessage.Text = "Error in 3D secure authentication";


 302    lblResultMessage.Text += "<ul>";


 303    string[] centinelErrorNos = centinelErrorNo.Split(',');


 304    string[] centinelErrorDescs = centinelErrorDesc.Split(',');


 305    for (int i = 0; i < centinelErrorNos.Length; i++)


 306        lblResultMessage.Text += "<li>" + centinelErrorDescs[i] + " (" + centinelErrorNos[i].Trim() + ")" + "</li>";


 307    lblResultMessage.Text += "</ul>";


 308 }


1 comment:

  1. Hi Andy

    Thanks for sharing this important information.
    I have got a one question to ask you.

    Why centinel not display Bank verify window? it goes sraight to Line no 266 - DoPayPalRequest()

    Thanks
    sajeelmunir@aol.com

    ReplyDelete