Adding Two-Factor Authentication to a ASP.NET MVC Application

by Jimmy 31. August 2011 15:06

In the previous post I gave you short explanation on two-factor authentication (2FA) so it's not time to crack some code. This post will show you how to implement 2FA with VeriSign Identity Protection (VIP) in a MVC 3 application. VeriSign is now part of Symantec, where I'm working, so it was much easier for me to use that as an example, however nothing stops you from doing the same using RSA. I'm fully aware, as you should be, that having 2FA with RSA or VeriSign is not a cheap option and you should not expect that you can make it working for your blog. My aim here is to show you that 2FA is very easy from technical point of view.

Perquisites

The first two steps is to set up an account for VeriSign Identity Protection and to get yourself a token. The former is not free but you can get a free 30 days trial by going here that would be enough to give you a kick start.  After you filled the form you will have to wait a few hours for the provisioning process to move cogwheels before you can do anything. Meanwhile you can get the free token from here, if you don't have one yet, that can be installed on your mobile or your PC.

After your account will be approved you can go to the link provided in the confirmation email, register your token and you are in. First, go straight to the Download Files page and get yourself three files:

  • VIP_API.zip with wsdl files of the web service you will be using,
  • VIPAuthWebServices.pdf – the web service documentation,
  • VIPAuthMember.pdf – guidelines for customers on how to use 2FA and recommended implementation details.

Finally you have to get a certificate that you will use to authenticate calls to the VIP web service. Go to the Manage VIP Certificates page and click Request a Certificate button. On the first page just continue, on the second enter a name for the certificate, and on the last page select PKCS#13 format and enter a password that will protect your certificate. Now you can download the certificate.

Registration

image

Before I start with the code, let me quickly explain the registration process using the diagram above. When the user clicks the submit button, the Register action is executed. Assuming the form is valid, I have to activate the user's credential ID within my VIP account. This is required for the validation to work and you can't validate a token that is not registered for your site. Thanks to that, only users that explicitly registered with your sire will be enabled for your account. Activation is made simply by calling the Validate method on the VIP management endpoint. This method accepts two arguments, CredentialID and the code, and after validating them, it will enable the user in your VIP account.

Let's look at the implementation. Fire up your Visual Studio and create the fresh, out of the box MVC 3 application, I called it TwoFactorDemo. Once all code has been generated, look at the models and add two more properties into RegisterModel class.

public class RegisterModel {
  [Required]
  [Display(Name = "User name")]
  public string UserName { get; set; }

  [Required]
  [DataType(DataType.EmailAddress)]
  [Display(Name = "Email address")]
  public string Email { get; set; }

