Click here to Skip to main content
16,017,285 members
Articles / Web Development / ASP.NET

Implementing Two Factor Authentication in ASP.NET MVC with Google Authenticator

Rate me:
Please Sign up or sign in to vote.
4.97/5 (65 votes)
21 Aug 2013MIT6 min read 432.4K   9.8K   180   81
How to add extra security to your MVC web application, using two factor authentication.

What is Two Factor Authentication?

Two Factor Authentication is a way to authenticate users using two of the three valid authentication factors: something the user knows (password, PIN, etc), something the user has (smart card, phone, ATM card, etc.), and something the user is (biometric data, including figerprints). In the case of this article, we will be using something the user knows, a password, and something the user has, a smartphone. 

What is Google Authenticator? 

Google Authenticator is a software based two-factor authentication token. It is available on iOS, Android, and BlackBerry operating systems. It provides a 6 digit, time or counter based number that acts as the 2nd factor for our two factor authentication.

Image 1 

Here is a link to a YouTube video describing Google Authenticator. 

How does it work? 

Google Authenticator implements the algorithms defined in RFC 4226 and RFC 6238. The first is a counter based implementation of two-factor authentication. The second is a time-based implementation. First, the server and the user agree on a secret key to use as the seed value for the hashing function. The user can type in this key to Google Authenticator or use a QR code to automatically set up your application. Then Google Authenticator uses one of the above algorithms to generate a code to be entered during authentication. Your server will then use the same algorithm and secret key to check the code. Once the secret key has been agreed on, the only data passing between the client and your server will be the 6-digit key generated by the Google Authenticator application. At no time does any of this data pass through Google's servers. 

Counter Based One Time Password Generation

To generate a one-time password, we need three pieces of information, the secret key, the counter number, and the number of digits the output should be. Since we are using Google Authenticator, we are limited to 6 digits. 

Here is the full GeneratePassword method:

C#
public static string GeneratePassword(string secret, long iterationNumber, int digits = 6)
{
    byte[] counter = BitConverter.GetBytes(iterationNumber);

    if (BitConverter.IsLittleEndian)
        Array.Reverse(counter);

    byte[] key = Encoding.ASCII.GetBytes(secret);

    HMACSHA1 hmac = new HMACSHA1(key, true);

    byte[] 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));
}

Let's go through what we are doing. First, we convert the iteration number to a byte[], which can be hashed using the HMAC-SHA-1 hash method. The iteration number should be incremented on the client and server every time authentication succeeds. We use the managed HMAC-SHA-1 hashing method available from the <a href="http://msdn.microsoft.com/en-us/library/system.security.cryptography.hmacsha1.aspx">System.Security.Cryptography.HMACSHA1</a> class. Next we compute the hash for the current value of the counter. The next part of the code extracts the binary value of a 4 byte integer, then shrinks it to the number of digits required. That's it. The entire algorithm in 25 lines. RFC 4226 Section 5.4 has a good example and description of what is happening, which I will copy and paste here:

5.4.  Example of HOTP Computation for Digit = 6

   The following code example describes the extraction of a dynamic
   binary code given that hmac_result is a byte array with the HMAC-
   SHA-1 result:

        int offset   =  hmac_result[19] & 0xf ;
        int bin_code = (hmac_result[offset]  & 0x7f) << 24
           | (hmac_result[offset+1] & 0xff) << 16
           | (hmac_result[offset+2] & 0xff) <<  8
           | (hmac_result[offset+3] & 0xff) ;

   SHA-1 HMAC Bytes (Example)

   -------------------------------------------------------------
   | Byte Number                                               |
   -------------------------------------------------------------
   |00|01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|
   -------------------------------------------------------------
   | Byte Value                                                |
   -------------------------------------------------------------
   |1f|86|98|69|0e|02|ca|16|61|85|50|ef|7f|19|da|8e|94|5b|55|5a|
   -------------------------------***********----------------++|

   * The last byte (byte 19) has the hex value 0x5a.
   * The value of the lower 4 bits is 0xa (the offset value).
   * The offset value is byte 10 (0xa).
   * The value of the 4 bytes starting at byte 10 is 0x50ef7f19,
     which is the dynamic binary code DBC1.
   * The MSB of DBC1 is 0x50 so DBC2 = DBC1 = 0x50ef7f19 .
   * HOTP = DBC2 modulo 10^6 = 872921.

   We treat the dynamic binary code as a 31-bit, unsigned, big-endian
   integer; the first byte is masked with a 0x7f.

   We then take this number modulo 1,000,000 (10^6) to generate the 6-
   digit HOTP value 872921 decimal.

Time Based One Time Password Generation

RFC 6238 defines the time based implementation of the one time password generation. Time based one time password generation builds on the counter based approach above. It is exactly the same, except it automatically defines the counter based on intervals of time since the Unix epoch (Jan 1, 1970, 00:00 UTC). Technically, the RFC allows for any start date and time interval, but Google Authenticator requires the Unix epoch and a 30 second time interval. What this means is that we can get the current one-time-password using only the secret key. Here is how:

C#
public static readonly DateTime UNIX_EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

