Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Paypal Payflow Transparent Redirect, SecureToken with AJAX?

I'm working on a C# VS2012 Framework 4.5 MVC application that is trying to become PCI compliant using Payflow Pro (https://pilot-payflowpro.paypal.com). We've been using PayflowPro for years, and this is what I have to use. From my reading it seems that I should use the Transparent Redirect so I'm not posting anything private to my webserver, though I don't know if I need that with how I'm hoping to handle this. I also have a few questions...

How I think this all works: My understanding is that you need a securetoken (communication to Paypal, trip 1). Then you post the secure data (CC, exp, security code) including the securetoken (communication to Paypal, trip 2) and receive the authorization and transactionID of the sale.

How I'm hoping to do it: I'm intending on having a form that will have all the info (user details, shipping details, and CC info), and when the user presses the purchase button, I'll use AJAX to process trip 1 to my server (no secure user info sent). Here I'll create the URL + params and send paypal my un/pw info to retrieve the token (all from my server). The response will be returned to the client and, if successful, I'll then directly communicate via AJAX to Paypal's Gateway server, this time sending the secure CC info + token (trip #2). Based on the response to trip #2, I'll let the user know what's up with their purchase. Trip 2 shouldn't need my Paypal UN/PW info as it could easily be see on the client, and I'm including the SecureToken which SHOULD identify the original transaction. From what I've explained I don't see a need for Transparent Redirect. Or am I missing something here?

Also, what Transaction Type do I want to use? Create an 'Authorization' for trip #1, then a 'Sale' for trip #2?

So here's the nitty gritty coding type stuff: For my R&D testing I'm building my own name/value pair parameter string (see below) and communicating to the gateway server via WebRequest through their sandbox/test url (pilot-payflowpro.paypal.com). I do get a successful response and SECURETOKEN back. Initial request (shown below) for secure token is TRXTYPE = A (Authorization), no card info is sent. Do I want to authorize first?

Here are my parameters (might include shipto info as well, but it's not listed below):

USER=myAuthUserName
&VENDOR=myAuthUserName
&PARTNER=myPartner
&PWD=myPassword
&AMT=21.43
&BILLTOFIRSTNAME=FName
&BILLTOLASTNAME=LName
&BILLTOSTREET=123 Main Street
&BILLTOSTREET2=Apt 203B
&BILLTOCITY=MyCity
&BILLTOSTATE=CA
&BILLTOZIP=77777
&BILLTOPHONENUM=4444444444
&[email protected]
&CURRENCY=USD
**&TRXTYPE=A**
&SILENTTRAN=TRUE
&CREATESECURETOKEN=Y
&SECURETOKENID=a99998afe2474b1b82c8214c0824df99

As I said, I get a successful response and move to the next step of sending the secure data (CC#, EXPDATE, security code). When I remove my UN/PW/VENDOR/Partner info from the params I get an error due to invalid user authentication. But, seeing I'm dynamically building this 2nd call I can't have my paypal un/pw there. What am I missing? Anyone offer assistance with this or the other questions from above?

Please let me know if I need any clarification to be added. Thanks in advance for your time!

like image 646
RichieMN Avatar asked Feb 26 '15 19:02

RichieMN


2 Answers

After spending a bunch of time with a Paypal engineer I've successfully figured out a solution for the Paypal's Payflow Transparent Redirect without hosted pages (have own payment page). Again, here's the documentation which, per the engineer, is pretty confusing: Payflow API Documentation. Also, the code isn't optimized as it was just a R&D app, but as a whole, it is working for me. Just an example and explanation, and I'm sure there are better ways of doing individual steps. Hope this helps and allows you to bypass some of the roadblocks that have been slowing down your Paypal Payflow integration.

YES, it is PCI compliant in that no secure customer data will hit your own servers. Remember that PCI compliance is pretty complicated and involved but this is big part of it. Ok, so I'll explain what I did to make this work in a MVC C# environment. I'll explain the steps here, then include code below.

  1. CLIENT: Client finishes adding items to the cart and presses BUY button. Javascript handles the button click, doesn't submit, and takes you to the next step.
  2. CLIENT --> SERVER: AJAX function POSTS to server method to contact Paypal for the single-use secure token. This communication identifies YOU (the merchant) to paypal with your authentication, a unique transaction id (a guid), and non secure details about the transaction (total, billing info, shipping info, return URL details). This way, all your merchant personal acct info is secure (web server to Paypal).
  3. SERVER --> CLIENT: From the transaction above you'll receive a parameter string that contains the secure token (among other stuff, see method with example). Using this piece of info, I dynamically create my url that I'll eventually need on the client for the transparent redirect part, and send the url string back to the client.
  4. CLIENT: Using the url that was returned in step #3, I complete the URL by adding the needed credit card parameters using jQuery.
  5. CLIENT --> PAYPAL: This is where I didn't understand what to do. While step #2 was a post, this step will be a REDIRECT. Sure, that seems appropriate seeing it's called 'transparent redirect', but that part just didn't make sense to me. So, once your entire URL is complete, you'll literally redirect the window to Paypal for processing your transaction.
  6. PAYPAL --> SERVER: PayPal posts back to one of the URLs you included in step 2 (to a public method on one of my controllers), and I read the response object and parse the parameters.

Easy, right? Perhaps, but for me step 5 caused me big problems. I was using a POST and didn't understand why I kept getting errors on the response. It was an html page with something about an invalid merchant or authentication. Remember to redirect, not post for step #5.

CODE:

STEP 1: onclick attribute on button to call GetToken function.

STEP 2 and STEP 3:

client-side:

function GetToken() {
$.ajax({
    url: '@Url.Action("GetToken", "MyController")',
    type: 'POST',
    cache: 'false',
    contentType: 'application/json; charset=utf-8',
    dataType: 'text',
    success: function (data) {
        // data is already formatted in parameter string
        SendCCDetailsToPaypal(data);
    },
    //error: 
    //TODO Handle the BAD stuff 
});}

Server Side:

I have separate methods used to build all the parameter values needed for the token request. First three build: authentication, transaction details, transparent redirect. I keep urls and payflow acct info in a web.config file. Last method, ProcessTokenTransaction, does all the heavy lifting to contact Paypal via WebRequest, and then parse it into the URL that will be sent back to the client. This method should be refactored for a cleaner delivery, but I'll leave that up to you. ParseResponse is a method that populates a simple model that I created, and returns that model.

URL for token (sandbox): https://pilot-payflowpro.paypal.com

THIS IS DIFFERENT THAN THE TOKEN URL!! Used in the PaypalTranactionAPI config value.

URL for transaction: (sandbox) https://pilot-payflowlink.paypal.com

private  string PrepareApiAuthenticationParams()        
    {
        var paypalUser = ConfigurationManager.AppSettings["PaypalUser"];
        var paypalVendor = ConfigurationManager.AppSettings["PaypalVendor"];
        var paypalPartner = ConfigurationManager.AppSettings["PaypalPartner"];
        var paypalPw = ConfigurationManager.AppSettings["PaypalPwd"];

        //var amount = (decimal)19.53;

        var apiParams = @"USER=" + paypalUser
                        + "&VENDOR=" + paypalVendor
                        + "&PARTNER=" + paypalPartner
                        + "&PWD=" + paypalPw
                        + "&TENDER=C"
                        + "&TRXTYPE=A"
                        + "&VERBOSITY=HIGH";

        // find more appropriate place for this param
        //+ "&VERBOSITY=HIGH";

        return apiParams;
    }


    private  string PrepareTransactionParams(CustomerDetail detail)
    {
        var currencyType = "USD";

        var transactionParams = @"&BILLTOFIRSTNAME=" + detail.FirstName
                                + "&BILLTOLASTNAME=" + detail.LastName
                                + "&BILLTOSTREET=" + detail.Address1
                                + "&BILLTOSTREET2=" + detail.Address2
                                + "&BILLTOCITY=" + detail.City
                                + "&BILLTOSTATE=" + detail.State
            //+ "&BILLTOCOUNTRY=" + detail.Country +  // NEEDS 3 digit country code
                                + "&BILLTOZIP=" + detail.Zip
                                + "&BILLTOPHONENUM=" + detail.PhoneNum
                                + "&EMAIL=" + detail.Email
                                + "&CURRENCY=" + currencyType
                                + "&AMT=" + GET_VALUE_FROM_DB
                                + "&ERRORURL= " + HostUrl + "/Checkout/Error"
                                + "&CANCELURL=" + HostUrl + "/Checkout/Cancel"
                                + "&RETURNURL=" + HostUrl + "/Checkout/Success";   

        // ADD SHIPTO info for address validation

        return transactionParams;
    }


private  string PrepareTransparentParams(string requestId, string transType)
    {
        var transparentParams = @"&TRXTYPE=" + transType +
                               "&SILENTTRAN=TRUE" +
                               "&CREATESECURETOKEN=Y" +
                               "&SECURETOKENID=" + requestId;

        return transparentParams;
    }


    // Method to build parameter string, and create webrequest object
public string ProcessTokenTransaction()
    {
        var result = "RESULT=0"; // default failure response
        var transactionType = "A";
        var secureToken = string.Empty;
        var requestId = Guid.NewGuid().ToString().Replace("-", string.Empty);

        var baseUrl = ConfigurationManager.AppSettings["PaypalGatewayAPI"];            

        var apiAuthenticationParams = PrepareApiAuthenticationParams();

        // Create url parameter name/value parameter string
        var apiTransactionParams = PrepareTransactionParams(detail);

        // PCI compliance, Create url parameter name/value parameter string specific to TRANSAPARENT PROCESSING 
        var transparentParams = PrepareTransparentParams(requestId, transactionType);

        var url = baseUrl;
        var parameters = apiAuthenticationParams + apiTransactionParams + transparentParams;


        // base api url + required 
        var request = (HttpWebRequest)WebRequest.Create(url);
        request.Method = "POST";
        request.ContentType = "text/name"; // Payflow?
        request.Headers.Add("X-VPS-REQUEST-ID", requestId);

        byte[] bytes = Encoding.UTF8.GetBytes(parameters);
        request.ContentLength = bytes.Length;

        Stream requestStream = request.GetRequestStream();
        requestStream.Write(bytes, 0, bytes.Length);
        requestStream.Close();


        WebResponse response = request.GetResponse();
        Stream stream = response.GetResponseStream();
        StreamReader reader = new StreamReader(stream);

        try
        {

            // sample successful response
            // RESULT=0&RESPMSG=Approved&SECURETOKEN=9pOyyUMAwRUWmmv9nMn7zhQ0h&SECURETOKENID=5e3c50a4c3d54ef8b412e358d24c8915

            result = reader.ReadToEnd();

            var token = ParseResponse(result, requestId, transactionType);

            var transactionUrl = ConfigurationManager.AppSettings["PaypalTransactionAPI"];
            secureToken = transactionUrl + "?SECURETOKEN=" + token.SecureToken + "&SECURETOKENID=" + requestId;

            //ameValueCollection parsedParams = HttpUtility.ParseQueryString(result);                

            stream.Dispose();
            reader.Dispose();
        }
        catch (WebException ex)
        {
            System.Diagnostics.Trace.WriteLine(ex.Message);

        }
        finally { request.Abort(); }

        return secureToken;
    }


private TokenResponse ParseResponse(string response, string requestId, string transactionType)
    {
        var nameValues = HttpUtility.ParseQueryString(response);

        int result = -999;  // invalid result to guarantee failure

        int.TryParse(nameValues.Get(TokenResponse.ResponseParameters.RESULT.ToString()), out result);

        // retrieving response message
        var responseMessage = nameValues.Get(TokenResponse.ResponseParameters.RESPMSG.ToString());

        // retrieving token value, if any
        var secureToken = nameValues.Get(TokenResponse.ResponseParameters.SECURETOKEN.ToString());

        var reference = nameValues.Get(TokenResponse.ResponseParameters.PNREF.ToString());

        var authCode = nameValues.Get(TokenResponse.ResponseParameters.AUTHCODE.ToString());

        var cscMatch = nameValues.Get(TokenResponse.ResponseParameters.CSCMATCH.ToString());

        // populating model with values
        var tokenResponse = new TokenResponse
        {
            Result = result,
            ResponseMessage = responseMessage,
            SecureToken = secureToken,
            TransactionIdentifierToken = requestId,
            TransactionType = transactionType,
            ReferenceCode = reference,
            AuthorizationCode = authCode,
            CSCMatch = cscMatch
        };

        return tokenResponse;
    }

STEP 4 and STEP 5:

Back to Client Side:

Here I use the URL built from the previous steps and add the final needed params (secure credit card info) using jQuery and then REDIRECT to Paypal.

 function SendCCDetailsToPaypal(secureParm) {

    //alert('in SendCCDetailsToPaypal:' + secureParm);

    var secureInfo = '&ACCT=' + $('#ccNumber').val() + '&EXPDATE=' + $("#expMonth").val() + $("#expYear").val() + "&CSC=" + $('#ccSecurityCode').val();
    secureInfo = secureParm + secureInfo;

    window.location.replace(secureInfo);               
}

STEP 6:

Paypal will post back to one of the following methods: Cancel, Error, or Return (name the methods anything you want in the token request). Parse the Response and look at the variables returned from Paypal, particularly the RESULT and RESPMSG. Read the documentation for specifics as you can incorporate address validation and a bunch of other features. Based on the response, display what's appropriate.

server side:

 public ActionResult Cancel()
    {
        var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));

        //return View("Return", result);
    }


    public ActionResult Error()
    {

        var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));

        return View("Return", result);
    }


    public ActionResult Return()
    {
        var result = ParseRequest(HttpUtility.UrlDecode(Request.Params.ToString()));

        return View("Return", result);
    }