  [Required]
  [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
  [DataType(DataType.Password)]
  [Display(Name = "Password")]
  public string Password { get; set; }

  [DataType(DataType.Password)]
  [Display(Name = "Confirm password")]
  [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
  public string ConfirmPassword { get; set; }

[Required] [Display(Name = "Credential Id")] public string CredentialId { get; set; } [Required] [Display(Name = "Security Code")] public string Code { get; set; }

}

Then add two fields to the Register view like so.

<div class="editor-label"> @Html.LabelFor(m => m.CredentialId)
</div> <div class="editor-field"> @Html.TextBoxFor(m => m.CredentialId)
  @Html.ValidationMessageFor(m => m.CredentialId)
</div> <div class="editor-label"> @Html.LabelFor(m => m.Code)
</div> <div class="editor-field"> @Html.TextBoxFor(m => m.Code)
  @Html.ValidationMessageFor(m => m.Code)
</div> 

The nest step is to modify the Register action in the AccountController to activate the credential. Here is the code

[HttpPost]
public ActionResult Register(RegisterModel model)
{
  if (ModelState.IsValid)
  {

try { VipServiceProxy.Activate(model.CredentialId, model.Code, null); } catch { ModelState.AddModelError("", "The code is not valid for the given Credential Id"); } if (ModelState.IsValid) {

      MembershipCreateStatus createStatus;
      Membership.CreateUser(model.UserName, model.Password, model.Email, 

model.CredentialId

, null, true, null, out createStatus);

      if (createStatus == MembershipCreateStatus.Success)
      {
        FormsAuthentication.SetAuthCookie(model.UserName, false /* createPersistentCookie */);
        return RedirectToAction("Index", "Home");
      }
      else {
        ModelState.AddModelError("", ErrorCodeToString(createStatus));
      }
    }

}

  return View(model);
}

You can see we are calling the Activate method of the VipServiceProxy class that doesn't exist yet. If the activation succeeds the Credential Id is saved along with the user data. I've used the password question property here so I could later use 2FA to reset my password too.

Let's create that missing method. The first move is to add a service reference. You will need content of the VIP_API.zip file you downloaded earlier, so if you still don't have it get it now. Unpack it, enter the vip_auth.wsdl file path in the address, and change the Namespace to VipService. The VipServiceProxy class is a wrapper that will help us to call the VIP service endpoints and encapsulate all logic into a separate class. Go and create the VipServiceProxy class in the root project folder.

We will be using WCF to call the service. Calls should be authenticated with the certificate you downloaded before. The first two methods will create an instance of the web service proxy class and configure the channel for communication.

private static vipSoapInterface GetProxyChannel(string endpointUrl)
{
  WSHttpBinding binding = new WSHttpBinding();
  binding.Security.Mode = SecurityMode.Transport;
  binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
  EndpointAddress address = new EndpointAddress(endpointUrl);
  ChannelFactory<vipSoapInterface> channel = new ChannelFactory<vipSoapInterface>(binding, address);
  channel.Credentials.ClientCertificate.Certificate = GetCert();
  var proxy = channel.CreateChannel();
  return proxy;
}

private static X509Certificate2 GetCert()
{
  string certFile = HttpContext.Current.Server.MapPath("~/App_Data/vip_cert.p12");
  FileStream fs = File.Open(certFile, FileMode.Open, FileAccess.Read);
  byte[] buffer = new byte[fs.Length];
  int count = fs.Read(buffer, 0, buffer.Length);
  fs.Close();
  X509Certificate2 cert = new X509Certificate2(buffer, "xxxxxxx");
  return cert;
}

The first method creates the WSHttpBinding, sets security, adds the client certificate as the credentials and finally creates the channel. The second method simply loads the certificate from the file in the App_Data folder. Now is would be good time to drop the downloaded certificate into the App_Data folder. Now we can create the Activate method.

private static readonly string managementEndpoint = "https://vipservices-auth.verisign.com/mgmt/soap";

public static void Activate(string tokenId, string oneTimePassword1, string oneTimePassword2)
{
  var proxy = GetProxyChannel(managementEndpoint);
  ActivateTokenRequest req = new ActivateTokenRequest();
  req.ActivateToken = new ActivateTokenType();
  req.ActivateToken.Id = Guid.NewGuid().ToString();
  req.ActivateToken.Version = "2.0";
  req.ActivateToken.TokenId = new TokenIdType() { Value = tokenId };
  if (!string.IsNullOrEmpty(oneTimePassword1))
  {
    req.ActivateToken.OTP1 = oneTimePassword1;
    if (!string.IsNullOrEmpty(oneTimePassword2))
    {
      req.ActivateToken.OTP2 = oneTimePassword2;
    }
  }

  ActivateTokenResponse r = proxy.ActivateToken(req);
  if (r.ActivateTokenResponse1.Status.ReasonCode[0] != 0x00)
  {
    throw new Exception(string.Format("Error: {0}, Code: {1}, Details: {2}", r.ActivateTokenResponse1.Status.StatusMessage, 
r.ActivateTokenResponse1.Status.ReasonCode, r.ActivateTokenResponse1.Status.ErrorDetail)); } }

This method encapsulates the ActivateToken method exposed by the VIP management endpoint. The method accepts three parameters, token ID and up to two codes, often called also one time passwords. This method would work perfectly fine with just the token ID which allows you to create management interface where you can active multiple tokens, i.e. for your employees, shall you need one. You can also provide one or two consecutive codes (they have to be different) that will be validated before activation will be made. We will use just one code, but if you want to be extra secure you can use two. The method itself is quite simple, it sends the ActivateToken message to the web service, checks the result and throws an exception when an error occurred.

We can now test our extended registration.

image

The code I used to create the above screenshot was invalid so all looks fine. You can now log in to your VIP account management and see that you have one enabled credential. That means all we did so far works just fine.

image

Login

So we have the user registered and we know his token ID. Let us look at the login process now.

image

Login with 2FA, as I mentioned in the introductory post, is a two-step process. First you have to verify "something one knows", which in our case is the user name and the password, and then "something one has" which is the token.

Let's do them one by one. When the user press the log in button the LogOn action is executed. Let's modify it a bit.

[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
  if (ModelState.IsValid)
  {

if (Membership.ValidateUser(model.UserName, model.Password)) { TempData["CurrentUser"] = Membership.FindUsersByName(model.UserName)[model.UserName]; TempData["CurrentUserRememberMe"] = model.RememberMe; return RedirectToAction("TwoFactor", new { returnUrl = returnUrl }); } else { ModelState.AddModelError("", "The user name or password provided is incorrect."); }

  }

  return View(model);
}

Once his user name and password are confirmed to be valid, we have to read his data, get the credential ID and store it somewhere for later. I'm using the TempData here. We should also keep the RememberMe value for later use. After that we have to redirect the user to the TwoFactor action that implements the second authentication step and looks like so.

public ActionResult TwoFactor(string returnUrl)
{
  MembershipUser user = TempData["CurrentUser"] as MembershipUser;
  TempData.Keep();
  if (user == null)
  {
    return RedirectToAction("LogOn");
  }
  CredentialnModel model = new CredentialnModel { CredentialId = user.PasswordQuestion };
  return View(model);
}

Here we have to get the user from the TempData whilst keeping it for later so we will not lose it when the user will press the button. After setting the CredentialId property of the model, the view is returned. The TwoFactor view is very simple.

@model TwoFactorDemo.Models.CredentialnModel @{   ViewBag.Title = "Verify Credential";
} <h2>Log On</h2> <p> Please enter the security code generated by your VIP device.
</p> <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script> @Html.ValidationSummary(true, "Login was unsuccessful. Please correct the errors and try again.")
@using (Html.BeginForm())
{
  <div> <fieldset> <legend>Verify Credential</legend> <img src="Content/VIP_logo_RGB.png" /> <div class="editor-label"> @Html.LabelFor(m => m.CredentialId)
      </div> <div class="editor-field"> @Html.DisplayFor(m => m.CredentialId)
      </div> <div class="editor-label"> @Html.LabelFor(m => m.Code)
      </div> <div class="editor-field"> @Html.TextBoxFor(m => m.Code)
        @Html.ValidationMessageFor(m => m.Code)
      </div> <p> <input type="submit" value="Verify" /> </p> </fieldset> </div> }

And the model it's using is even simpler.

public class CredentialnModel {
  [Display(Name = "Credential Id")]
  public string CredentialId { get; set; }

  [Required]
  [Display(Name = "Security Code")]
  public string Code { get; set; }
}

There is nothing special that would need an explanation here. A bit more happens when the user presses the submit button on the view and the second TwoFactor action is executed. Here is the code.

[HttpPost]
public ActionResult TwoFactor(CredentialnModel model, string returnUrl)
{
  if (ModelState.IsValid)
  {
    try {
      MembershipUser user = TempData["CurrentUser"] as MembershipUser;
      model.CredentialId = user.PasswordQuestion;
      TempData.Keep();
      VipServiceProxy.Validate(model.CredentialId, model.Code);
      FormsAuthentication.SetAuthCookie(user.UserName, (bool)TempData["CurrentUserRememberMe"]);
      if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
          && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
      {
        return Redirect(returnUrl);
      }
      else {
        return RedirectToAction("Index", "Home");
      }
    }
    catch {
      ModelState.AddModelError("", "The code is not valid for the given Credential Id");
    }
  }

  return View(model);
}

This may look familiar to the LogOn action we modified before. As previously I'm getting the user from the TempData and then the code is validated. If the validation is successful the authentication cookie is set (that is where we need preserved RememberMe value) and the user is redirected to the page he requested. The last bit that is missing is the Validate method. Let's add it into the  VipServiceProxy class.

private static readonly string validationEndpoint = "https://vipservices-auth.verisign.com/val/soap";

public static void Validate(string tokenId, string oneTimePassword)
{
  var proxy = GetProxyChannel(validationEndpoint);
  ValidateRequest req = new ValidateRequest();
  req.Validate = new ValidateType();
  req.Validate.Id = Guid.NewGuid().ToString();
  req.Validate.Version = "2.0";
  req.Validate.TokenId = new TokenIdType() { Value = tokenId };
  req.Validate.OTP = oneTimePassword;
  ValidateResponse r = proxy.Validate(req);
  if (r.ValidateResponse1.Status.ReasonCode[0] != 0x00)
  {
    throw new Exception(string.Format("Error: {0}, Code: {1}, Details: {2}", r.ValidateResponse1.Status.StatusMessage, 
r.ValidateResponse1.Status.ReasonCode, r.ValidateResponse1.Status.ErrorDetail)); } else { if (!r.ValidateResponse1.Status.StatusMessage.Equals("success", StringComparison.OrdinalIgnoreCase)) { throw new InvalidOperationException("Response to Validate message returned no error but no 'success' response received."); } } }

The method is very similar to the Activate method we created before, except this time the validation endpoint is used. The web service Validate call returns the status code "Success" when the validation is successful. We have to check the response and throw an exception when anything else is returned. That's it so let's test it.

image

After I entered my username and password I was asked for the code and only when I entered the correct code I was logged in. Perfect.

Summary

I hope this post illustrated how easy is to add two-factor authentication to a MVC application. The process itself is very easy and can be extended to include other token providers in no time. The complete source code can be downloaded from here.

There is also much more than just two-factor authentication. Tokens can be used to reset password, recover account and more. You can also provide an infrastructure for users to log in when they don't have the token available by generating one time code for them. I will be happy to write about all of that if there will be enough of interest.

I would also love to know if any of you readers will use that. Please share your experience.

Tags: , , , , , ,

Identity | SAML | Security

blog comments powered by Disqus