ASP.NET MVC - Authentication (Two-Factor, MembershipProvider, SimpleMembership)

 
9/18/2013
.NET, ASP.NET, C#, MVC
3 Comments

Most web applications are using username and password for authentication. ASP.NET supports this concept since the very beginning.
With MVC 4 Microsoft also introduced the SimpleMembership, which makes authentication and user management more flexible.
In this post I show the various options for authentication for ASP.NET MVC applications, including a demo that implements Two-Factor authentication.

Forms authentication (Web.config)

If your application should only be available for a limited number of users, then forms authentication is a good choice. The users and their passwords are configured statically in Web.config:

<authentication mode="Forms">
   <forms name=".ASPXAUTH" loginUrl="~/Account/Login" 
timeout="40000">
     <credentials passwordFormat="SHA1">
       <user name="admin" 
password="D033E22AE348AEB5660FC2140AEC35850C4DA997" />
     </credentials>
   </forms>
</authentication>

To check if a username and password is valid, you have to call the following (deprecated) method:

FormsAuthentication.Authenticate(userName, password)

As soon as you need dynamic user accounts or role management, this approach will not work for you.

Custom MembershipProvider

Many websites use the default SqlMembershipProvider. This approach is quite easy to setup but not very extensible.
Implementing a custom Membership- and RoleProvider is not very difficult, since you only have to implement two methods to get started. The advantage of this approach is that you have full control how the users and roles are stored. You could use a separate database or integrate into an existing one. It's totally up to you.

In my example I'm using a custom database with Entity Framework.

The MembershipProvider could look like this (only the method ValidateUser is important):

public class SampleMembershipProvider : MembershipProvider
{
  public override bool ValidateUser(string username, string password)
  {
    username = username.ToLowerInvariant();

    User user = null;

    using (var db = new DatabaseContext())
    {
      user = db.Users.SingleOrDefault(u => u.Username == username);
    }

    if (user == null || password == null)
    {
      return false;
    }

    return user.ValidatePassword(password);
  }
}

The RoleProvider could look like this (only the method GetRolesForUser is important):

public class SampleRoleProvider : RoleProvider
{
  public override string[] GetRolesForUser(string username)
  {
    username = username.ToLowerInvariant();

    User user = null;

    using (var db = new DatabaseContext())
    {
      user = db.Users.Single(u => u.Username == username);
    }

    return Enum.GetValues(typeof(RoleType))
      .Cast<RoleType>()
      .Where(r => user.Role.HasFlag(r))
      .Select(r => r.ToString())
      .ToArray();
  }
}

Now you have to register the providers in Web.config:

<roleManager defaultProvider="DefaultRoleProvider" enabled="true">
  <providers>
    <clear />
    <add name="DefaultRoleProvider" type="YOURNAMESPACE.SampleRoleProvider" />
  </providers>
</roleManager>

<membership defaultProvider="DefaultMembershipProvider" userIsOnlineTimeWindow="15"> <providers> <clear /> <add name="DefaultMembershipProvider" type="YOURNAMESPACE.SampleMembershipProvider" /> </providers> </membership>

The following line of code verifies a given user:

Membership.Provider.ValidateUser(userName, password)

SimpleMembership

SimpleMembership is built on top of Membership- and RoleProvider. It is more flexible and enables you to authenticate via OAuth/OpenId. Have a look at the post of Jon Galloway to get the full picture.
Also the default template for a MVC 4 application uses the SimpleMembership system.

To verify a username and password use the following statement:

WebSecurity.Login(userName, password)

Two-Factor Authentication

In this final example I will show how you could extend the SimpleMembership system to use Two-Factor authentication with Google Authenticator.
Additionally to username and password a 6 digits code has to be entered to login. This code is generated by Google Authenticator. It is calculated based on a secret key and a counter or time.
The code is calculated on the server using the same algorithm, if both keys match, the user is allowed to login.

The demo application is based on the default template that comes with MVC 4. First you have to add the property TwoFactorSecret to the UserProfile class.

[Table("UserProfile")]
public class UserProfile
{
  [Key]
  [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
  public int UserId { get; set; }
  public string UserName { get; set; }
  public string TwoFactorSecret { get; set; }
}

Whenever a new user registers we have to generate a secret key for him, the password must have at least 10 characters:

public static string GenerateSecretKey()
{
  byte[] buffer = new byte[9];

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

  return Convert.ToBase64String(buffer)
    .Substring(0, 10)
    .Replace('/', '0')
    .Replace('+', '1');
}

After the user account is created, the user is redirected to a page where he can see his secret key. It's displayed Base32 encoded (Google Authenticator expects this format). The view contains a QR code as well, this makes it easier to transfer the secret key to Google Authenticator. The QR code shows the secret key embedded in the following URL format:
otpauth://totp/NAMEOFYOURWEBSITE?secret=SECRETKEYASBASE32

.

Secret Key

The secret key should be kept very carefully. If you loose the key and you phone, are won't be able to login again!

Since I chose to use the time based algorithm, the password changes every 30 seconds. To calculate the current key we have to use this algorithm:

public static string GenerateTimeBasedPassword(string secret, int digits = 6)
{
  long timeBasedCounter = (long)(DateTime.UtcNow - UnixStartTime).TotalSeconds / 30;

  byte[] counter = BitConverter.GetBytes(timeBasedCounter);

  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);

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

To verify a login we need the following code:

UserProfile user = null;

using (var db = new UsersContext())
{
    user = db.UserProfiles.SingleOrDefault(u => u.UserName == userName);
}

if (user != null
    && TwoFactorPasswordGenerator.GenerateTimeBasedPassword(user.TwoFactorSecret) == twoFactorCode
    && WebSecurity.Login(userName, password))
{
  return RedirectToLocal(returnUrl);
}

With MVC 5 the identity system was changed again. Microsoft.AspNet.Identity.Core 2.0 has some extensions points to integrate custom Two-Factor providers. Jerrie Pelser has a nice blog post that explains all the details.

Conclusion

There are several solutions available for user authentication in ASP.NET MVC. The demo solution contains sample projects for the methods I discussed above. Feel free to have a look at the projects and to modify the code as you need it.

Updates

05.05.2014: Added some information regarding MVC 5 and ASP.NET Identity.

Downloads

Feedly Feedly Tweet


Related posts


Comments


Daniel

Daniel

6/1/2019

@Eric: I don't have a (public) sample of this. But this blog has a custom implementation using a custom MembershipProvider and 2-factor code. It's not that hard to implement the login process. The setup pages with QR code and password reset are more work, but I don't need that for this blog.


Eric

Eric

5/31/2019

Is there any example of using a Custom MembershipProvider and two-factor authentication with the Microsoft Authenticator app? Some older apps aren't in a position to upgrade a custom MembershipProvider to either Identity or .NET Core Identity without a head-to-toe rebuild. Thanks!


devtools korzh

devtools korzh

9/22/2013
http://devtools.korzh.com/

Thanks for the useful info!