Need Quality Code? Get Silver Backed

2-Factor Auth in MVC

20thFeb

0

by Gary H

The weakest link in security is people. Whether this comes in the form of social engineering, poor code or user created vulnerabilities it is our job as developers to mitigate this insecurity as much as possible. To that end adding two factor authentication can help raise another barrier against malicious access to your sites.

Two Factor Authentication (TFA) is the process whereby we ask for another unique piece of information from a user at login that is generated using a thing that they must physically posess. In our example we will look at using TFA using Google Authenticator in time based mode but the principles apply to whatever solution you choose to apply. In addition, the techniques explored here will work for any authenticator that can support time based authentication.

We'll be using time-based one-time password algorithm which is an open standard published under RFC 6238. The algorithm takes a known time (the start of the UNIX epoch) to act as a counter. The count is then passed into the HMAC-Based One-Time Password Algorithm (HOTP) which is published under RFC 4226. HOTP takes this counter, creates a HMAC hash and from this we generate our unique 6-digit authentication code.

Getting Started

The first step is implementing the HOTP and TOTP algorithms for use in our code. Below is a sample class that implements HOTP, TOTP and a validity check which will ensure that an incoming code is correct.

using System;
using System.Security.Cryptography;

public class TwoFactorAuth
{
	public static readonly DateTime UnixEpoch = 
			new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

	public static string GeneratePassword(string secret)
	{
		return GeneratePassword(secret, GetCurrentCounter());
	}

	public static string GeneratePassword(	string secret, 
											long iterationNumber, 
											int digits = 6)
	{
		var counter = BitConverter.GetBytes(iterationNumber);
		if (BitConverter.IsLittleEndian)
		{
			Array.Reverse(counter);
		}

		var key = Base32Encoder.ToBytes(secret);
		using (var hmac = new HMACSHA1(key, true))
		{
			var hash = hmac.ComputeHash(counter);

			int offset = hash[hash.Length - 1] & 0xf;

			int binary =
				((hash[offset] & 0x7f) << 24)
				| ((hash[offset + 1] & 0xff) << 16)
				| ((hash[offset + 2] & 0xff) << 8)
				| (hash[offset + 3] & 0xff);

			int password = binary % (int)Math.Pow(10, digits); // 6 digits

			return password.ToString(new string('0', digits));
		}
	}

	public static bool IsValid(string secret, string password, 
								int checkAdjacentIntervals = 1)
	{
		if (password == GeneratePassword(secret))
		{
			return true;
		}

		var counter = GetCurrentCounter();

		for (int i = 1; i <= checkAdjacentIntervals; i++)
		{
			if (password == GeneratePassword(secret, counter + i))
			{
				return true;
			}

			if (password == GeneratePassword(secret, counter - i))
			{
				return true;
			}
		}

		return false;
	}

	public static long GetCurrentCounter()
	{
		return (long)(DateTime.UtcNow - UnixEpoch).TotalSeconds / 30;
	}
}

The secret that we work against is a Base32 encoded string. As .Net does not have a Base32 encoder we need to make one ourselves or acquire one. I would recommend this Stack Overflow question as a starting point. To generate the secret we take 10 random bytes and base32 encode them. An example for this is:

var usr = Users.GetByUsername(User.Identity.Name);
if (String.IsNullOrEmpty(usr.TwoFactorSecret))
{
	var buffer = new byte[10];
	using (var rng = RNGCryptoServiceProvider.Create())
	{
		rng.GetBytes(buffer);
	}
	usr.TwoFactorSecret = Base32Encoder.ToString(buffer);
	Users.Save(usr);
}

QR Encoding

We then need to get this into the hands of our user. You should display the secret, an identifier (like username@yoursite.com) and for ease of use display a QR code that they can scan directly with their authenticator. The QR code display is a particularly nice touch and deceptively simple to do. We can tap into an open service offered by Google that generates QR codes for just this purpose. This can be bundled into a HTML Helper such as:

public static class QRCodeHtmlHelper
{
	public static MvcHtmlString QRCode(this HtmlHelper htmlHelper, string data, int size = 80, int margin = 4, QRCodeErrorCorrectionLevel errorCorrectionLevel = QRCodeErrorCorrectionLevel.Low, object htmlAttributes = null)
	{
		if (data == null)
			throw new ArgumentNullException("data");
		if (size < 1)
			throw new ArgumentOutOfRangeException("size", size, "Must be greater than zero.");
		if (margin < 0)
			throw new ArgumentOutOfRangeException("margin", margin, "Must be greater than or equal to zero.");
		if (!Enum.IsDefined(typeof(QRCodeErrorCorrectionLevel), errorCorrectionLevel))
			throw new InvalidEnumArgumentException("errorCorrectionLevel", (int)errorCorrectionLevel, typeof(QRCodeErrorCorrectionLevel));

		var url = string.Format("http://chart.apis.google.com/chart?cht=qr&chld={2}|{3}&chs={0}x{0}&chl={1}", size, HttpUtility.UrlEncode(data), errorCorrectionLevel.ToString()[0], margin);

		var tag = new TagBuilder("img");
		if (htmlAttributes != null)
			tag.MergeAttributes(new RouteValueDictionary(htmlAttributes));
		tag.Attributes.Add("src", url);
		tag.Attributes.Add("width", size.ToString());
		tag.Attributes.Add("height", size.ToString());

		return new MvcHtmlString(tag.ToString(TagRenderMode.SelfClosing));
	}
}

Then in our view we can show the QR code using:

	@Html.QRCode(string.Format("otpauth://totp/{0}?secret={1}", 
		Model.UserLabel, Model.EncodedSecret), 
		256, 4, QRCodeErrorCorrectionLevel.QuiteGood)

Authentication

Authentication code and secret in hand we need to plumb it into our login procedure. The simplest way is to extend your login model with an additional string for the authentication token. In your AccountController (or wherever else you use to handle the user login) you should verify the token after the standard MembershipProvider is invoked. For example we may want something like:

if (Membership.ValidateUser(model.UserName, model.Password))
{
	var usr = Users.GetByUsername(model.UserName);

	if (usr.TwoFactorSecret != null)
	{
		if (String.IsNullOrEmpty(model.AuthenticatorToken))
		{
			ModelState.AddModelError("", "Invalid Password.");
			return View(model);
		}
		
		if (!TwoFactorAuth.IsValid(usr.TwoFactorSecret, 
									model.AuthenticatorToken))
		{
			ModelState.AddModelError("", "Invalid Password.");
			return View(model);
		}
	}
	
	// ... SNIP
}

Of course this could be refined such as if two-factor auth will be optional in your site you may want to check at login then redirect to a token prompt only if it is enabled.

That's it. Simple two-factor authentication backed by the Google Authenticator.

C# , MVC , Security

Comments are Locked for this Post