ASP.NET MVC 2 RC – Validating Credit Card Numbers
Today we announced the release candidate of ASP.NET MVC 2. Apart from some bug fixes, client validation has undergone some significant changes, amongst others, making it easier to work with custom validators. You can find more details about all the changes in the readme. As usual, Phil Haack also made an announcement.
Having spent some time in the electronic funds transfer (EFT) industry before joining Microsoft, I thought it might be fun combining my experience from both worlds by showing how to validate a credit card number using the validation support provided by MVC 2 RC.
Anatomy of a PAN
The number printed on your credit card is called a primary account number (PAN) and is based on the ISO/IEC 7812 standard. Let’s start by looking at a contrived PAN.
- The first six digits of the PAN are called the bank identification number (BIN) or issuer identification number (IIN). The first digit of the IIN is called the major industry identifier (MII). For the above number, the BIN/IIN is 123456 and the MII is 1. The IIN is primarily used to route transactions from the acquirer to the card issuer. The acquirer is the owner of the device from which the transaction originated, such as an ATM or POS device.
- All the digits following the BIN, except the last digit, is the account number that the issuer assigned to your card. The account number of the card holder in the above example is 781234567 and is the account that will be debited or credited as a result of a transaction.
- The rightmost digit of the PAN is called the check digit. When an issuer creates a new card it concatenates the BIN and account number and then calculates the value of the check digit using the Luhn algorithm to form the PAN.
- The maximum length of a PAN may not exceed 19 digits.
Luhn algorithm
The Luhn algorithm, also known as the modulo-10 algorithm, can be used to verify that the check digit used in a PAN is valid and can also be used to generate a check digit. The algorithm is fairly simple:
- Enumerate all the digits in the PAN (including the check digit) from right to left and calculate their sum as follows:
- Every odd digit is taken as-is, starting with the check digit.
- Multiply every second digit by 2. If the product is greater than 9, the individual digits of the product is added to the sum. You can simplify this by simply subtracting 9 from the product. For example, if the current digit being evaluated is 6, the product is 12 and the sum will be increased by 3 (1+2).
-
- If the final total is divisible by 10, the PAN is valid.
Example
The sum for a PAN equal to 1234 5678 1234 5678 is calculated as: 8+(1+4)+6+(1+0)+4+6+2+2+8+(1+4)+6+(1+0)+4+6+2+2=68. Since 68 is not divisible by 10 the PAN is invalid.
PAN Validation on the Server
Given the ISO/IEC 7812 specification, we need to perform two checks in our application to verify the validity of a PAN:
- Check that the PAN does not exceed 19 characters and that it meets a predefined minimum length. The majority of card issuers use PANs that are between 13 and 19 digits long. Strictly speaking though, the minimum length is 8 digits: 6 digits for the BIN, 1 digit for the account number and 1 for the check digit. Of course, this does limit the issuer to only 10 accounts.
- Verify that the check digit is correct.
The model I will be using is very simple and for brevity only contains a single property.
public class CreditCardModel { [CreditCard] [DisplayName("Credit Card Number")] public string CreditCardNumber { get; set; } }
The CreditCard attribute we applied to the CreditCardModel is responsible for doing the bulk of the work. The MinLength property defaults to 13 and specifies the minimum length of the PAN we’d like to validate. The validation work is divided into two parts. First, we use a regular expression to verify that the string only contains digits and meets the required minimum and maximum length criteria. If this check passes we proceed to apply the Luhn algorithm and verify the check digit.
public class CreditCardAttribute : ValidationAttribute { private const int _defaultMinLength = 13; private const int _maxLength = 19; private int _minLength = _defaultMinLength; private string _regex = @"^\d{{{0},{1}}}$"; private const int _zero = '0'; public CreditCardAttribute() : base() { ErrorMessage = "Please enter a valid credit card number"; } public int MinLength { get { return _minLength; } set { if ((value < 8) || (value > _maxLength)) { _minLength = _defaultMinLength; } else { _minLength = value; } } } public override bool IsValid(object value) { string pan = value as string; if (String.IsNullOrEmpty(pan)) { return false; } Regex panRegex = new Regex(String.Format(_regex, MinLength.ToString(CultureInfo.InvariantCulture.NumberFormat), _maxLength.ToString(CultureInfo.InvariantCulture.NumberFormat))); if (!panRegex.IsMatch(pan)) { return false; } // Validate the check digit using the Luhn algorithm string reversedPan = new string((pan.ToCharArray()).Reverse().ToArray()); int sum = 0; int multiplier = 0; foreach (char ch in reversedPan) { int product = (ch - _zero) * (multiplier + 1); sum = sum + (product / 10) + (product % 10); multiplier = (multiplier + 1) % 2; } return sum % 10 == 0; } }
We can write a simple view that allows a user to enter a credit card number.
<% using (Html.BeginForm()) { %> <div class="editor-label"> <%= Html.LabelFor(m => m.CreditCardNumber ) %> </div> <div class="editor-field"> <%= Html.TextBoxFor(m => m.CreditCardNumber) %> <%= Html.ValidationMessageFor(m => m.CreditCardNumber) %> </div> <div> <input type="submit" value="Place Order"/> </div> <% } %>
If the user enters an invalid number and submits the form they are greeted with the server side response as shown below.
Client Side Support
The first step to support client side validation is to implement a ModelValidator that can be associated with the CreditCardAttribute.
public class CreditCardValidator : DataAnnotationsModelValidator<CreditCardAttribute> { public CreditCardValidator(ModelMetadata metadata, ControllerContext controllerContext, CreditCardAttribute attribute) : base(metadata, controllerContext, attribute) { } public override IEnumerable<ModelClientValidationRule> GetClientValidationRules() { var rule = new ModelClientValidationRule { ValidationType = "creditcardnumber", ErrorMessage = Attribute.FormatErrorMessage(Metadata.PropertyName) }; rule.ValidationParameters["minLength"] = Attribute.MinLength; return new[] { rule }; } }
Next, the validator is registered in Global.asax.
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); DataAnnotationsModelValidatorProvider.RegisterAdapter(typeof(CreditCardAttribute), typeof(CreditCardValidator)); }
We also need to provide a custom validation rule that can be used on the client to validate the card number. Note that we can access the MinLength property of the server attribute using the rule.ValidationParameters. This keeps the client and server side validation in sync.
<script type="text/javascript"> Sys.Mvc.ValidatorRegistry.validators["creditcardnumber"] = function(rule) { var zero = "0".charCodeAt(0); return function(value, context) { pan = value.toString(); re = new RegExp("^\\d{" + rule.ValidationParameters.minLength + ",19}$"); var valid = re.test(pan); if (valid) { reversedPan = pan.split("").reverse().join(""); var sum = 0; var multiplier = 0; for (i = 0; i < reversedPan.length; i++) { product = (reversedPan.charCodeAt(i) - zero) * (multiplier + 1); sum = sum + Math.floor(product / 10) + (product % 10); multiplier = (multiplier + 1) % 2; } valid = sum % 10 == 0; } return valid; }; }; </script>
All that’s left is to enable client validation by making a call to Html.EnableClientValidation in the view just before calling Html.BeginForm.
Further Extensions
Apart from using the IIN to route transactions, it can also be used to identify branded card. All the major card organizations and issuers such as Visa, American Express and MasterCard have predefined IIN ranges. These are more commonly known as BIN prefixes. By applying a mask of BIN prefixes you can limit you application to accept specific card types. For example, the PAN on Visa branded cards always begins with 4. Of course it would be prudent to check with all the organizations that you’d like to support to ensure you have the correct prefixes.
Happy validating!