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 }
Hi Andy
ReplyDeleteThanks 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