Hope this helps, and good luck! I'll answer clarification questions as I'm able. Thanks for checking this out, and remember to pay it forward.

like image 167
RichieMN Avatar answered Nov 13 '22 02:11

RichieMN


I was able to use RichieMN's answer to get a working Transparent Redirect happening. However, the problem with doing a redirect with window.location.replace in the SendCCDetailsToPaypal function is that you're passing the data on a GET string.

This works on the PayFlow Gateway side, but when they send the customer's browser back to your ResponseURL, your Apache logs will show the whole payflowlink.paypal.com URL, including the GET string as the referrer in your Apache access logs! That GET string includes the Credit Card number and now you have just lost your PCI compliance!

To alleviate this problem, you can either put the SecureToken and SecureTokenID into your Credit Card entry form, and POST it directly to payflowlink.paypal.com, or you can rewrite the SendCCDetailsToPaypal function to build a form and submit it, like this:

function SendCCDetailsToPaypal() {
    var parameters = {
        "SECURETOKEN": secureToken,
        "SECURETOKENID": secureTokenID,
        "ACCT": $("#ccNumber").val(),
        "EXPDATE": $("#expMonth").val() + $("#expYear").val(),
        "CSC": $("#ccSecurityCode").val()
    };
    var form = $('<form></form>');
    form.attr("method", "post");
    form.attr("action", "https://pilot-payflowlink.paypal.com");
    $.each(parameters, function(key, value) {
        var field = $('<input></input>');
        field.attr("type", "hidden");
        field.attr("name", key);
        field.attr("value", value);
        form.append(field);
    });
    $(document.body).append(form);
    form.submit();
}

Since that form transfers the data via POST, when your server gets the result POST back, the referrer does not contain any sensitive data, and your PCI compliance is maintained.

like image 34
Reverend Pete Avatar answered Nov 13 '22 00:11

Reverend Pete