PayPal Payment Standard IPN/PDT–Asynchronous Processing

Hi Guys,

I am currently working on a personnel project to interface with PayPal Payment Standard on an MVC3 and Windows Azure based application. I needed to find a nice way to convert request/response objects from PayPal to their corresponding IPN/PDT objects.

I want a payment solution that can be easily scaled on high load. So we will leverage NServiceBus as the front end –> back end service bus infrastructure. This allows decoupling of payments from the functionality of the site, so under high load the payments will not restrict the usability of the site, This is done with MSMQ and NServicebus. I will show a basic example of how.

Below is a DTO you can use for IPN or PDT data. Of course you will need logic to convert http request/response data to this DTO and you have many ways of doing so.

More information about PDT/IPN variables can be found here:

https://www.paypal.com/IntegrationCenter/ic_ipn-pdt-variable-reference.html
 https://cms.paypal.com/us/cgi-bin/?cmd=_render-content&content_ID=developer/e_howto_html_IPNandPDTVariables#id091EB0901HT

You can then take and IPN and convert the request form/query string values  to a DTO e.g.

  _myIpnNotification.Invoice = _myRequest["invoice"];

The same applies for the PDT which you will receive out of band on a separate listener.

 

Here are the different type of transaction types and status

 [Serializable]
    public enum PdtStatus
    {
        Unknown = 0,

        [Description("SUCCESS")] Success,
        [Description("FAIL")] Fail
    }

  [Serializable]
    public enum IpnStatus
    {
        Unknown,
        [Description("verified")] Verified,
        [Description("invalid")] Invalid
    }

    [Serializable]
    public enum TransactionType
    {
        //[Description(null)]
        Unknown = 0,

        [Description("cart")] Cart,

        [Description("express_checkout")] ExpressCheckout,

        [Description("merch_pmt")] MerchantPayment,

        [Description("send_money")] SendMoney,

        [Description("virtual_terminal")] VirtualTerminal,

        [Description("web_accept")] WebAccept,

        [Description("masspay")] MassPayment,

        [Description("subscr_signup")] SubscriptionSignUp,

        [Description("subscr_cancel")] SubscriptionCancellation,

        [Description("subscr_failed")] SubscriptionPaymentFailed,

        [Description("subscr_payment")] SubscriptionPayment,

        [Description("subscr_eot")] SubscriptionEndOfTime,

        [Description("subscr_modify")] SubscriptionModification
    }

Security, if you want the best security, never trust anything from Paypal, always double check the data, so when an IPN comes in, take that data and do a check with Payal and then verify the results match, and the same with PDT data.

This means checking the IPN data – currency, amount, date, subscription type etc is the same as what is in your transaction log, this way, any spoofing is protected, here is an example of a rule.

 public interface IRulesCommon
    {
        List<Error> ApplyRulesStandardPayments( decimal resultAmount, Currency resultCurrency, string receiver, TransactionLog transaction);
    }
 public interface IRulesPaypal
    {
        List<Error> ComparePaymentFrequencyTransactionStatus(TransactionLog transaction, TransactionType transactionType);

        //Used by IPN Only for SignUp
        List<Error> CheckPaymentFrequencyOnSignupIpn(TransactionLog transaction, TimePeriod subscriptionPeriod);

        List<Error> CheckSubscriptionPayment(decimal mcAmount3, Currency currency, string receiver,
                                             TransactionLog transaction);



        List<Error> CheckSubscriptionSignUpCancel(decimal mcAmount3, string recurring, Currency currency, string receiver,
                                     TransactionLog transaction);
    }

 

Unfortunately, I cannot show you implementation logic for this, as it may breach the security of my site if something is found which can be compromised, however this interface is a good start.

As you can see above, I do allot of checks, I ensure the amount, currency, receiver all match the original transaction object.

So, now we have an IPN or PDT listener, so say I have an MVC3 listener controller like so:

 public class IpnController : Controller
    {
        public readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        readonly IBus _bus; //NServiceBus Object
        readonly IIpnDataProvider _ipnDataProvider;
        readonly ISettingsDataProvider _settings;

        
        //Constructor Inject with Autofac
        public IpnController(IBus bus, IIpnDataProvider ipnDataProvider, ISettingsDataProvider settings)
        {
            _bus = bus;
            _settings = settings;
            _ipnDataProvider = ipnDataProvider;
        }

        /// <summary>
        /// Expects a post with variables like: ?mc_gross=19.95&protection_eligibility=Eligible&address_status=confirmed&payer_id=LPLWNMTBWMFAY&tax=0.00&address_street=1+Main+St&payment_date=20%3A12%3A59+Jan+13%2C+2009+PST&payment_status=Completed&charset=windows-1252&address_zip=95131&first_name=Test&mc_fee=0.88&address_country_code=US&address_name=Test+User&notify_version=2.6&custom=1||1||myredirecturl&payer_status=verified&address_country=United+States&address_city=San+Jose&quantity=1&verify_sign=AtkOfCXbDm2hu0ZELryHFjY-Vb7PAUvS6nMXgysbElEn9v-1XcmSoGtf&payer_email=gpmac_1231902590_per%40paypal.com&txn_id=61E67681CH3238416&payment_type=instant&last_name=User&address_state=CA&receiver_email=gpmac_1231902686_biz%40paypal.com&payment_fee=0.88&receiver_id=S8XGHLYDW9T3S&txn_type=express_checkout&item_name=&mc_currency=USD&item_number=&residence_country=US&test_ipn=1&handling_amount=0.00&transaction_subject=&payment_gross=19.95&shipping=0.0
        /// </summary>
        /// <returns></returns>
        public ActionResult Ipn()
        {
            string rawData = Extract.GetHttpRawData(Request);

            if (_settings.AuditHttpEnabled)
                Audit.AuditHttp(rawData, _bus, Request.RawUrl); //Asynchronous auditing of the IPN message for audit tracking

            if (_ipnDataProvider != null && _ipnDataProvider.MandatoryDataSpecified)
            {
                try
                {
                    var message = new PaypalIpnMessage
                        {
                            OriginalHttpRequest =
                                rawData,
                            MessageId = Guid.NewGuid(),
                            InvoiceId = _ipnDataProvider.TransactionId,
                            Notification =
                                new ResponseToIpnNotification(Request.Form).GetIpnNotification()
                        };
                    _bus.Send(message); //Asynchronous processing of the payment message to the backend systems
                }
                catch (Exception e)
                {
                    Log.Info(e);
                    Log.Info(rawData);
                    return new HttpStatusCodeResult(400);
                }
                return View();
            }

            Log.Info("Request data does not contain mandatory fields e.g. MerchantId and MerchantTransactionId");
            Log.Info(rawData);
            return new HttpStatusCodeResult(“_codeResult”);
        }
    }

 