public static string GetPassword(string secret)
{
    long counter = (long)(DateTime.UtcNow - UNIX_EPOCH).TotalSeconds / 30;

    return HashedOneTimePassword.GeneratePassword(secret, counter);
}

As you can see, we are just getting the number of 30 second intervals since the Unix epoch and using that as our counter value. This means that the clock on both the client and the server need to be kept in sync with each other. This is commonly done with the Network Time Protocol.

How do I put it to use?

Well, now we have covered how the code works, the next question is how do you use it? I have created some extra overloads for the GetPassword method for time-based generation, and added an IsValid method.

C#
public static bool IsValid(string secret, string password, int checkAdjacentIntervals = 1)
{
    if (password == GetPassword(secret))
        return true;

    for (int i = 1; i <= checkAdjacentIntervals; i++)
    {
        if (password == GetPassword(secret, GetCurrentCounter() + i))
            return true;

        if (password == GetPassword(secret, GetCurrentCounter() - i))
            return true;
    }

    return false;
}

IsValid helps a little with clock skew by checking adjacent intervals for the password as well. This can help improve user experience a lot, because it doesn't require the clocks to be perfectly aligned. 

Create the MVC Web Application

Create a new MVC 3 Web Application using the "New Project" wizard in Visual Studio 2010. Be sure to select "Internet" application in the wizard. This will create the default Account controller and views needed for forms authentication.

Create the TwoFactorProfile Class

Next we create a Profile class that inherits from ProfileBase. This will store the 2 factor secret for a given user.

C#
public class TwoFactorProfile : ProfileBase
{
    public static TwoFactorProfile CurrentUser
    {
        get
        {
            return GetByUserName(Membership.GetUser().UserName);
        }
    }

    public static TwoFactorProfile GetByUserName(string username)
    {
        return (TwoFactorProfile)Create(username);
    }

    public string TwoFactorSecret
    {
        get
        {
            return (string)base["TwoFactorSecret"];
        }
        set
        {
            base["TwoFactorSecret"] = value;
            Save();
        }
    }
}

Modify the web.config

Modify the <system.web><profile> element to inherit from the TwoFactorProfile class we just created:

XML
<profile inherits="TwoFactorWeb.TwoFactorProfile">

Modify AccountController

We need to modify AccountController in a few places. First, the Register action needs to be modified to send the user to the ShowTwoFactorSecret page, so they can set up their Google Authenticator. In the Register action, modify the RedirectToAction from:

C#
return RedirectToAction("Index", "Home");

to:

C#
return RedirectToAction("ShowTwoFactorSecret", "Account");

Next we create the ShowTwoFactorSecret action:

C#
[Authorize]
public ActionResult ShowTwoFactorSecret()
{
    string secret = TwoFactorProfile.CurrentUser.TwoFactorSecret;

    if (string.IsNullOrEmpty(secret))
    {
        byte[] buffer = new byte[9];

        using (RandomNumberGenerator rng = RNGCryptoServiceProvider.Create())
        {
            rng.GetBytes(buffer);
        }

        // Generates a 10 character string of A-Z, a-z, 0-9
        // Don't need to worry about any = padding from the
        // Base64 encoding, since our input buffer is divisible by 3
        TwoFactorProfile.CurrentUser.TwoFactorSecret = Convert.ToBase64String(buffer).Substring(0, 10).Replace('/', '0').Replace('+', '1');

        secret = TwoFactorProfile.CurrentUser.TwoFactorSecret;
    }

    var enc = new Base32Encoder().Encode(Encoding.ASCII.GetBytes(secret));

    return View(new TwoFactorSecret { EncodedSecret = enc });
}

This just generates a new random 10 character secret, then shows it to the user in Base32 encoded format, which is how Google Authenticator expects the user to enter it. Feel free to create your secret any way you want, but it needs to be at least 10 characters or Google Authenticator will complain.

Finally, we change the LogOn action to check the code provided by the user to ensure it is valid. Our new LogOn action is below:

