Bonjour a tous,
Aujourd'hui nous allons parler des paiement en ligne sécurisé avec Paypal et ASP.NET. Paypal est comme tous le monde le sais un moyen simple et sécurisé pour régler ses achats sur internet. Cependant les solutions de "bouton" Paypal ne sont pas forcement très sécurisées et peuvent entrainées de grave problèmes pour le site web marchant.
Je vous propose donc aujourd'hui une solution complete afin de régler ce problème de sécurité et rendre votre site internet marchant "Paypal Capable" :)
Le formulaire de redirection vers Paypal
Ce formulaire est en fait un HttpHandler qui va générer un formulaire html et le poster automatiquement afin d'effectuer la redirection vers Paypal avec les informations que vous avez spécifiées.
La classe PaypalHandler est une classe abstraite dont vous devrez en faire dériver votre propre classe pour l'utiliser.
Pour ensuite en implémenter la méthode Initialize(). Cette méthode va vous permettre de parametrer le PaypalHandler, la méthode ProcessRequest de l'interface IHttpHandler sera ensuite automatiquement appelée.
Ci-dessous le code de notre classe PaypalHandler
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Globalization;
namespace Paypal
{
public abstract class PaypalHandler : IHttpHandler
{
#region Public Properties
public String Command { get; set; }
public String BusinessEmail { get; set; }
public String ItemName { get; set; }
public Decimal Amount { get; set; }
public String ReturnUrl { get; set; }
public String NotifyUrl { get; set; }
public String CancelUrl { get; set; }
public String CurrencyCode { get; set; }
public Boolean IsShipping { get; set; }
public String PaypalUrl { get; set; }
public String CertificateId { get; set; }
public String Custom { get; set; }
public String Rm { get; set; }
public String Lc { get; set; }
//public String PaypalCertPath { get; set; }
public String SignerPfxPath { get; set; }
public String SignerPfxPassword { get; set; }
public String PayerFirstName { get; set; }
public String PayerLastName { get; set; }
public String PayerAddress1 { get; set; }
public String PayerAddress2 { get; set; }
public String PayerZipCode { get; set; }
public String PayerCity { get; set; }
public String PayerEmail { get; set; }
public String WaitingText { get; set; }
#endregion
#region Protected Abstract Methods
protected abstract void Initialize();
#endregion
private void GeneratePaypalForm()
{
StringBuilder clearText = new StringBuilder();
// Paypal Related Informations
clearText.AppendFormat("cmd={0}\n", Command);
clearText.AppendFormat("business={0}\n", BusinessEmail);
clearText.AppendFormat("item_name={0}\n", ItemName);
clearText.AppendFormat("amount={0}\n", Amount.ToString(new CultureInfo("en-US")));
clearText.AppendFormat("currency_code={0}\n", CurrencyCode);
clearText.AppendFormat("no_shipping={0}\n", IsShipping ? "1" : "0");
clearText.AppendFormat("return={0}\n", ReturnUrl);
clearText.AppendFormat("rm={0}\n", Rm);
clearText.AppendFormat("notify_url={0}\n", NotifyUrl);
clearText.AppendFormat("cancel_return={0}\n", CancelUrl);
clearText.AppendFormat("custom={0}\n", Custom);
clearText.AppendFormat("lc={0}\n", Lc);
clearText.AppendFormat("cert_id={0}\n", CertificateId);
clearText.AppendFormat("first_name={0}\n", PayerFirstName);
clearText.AppendFormat("last_name={0}\n", PayerLastName);
clearText.AppendFormat("address1={0}\n", PayerAddress1);
clearText.AppendFormat("address2={0}\n", PayerAddress2);
clearText.AppendFormat("zip={0}\n", PayerZipCode);
clearText.AppendFormat("city={0}\n", PayerCity);
clearText.AppendFormat("email={0}\n", PayerEmail);
// Creating Encrypted Data
PaypalSecure secure = new PaypalSecure(SignerPfxPath, SignerPfxPassword);
String encryptedString = secure.SignAndEncrypt(clearText.ToString());
// Display Encrypted Form
StringBuilder sb = new StringBuilder();
sb.Append("<html>\r");
sb.Append("<head>\r");
sb.Append("</head>\r");
sb.Append("<body>\r");
sb.Append(WaitingText);
sb.AppendFormat("<form id=\"payForm\" method=\"post\" action=\"{0}\">\r", PaypalUrl);
sb.Append("<input type=\"hidden\" name=\"cmd\" value=\"_s-xclick\">\r");
sb.AppendFormat("<input type=\"hidden\" name=\"encrypted\" value=\"{0}\">\r", encryptedString);
sb.Append("</form>\r");
sb.Append("<script language=\"javascript\">\r");
sb.Append("document.forms[\"payForm\"].submit();\r");
sb.Append("</script>\r");
sb.Append("</body>\r");
sb.Append("</html>\r");
HttpContext.Current.Response.Write(sb.ToString());
}
#region IHttpHandler Members
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
Initialize();
context.Response.ContentType = "text/html";
GeneratePaypalForm();
}
#endregion
}
}
Voici donc un exemple de notre PaypalHandler implémenté
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
namespace Paypal
{
public class MyPPHandler : PaypalHandler
{
protected override void Initialize()
{
base.Amount = (Decimal)23.45;
base.BusinessEmail = "me@dotmail.com";
base.CancelUrl = "http://www.mysite.com/CancelOrder.aspx";
base.Command = "_xclick";
base.CurrencyCode = "EUR";
base.Custom = "3456";
base.IsShipping = false;
base.ItemName = "My Shop";
base.Lc = "fr";
base.NotifyUrl = "http://www.mysite.com/PaypalIPN.axd";
base.PayerEmail = "buyer@dotmail.com";
base.PayerFirstName = "Scott";
base.PayerLastName = "Dummy";
base.PayerAddress1 = "23 Street";
base.PayerZipCode = "4567Z";
base.PayerCity = "New York";
base.PaypalUrl = "https://www.paypal.com/cgi-bin/webscr";
base.ReturnUrl = "http://www.mysite.com/OrderSuccess.aspx";
base.Rm = "0";
base.CertificateId = "XXX-XXXX";
base.SignerPfxPassword = "MyPassword";
base.SignerPfxPath = HttpContext.Current.Server.MapPath("~/App_Data/Cert.pfx");
base.WaitingText = "Please wait ... Redirecting to Paypal";
}
}
}
Vous remarquez CertificateId, SignerPfxPassword et SignerPfxPath. Ces paramètres correspondent à votre certificat que vous avez précédement généré puis uploadé dans votre compte Paypal.
Pourquoi un certificat ?
Et bien en fait c'est ici que va se passer la réelle sécurité de votre site web au niveau du paiement. Le formulaire Paypal sera crypté et donc impossible à modifier entre temps lors de la navigation entre votre site web et Paypal (par exemple pour changer le montant ...)
Ce cryptage s'effectue via la classe PaypalSecure que le PaypalHandler va utiliser en arrière plan.
using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using X509 = System.Security.Cryptography.X509Certificates;
using Pkcs = System.Security.Cryptography.Pkcs;
using System.IO;
using System.Security.Cryptography.X509Certificates;
namespace Paypal
{
public sealed class PaypalSecure
{
#region Private Fields
private Encoding _encoding = Encoding.Default;
private string _recipientPublicCertPath;
private X509.X509Certificate2 _signerCert;
private X509.X509Certificate2 _recipientCert;
#endregion
#region Constructors
public PaypalSecure(String SignerPfxCertPath, String SignerPfxCertPassword)
{
_signerCert = new X509.X509Certificate2(File.ReadAllBytes(SignerPfxCertPath),
SignerPfxCertPassword, X509KeyStorageFlags.MachineKeySet);
}
#endregion
#region Private Properties
private string Charset
{
get { return _encoding.WebName; }
set
{
if (value != null && value != "")
{
_encoding = Encoding.GetEncoding(value);
}
}
}
private string RecipientPublicCertPath
{
get { return _recipientPublicCertPath; }
set
{
_recipientPublicCertPath = value;
_recipientCert = new X509.X509Certificate2(_recipientPublicCertPath);
}
}
#endregion
#region Public Methods
public string SignAndEncrypt(string clearText)
{
string result = null;
byte[] messageBytes = _encoding.GetBytes(clearText);
byte[] signedBytes = Sign(messageBytes);
byte[] encryptedBytes = Envelope(signedBytes);
result = Base64Encode(encryptedBytes);
return result;
}
#endregion
#region Private Methods
private byte[] Sign(byte[] messageBytes)
{
System.Security.Cryptography.Pkcs.ContentInfo content = new Pkcs.ContentInfo(messageBytes);
Pkcs.SignedCms signed = new System.Security.Cryptography.Pkcs.SignedCms(content);
Pkcs.CmsSigner signer = new System.Security.Cryptography.Pkcs.CmsSigner(_signerCert);
signed.ComputeSignature(signer);
byte[] signedBytes = signed.Encode();
return signedBytes;
}
private byte[] Envelope(byte[] contentBytes)
{
Pkcs.ContentInfo content = new Pkcs.ContentInfo(contentBytes);
Pkcs.EnvelopedCms envMsg = new Pkcs.EnvelopedCms(content);
Pkcs.CmsRecipient recipient = new Pkcs.CmsRecipient(Pkcs.SubjectIdentifierType.IssuerAndSerialNumber, _recipientCert);
envMsg.Encrypt(recipient);
byte[] encryptedBytes = envMsg.Encode();
return encryptedBytes;
}
private string Base64Encode(byte[] encoded)
{
const string PKCS7_HEADER = "-----BEGIN PKCS7-----";
const string PKCS7_FOOTER = "-----END PKCS7-----";
string base64 = Convert.ToBase64String(encoded);
StringBuilder formatted = new StringBuilder();
formatted.Append(PKCS7_HEADER);
formatted.Append(base64);
formatted.Append(PKCS7_FOOTER);
return formatted.ToString();
}
#endregion
}
}
Bon maintenant que nous avons fait cela, le paiement devrai fonctionner et vos clients peuvent régler leurs achats. Cependant il faudrai que vous puissiez être notifié lorsque la transaction c'est terminée pour par exemple mettre a jours votre base de données, envoyer un email etc ...
C'est ici qu'intervient le PaypalIPNHandler qui est également une classe abstraite dont vous allez devoir en faire dériver votre propre classe afin d'initialiser ses paramètres.
La notification de paiement Paypal (IPN)
Voici donc un exemple de notre classe MyPPIPN implémentée
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Paypal
{
public class MyPPIPN : PaypalIPN
{
protected override void Initialize()
{
base.BusinessEmail = "me@dotmail.com";
base.PaypalUrl = "https://www.paypal.com/cgi-bin/webscr";
}
protected override void CreatePaymentResponses(string TransactionId, decimal PaymentPrice, object CustomData, bool IsSuccess, string FaultReason)
{
// Gerer la réponse, base de données, email etc ...
}
}
}
Et ici le code de la classe abstraite PaypalIPN
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Net;
using System.IO;
using System.Globalization;
namespace Paypal
{
public abstract class PaypalIPN : IHttpHandler
{
#region Private Structs
private struct PaypalResponseStatus
{
public const string VERIFIED = "VERIFIED";
public const string INVALID = "INVALID";
}
private struct PaypalPaymentStatus
{
public const string COMPLETED = "Completed";
public const string PENDING = "Pending";
public const string FAILED = "Failed";
public const string DENIED = "Denied";
}
private struct PaypalPendingStatus
{
public const string ADDRESS = "address";
public const string AUTHORIZATION = "authorization";
public const string ECHECK = "echeck";
public const string INTL = "intl";
public const string MULTICURRENCY = "multi-currency";
public const string UNILATERAL = "unilateral";
public const string UPGRADE = "upgrade";
public const string VERIFIY = "verify";
public const string OTHER = "other";
}
#endregion
#region Public Structs
public struct PaypalErrorMessage
{
public const string P_ERROR_UNKNOWN_ORDER = "PayPal: Unknown order...please check your paypal account";
public const string P_PENDING_ADDRESS = "PayPal: Pending Order because of address";
public const string P_PENDING_AUTORIZATION = "PayPal: Pending Order because of authorization";
public const string P_PENDING_ECHECK = "PayPal: Pending Order because of echeck";
public const string P_PENDING_INTL = "PayPal: Pending Order because of non-US Acccount";
public const string P_PENDING_MULTI_CURRENCY = "PayPal: Pending Order because of multi-currency";
public const string P_PENDING_UNILITERAL = "PayPal: Pending Order because of Unilateral";
public const string P_PENDING_UPGRADE = "PayPal: Pending Order because of Upgrade";
public const string P_PENDING_VERIFY = "PayPal: Pending Order because of Verification needed";
public const string P_PENDING_OTHER = "PayPal: Pending Order because of other reason";
public const string P_FAILED_ORDER_FAILED = "PayPal: Failed order";
public const string P_DENIED_ORDER_DENIED = "PayPal: Denied order";
public const string P_INVALID_ORDER = "PayPal: Invalid order, please review and investigate";
}
#endregion
#region Private Fields
private string _postUrl = "";
private string _strRequest = "";
private string _businessEmail = "";
private int respId = 0;
#endregion
#region Public Properties
public string BusinessEmail { get; set; }
public string PaypalUrl { get; set; }
#endregion
#region Private Properties
private string Response { get; set; }
private string RequestLength { get; set; }
private string Business { get; set; }
private string TXN_ID { get; set; }
private string TXN_Type { get; set; }
private string PaymentStatus { get; set; }
private string ReceiverEmail { get; set; }
private string ReceiverID { get; set; }
private string ItemName { get; set; }
private string ItemNumber { get; set; }
private string Quantity { get; set; }
private string QuantityCartItems { get; set; }
private string Invoice { get; set; }
private string Custom { get; set; }
private string Memo { get; set; }
private string Tax { get; set; }
private string PaymentGross { get; set; }
private string PaymentDate { get; set; }
private string PaymentFee { get; set; }
private string PayerEmail { get; set; }
private string PayerPhone { get; set; }
private string PayerBusinessName { get; set; }
private string PendingReason { get; set; }
private string ShippingMethod { get; set; }
private string Shipping { get; set; }
private string PayerFirstName { get; set; }
private string PayerLastName { get; set; }
private string PayerAddress { get; set; }
private string PayerCity { get; set; }
private string PayerState { get; set; }
private string PayerZipCode { get; set; }
private string PayerCountry { get; set; }
private string PayerCountryCode { get; set; }
private string PayerAddressStatus { get; set; }
private string PayerStatus { get; set; }
private string PayerID { get; set; }
private string PaymentType { get; set; }
private string NotifyVersion { get; set; }
private string VerifySign { get; set; }
#endregion
#region Public IHttpHandler Properties
public bool IsReusable
{
get { return false; }
}
#endregion
#region Public IHttpHandler Methods
public void ProcessRequest(HttpContext context)
{
HttpRequest Request = context.Request;
// Initialize
Initialize();
// Fill Properties
FillProperties();
// Make Paypal Post
MakeHttpPost();
// Check PaymentStatus
ProcessIPN();
}
#endregion
#region Private Abstract Methods
protected abstract void Initialize();
protected abstract void CreatePaymentResponses(String TransactionId,
Decimal PaymentPrice,
Object CustomData,
Boolean IsSuccess,
String FaultReason);
#endregion
#region Private Methods
private NumberFormatInfo GetNumberFormatProvider()
{
NumberFormatInfo provider = new NumberFormatInfo();
provider.NumberDecimalSeparator = ".";
provider.NumberGroupSeparator = ",";
provider.NumberGroupSizes = new Int32[] { 3 };
return provider;
}
private void MakeHttpPost()
{
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(this.PaypalUrl);
req.Method = "POST";
req.ContentLength = this.RequestLength.Length + 21;
req.ContentType = "application/x-www-form-urlencoded";
byte[] param = HttpContext.Current.Request.BinaryRead(HttpContext.Current.Request.ContentLength);
this.RequestLength = Encoding.ASCII.GetString(param);
this.RequestLength += "&cmd=_notify-validate";
req.ContentLength = this.RequestLength.Length;
StreamWriter streamOut = new StreamWriter(req.GetRequestStream(), System.Text.Encoding.ASCII);
streamOut.Write(this.RequestLength);
streamOut.Close();
StreamReader streamIn = new StreamReader(req.GetResponse().GetResponseStream());
this.Response = streamIn.ReadToEnd();
streamIn.Close();
}
private void FillProperties()
{
this.RequestLength = HttpContext.Current.Request.Form.ToString();
this.PayerCity = HttpContext.Current.Request.Form["address_city"];
this.PayerCountry = HttpContext.Current.Request.Form["address_country"];
this.PayerCountryCode = HttpContext.Current.Request.Form["address_country_code"];
this.PayerState = HttpContext.Current.Request.Form["address_state"];
this.PayerAddressStatus = HttpContext.Current.Request.Form["address_status"];
this.PayerAddress = HttpContext.Current.Request.Form["address_street"];
this.PayerZipCode = HttpContext.Current.Request.Form["address_zip"];
this.PayerFirstName = HttpContext.Current.Request.Form["first_name"];
this.PayerLastName = HttpContext.Current.Request.Form["last_name"];
this.PayerBusinessName = HttpContext.Current.Request.Form["payer_business_name"];
this.PayerEmail = HttpContext.Current.Request.Form["payer_email"];
this.PayerID = HttpContext.Current.Request.Form["payer_id"];
this.PayerStatus = HttpContext.Current.Request.Form["payer_status"];
this.PayerPhone = HttpContext.Current.Request.Form["contact_phone"];
this.Business = HttpContext.Current.Request.Form["business"];
this.ItemName = HttpContext.Current.Request.Form["item_name"];
this.ItemNumber = HttpContext.Current.Request.Form["item_number"];
this.Quantity = HttpContext.Current.Request.Form["quantity"];
this.ReceiverEmail = HttpContext.Current.Request.Form["receiver_email"];
this.ReceiverID = HttpContext.Current.Request.Form["receiver_id"];
this.Custom = HttpContext.Current.Request.Form["custom"];
this.Memo = HttpContext.Current.Request.Form["memo"];
this.Invoice = HttpContext.Current.Request.Form["invoice"];
this.Tax = HttpContext.Current.Request.Form["tax"];
this.QuantityCartItems = HttpContext.Current.Request.Form["num_cart_items"];
this.PaymentDate = HttpContext.Current.Request.Form["payment_date"];
this.PaymentStatus = HttpContext.Current.Request.Form["payment_status"];
this.PaymentType = HttpContext.Current.Request.Form["payment_type"];
this.PendingReason = HttpContext.Current.Request.Form["pending_reason"];
this.TXN_ID = HttpContext.Current.Request.Form["txn_id"];
this.TXN_Type = HttpContext.Current.Request.Form["txn_type"];
this.PaymentFee = HttpContext.Current.Request.Form["mc_fee"];
this.PaymentGross = HttpContext.Current.Request.Form["mc_gross"];
this.NotifyVersion = HttpContext.Current.Request.Form["notify_version"];
this.VerifySign = HttpContext.Current.Request.Form["verify_sign"];
}
private void ProcessIPN()
{
switch (this.Response)
{
case PaypalResponseStatus.VERIFIED:
ProcessVerifiedResponse();
break;
case PaypalResponseStatus.INVALID:
ProcessInvalidResponse();
break;
default:
ProcessUnknownResponse();
break;
}
}
private void ProcessUnknownResponse()
{
ProcessInvalidResponse();
}
private void ProcessInvalidResponse()
{
try
{
NumberFormatInfo provider = GetNumberFormatProvider();
CreatePaymentResponses(this.TXN_ID,
Convert.ToDecimal(this.PaymentGross, provider),
Custom,
false,
PaypalErrorMessage.P_INVALID_ORDER + this.Response);
}
catch (Exception ex)
{
throw ex;
}
}
private void ProcessVerifiedResponse()
{
switch (this.PaymentStatus)
{
case PaypalPaymentStatus.COMPLETED:
ProcessCompletedResponse();
break;
case PaypalPaymentStatus.PENDING:
ProcessPendingResponse();
break;
case PaypalPaymentStatus.FAILED:
ProcessFailedResponse();
break;
case PaypalPaymentStatus.DENIED:
ProcessDeniedResponse();
break;
}
}
private void ProcessDeniedResponse()
{
NumberFormatInfo provider = GetNumberFormatProvider();
String error = String.Empty;
error = PaypalErrorMessage.P_DENIED_ORDER_DENIED;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, false, error);
}
private void ProcessFailedResponse()
{
NumberFormatInfo provider = GetNumberFormatProvider();
String error = String.Empty;
error = PaypalErrorMessage.P_FAILED_ORDER_FAILED;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, false, error);
}
private void ProcessPendingResponse()
{
NumberFormatInfo provider = GetNumberFormatProvider();
String error = String.Empty;
switch (this.PendingReason)
{
case PaypalPendingStatus.ADDRESS:
error = PaypalErrorMessage.P_PENDING_ADDRESS;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
case PaypalPendingStatus.AUTHORIZATION:
error = PaypalErrorMessage.P_PENDING_AUTORIZATION;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
case PaypalPendingStatus.ECHECK:
error = PaypalErrorMessage.P_PENDING_ECHECK;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
case PaypalPendingStatus.INTL:
error = PaypalErrorMessage.P_PENDING_INTL;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
case PaypalPendingStatus.MULTICURRENCY:
error = PaypalErrorMessage.P_PENDING_MULTI_CURRENCY;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
case PaypalPendingStatus.UNILATERAL:
error = PaypalErrorMessage.P_PENDING_UNILITERAL;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
case PaypalPendingStatus.UPGRADE:
error = PaypalErrorMessage.P_PENDING_UPGRADE;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
case PaypalPendingStatus.VERIFIY:
error = PaypalErrorMessage.P_PENDING_VERIFY;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
case PaypalPendingStatus.OTHER:
error = PaypalErrorMessage.P_PENDING_OTHER;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
default:
error = string.Format("PayPal: Pending Order because of unknown reason of {0}", this.PendingReason);
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
break;
}
}
private void ProcessCompletedResponse()
{
NumberFormatInfo provider = GetNumberFormatProvider();
String error = String.Empty;
if (this.ReceiverEmail == this.BusinessEmail)
{
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
}
else
{
error = PaypalErrorMessage.P_ERROR_UNKNOWN_ORDER;
CreatePaymentResponses(this.TXN_ID, Convert.ToDecimal(this.PaymentGross, provider), Custom, true, error);
}
}
#endregion
}
}
Et voila maintenant vos clients peuvent acheter sur votre site web et vous en êtes notifié !
En espérant que ca vous aidera pour vos projets de sites web.
Toutes vos suggestions sont bien évidement les bienvenues :)
yield return this;
Views(2513)