As you can see above, any payment processing is now totally decoupled, the Front End and Back End work independent of each other with no synchronous calls. Yes this is all wrapped within a MSDTC transaction. I prefer this than using WCF, due to no issue in dealing with latency and I have guaranteed delivery of my crucial message.,You will write a similar controller for the PDT out of band response as well. We also leveraging an IoC container to automatically inject the service bus object into the controller as well as other dependencies. My favourite is Autofac.

 public ActionResult Process()
        {
            string rawData = Extract.GetHttpRawData(Request);
            if (_settings.AuditHttpEnabled)
                Audit.AuditHttp(rawData, _bus, Request.RawUrl);

            if (_successDataProvider != null && _successDataProvider.MandatoryDataSpecified)
            {
                try
                {
                    var message = new PaypalPdtMessage
                        {
                            OriginalHttpRequest =rawData,
                            MessageId = Guid.NewGuid(),
                            TransactionIdForPdtToken = _successDataProvider.TransactionIdForPdtToken,
                            AmountPaidRecordedByPdt = _successDataProvider.AmountPaidRecordedByPdt,
                            InvoiceId = _successDataProvider.TransactionId,
                            
                        };

                    _bus.Send(message);
                   
                }
                catch (Exception e)
                {
                    Log.Info(e);
                    Log.Info(rawData);
                }

                return Redirect(_successDataProvider.MerchantRedirectURL);
            }


            Log.Info("Request data does not contain mandatory fields e.g. MerchantId");
            Log.Info(rawData);
            return new HttpStatusCodeResult(“_codeResult”);
        }
    }

I hope this gives you some ideas for developing robust payment options for your site and provide a nice user experience for users Smile

Remember to also deal with your dates in UTC format, and ensure the date kind property is set to UTC as well for extra safety when storing the transaction date.

So in a nutshell, you can have top notch validation that protects from spoofing attacks if you ALWAYS take a IPN and PDT and compare it with the original transaction object! If you keep your transaction id’s unpredictable, users can never guess someone else  transactionid and hijack it for payment. So you can have a nice invoice number e.g. 90124, but the transactionid is not easy to predict, it could be a Guid etc.

 

Imagine if the above was not the case a smart user could then create two transactions, one for a cheap item and one for an expensive one, here can then hijack the ipn/pdt, or send a FAKE pdt/ipn to your system, and then swap the item information around, and then later cancel the expensive item transaction, however, he hijacked the cheap item and changed the item list and transactionid, hence why I say, check AMOUNT, CURRENCY, Items, TranactionID etc. There are allot of sites out there that are easy to hack due to them not doing these sort of double checks on Ipn and Pdt messages from Paypal. If I know you IPN or PDT listener, I can send fake messages in and try guess weaknesses by using methods such as generating two transactions on the site and trying to swap items around, currencies, etc In fact, because they cannot guess my transaction number, I do not need to check items in the list, as this is not processed in my listener, so if they changed a flower pot to a BMW, who cares, I ignore this sort of data in an IPN/PDT, as long as the fundamentals are the same we in good shape.

Matthew Will and I spent allot of time think these sort of scenarios through and nutted out a nice secure solution. The above samples should get you started in the right direction for Paypal Standard integration. Allot of implementation logic is left out, and this is done on purpose.

Cheers

Advertisements

5 thoughts on “PayPal Payment Standard IPN/PDT–Asynchronous Processing

  1. Hi,

    Am also working on a MVC3 app hosted on Azure and want to integrate with Paypal IPN and PDT payments .
    However IPIN sand box is unable to hit my ActionResult. I can hit the ActionResult by manually typing the url but the Sandbox says “Url not found” . Did you have to make any Azure specific settings to enable its visibility to the IPN Sandbox??

    Thanks in Advance!
    Anshulee
    Cennest Technologies

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s