C#
[HttpPost]
public ActionResult LogOn(LogOnModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        if (Membership.ValidateUser(model.UserName, model.Password))
        {
            var profile = TwoFactorProfile.GetByUserName(model.UserName);

            if (profile != null && !string.IsNullOrEmpty(profile.TwoFactorSecret))
            {
                if (TimeBasedOneTimePassword.IsValid(profile.TwoFactorSecret, model.TwoFactorCode))
                {
                    FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
                    if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1 && returnUrl.StartsWith("/")
                        && !returnUrl.StartsWith("//") && !returnUrl.StartsWith("/\\"))
                    {
                        return Redirect(returnUrl);
                    }
                    else
                    {
                        return RedirectToAction("Index", "Home");
                    }
                }
                else
                {
                    ModelState.AddModelError("", "The two factor code is incorrect.");
                }
            }
            else
            {
                ModelState.AddModelError("", "The two factor code is incorrect.");
            }
        }
        else
        {
            ModelState.AddModelError("", "The user name or password provided is incorrect.");
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Modify AccountModels

To make the new LogOn and ShowTwoFactorSecret actions work, we need to add a field to the LogOnModel class:

C#
[Required]
[Display(Name = "Google Authenticator Code")]
public string TwoFactorCode { get; set; }

and create our new TwoFactorSecret class:

C#
public class TwoFactorSecret
{
    public string EncodedSecret { get; set; }
} 

Modify the LogOn.cshtml View

Now we modify the LogOn view to add the new TwoFactorCode field the user needs to enter:

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

Create the ShowTwoFactorSecret View

Finally, we create the ShowTwoFactorSecret view: 

ASP.NET
@model TwoFactorWeb.Models.TwoFactorSecret
@{
    ViewBag.Title = "ShowTwoFactorSecret";
}

<h2>Show Two Factor Secret</h2>

<p>
    Add the code below to Google Authenticator:
</p>
<p>
    @Html.QRCode(string.Format("otpauth://totp/MY_APP_LABEL?secret={0}", Model.EncodedSecret))
</p>
<p>
    @Model.EncodedSecret
</p>

As you can see, we show an image of a QR code the user can scan and we also show the secret as a string the user can manually enter. The format of the QR code is defined here.

See the result

After registering as a new user in the web application, you should see a screen like the following:

Image 2

At this point, you should scan the QR code with the Google Authenticator app, or enter the code below the QR code manually:

Image 3 

Now, when you log in, you should see a new field to enter your "Google Authenticator Code":

Image 4

Just enter the current 6-digit code on the Google Authenticator screen for your application:

Image 5 

If you entered your username, password and code correctly, you should be able to log in. 

History 

  • 13 June 2012 - First release. 
  • 14 June 2012 - Added screenshots of the web app and Google Authenticator. Thanks to Priyank Bolia for suggesting this. 
  • 13 August 2012 - Updated to use a more secure method of generating the random secret for the user. Thanks to rhoffman for pointing this out. 
  • 11 September 2012 - Updated to add a time delay between login attempts for a given user. This helps mitigate the chance of a brute force attack when the attacker knows the password. Updated to generate the QR code directly, instead of relying on an external service.  
  • 23 October 2012 - Updated the code attached to provided caching of previously used one-time passwords, to prevent them from being used more than once. Thanks to MatrixQN for pointing out this issue. 

Full source code is available on github.  

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Software Developer (Senior)
United States United States
I have been a software developer since 2005, focusing on .Net applications with MS SQL backends, and recently, C++ applications in Linux, Mac OS X, and Windows.

Comments and Discussions

 
GeneralRe: My vote of 5 Pin
Rick Bassham2-Jul-12 10:20
Rick Bassham2-Jul-12 10:20 
QuestionThanks for sharing Pin
Patrick Kalkman25-Jun-12 6:27
Patrick Kalkman25-Jun-12 6:27 
AnswerRe: Thanks for sharing Pin
Rick Bassham25-Jun-12 7:20
Rick Bassham25-Jun-12 7:20 
GeneralMy vote of 5 Pin
Reiss22-Jun-12 3:01
professionalReiss22-Jun-12 3:01 
GeneralRe: My vote of 5 Pin
Rick Bassham22-Jun-12 3:24
Rick Bassham22-Jun-12 3:24 
GeneralMy vote of 5 Pin
Madhan Mohan Reddy P19-Jun-12 17:51
professionalMadhan Mohan Reddy P19-Jun-12 17:51 
GeneralRe: My vote of 5 Pin
Rick Bassham20-Jun-12 4:39
Rick Bassham20-Jun-12 4:39 
GeneralMy vote of 5 Pin
Rajesh Pillai16-Jun-12 3:18
Rajesh Pillai16-Jun-12 3:18 
Thanks, something of immediate use to me.
GeneralRe: My vote of 5 Pin
Rick Bassham19-Jun-12 6:25
Rick Bassham19-Jun-12 6:25 
GeneralMy vote of 5 Pin
Tim Corey15-Jun-12 4:29
professionalTim Corey15-Jun-12 4:29 
GeneralRe: My vote of 5 Pin
Rick Bassham15-Jun-12 5:03
Rick Bassham15-Jun-12 5:03 
GeneralMy vote of 5 Pin
JF201514-Jun-12 8:28
JF201514-Jun-12 8:28 
GeneralRe: My vote of 5 Pin
Rick Bassham15-Jun-12 4:00
Rick Bassham15-Jun-12 4:00 
GeneralMy vote of 5 Pin
Abinash Bishoyi14-Jun-12 8:18
Abinash Bishoyi14-Jun-12 8:18 
GeneralRe: My vote of 5 Pin
Rick Bassham15-Jun-12 4:00
Rick Bassham15-Jun-12 4:00 
GeneralMy vote of 5 Pin
Priyank Bolia13-Jun-12 18:11
Priyank Bolia13-Jun-12 18:11 
GeneralRe: My vote of 5 Pin
Rick Bassham14-Jun-12 3:22
Rick Bassham14-Jun-12 3:22 
GeneralMy vote of 5 Pin
sam.hill13-Jun-12 11:58
sam.hill13-Jun-12 11:58 
GeneralRe: My vote of 5 Pin
Rick Bassham13-Jun-12 15:54
Rick Bassham13-Jun-12 15:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.