Adding two-factor authentication to an application using ASP.NET Identity

Adding two-factor authentication to an application using ASP.NET Identity

  • Comments 5

Introduction

We recently released the 2.0.0-beta1 version of ASP.NET Identity. Learn more here by visiting this link. This is an update to 2.0.0-alpha1 and adds two-factor authentication along with a few bug fixes. To learn more about the Alpha release, visit this link.

As mentioned in the release article, with the 2.0.0-beta1 version of Identity we have added support for enabling two-factor authentication in an application. Two factor authentication is a 2 step process for authenticating a user. For example, using local account credentials and a secure PIN sent as a text message, or using OAuth login and QR code readers. More information on multi-factor authentication can be found here.

At a base level the two factor authentication flow is explained below

clip_image002[5]

Step 1 is when the user enters his/her local username and password on the Login page. If the local credentials are valid, then an email is sent to the registered email address of the user with the security PIN. An alternative is to send the PIN in a text message. The user then enters the PIN on the web page. The PIN is validated and the user logged in to the application. To do this we need to initially confirm the email or the phone number that the user provides during the registration process.

The current article explains how to add two-factor authentication in an application where the users use their local account credentials and a secure PIN sent in email. We will start with an application created using Visual Studio 2013, update the Identity packages to 2.0.0-beta1, add code to confirm user password, register and enable the email token provider for the two step authentication, and verify the functionality.

All the code in the article is available in the newly released Microsoft.AspNet.Identity.Samples package which has some additional features too. The article here is a walkthrough explaining the steps as we add the feature in the application.

Create application and configure UserManager

The application created using Visual Studio 2013 has Identity version 1.0.0. This article describes updating the version to 2.0.0-beta1 and configuring the UserManager class in the application in the recommend manner. This article shows you how can get a single instance of UserManager per request.

Please follow the article as the first step in the adding the new features.

Setting up test email client

For the two factor authentication shown in this post, the security PIN is emailed to the user. The UserManager has properties which are used for the messaging operations. The two properties ‘EmailService’ and ‘SmsService’ can be set to the implementation of ‘IIdentityMessageService’ interface to send an email or a text message. The ‘IIdentityMessageService’ has a single ‘SendAsync’ method which is implemented according to the messaging action. For our sample, we will create a class to send an email using a test SMTP server.

For the sake of this article we will use the Windows Live SMTP server as a test server which is available free of cost if you have a Windows Live account.

Create a class called EmailService in the project. Paste the code below

  1. publicclassEmailService : IIdentityMessageService
  2.     {
  3.         publicTask SendAsync(IdentityMessage message)
  4.         {
  5.             MailMessage email = newMailMessage("XXX@hotmail.com", message.Destination);
  6.  
  7.             email.Subject = message.Subject;
  8.  
  9.             email.Body = message.Body;
  10.  
  11.             email.IsBodyHtml = true;
  12.  
  13.             var mailClient = newSmtpClient("smtp.live.com", 587) { Credentials = newNetworkCredential("XXX@hotmail.com", "password"), EnableSsl = true };
  14.  
  15.             return mailClient.SendMailAsync(email);
  16.         }
  17.     }

The IdentityMessage class is the wrapper class for the message to be sent to the user. It has the following properties

· Destination: Where the message is to be sent. In our case, the user’s email address

· Subject: message subject

· Body: the actual message

The ‘SendAsync’ method takes the IdentityMessage class parameter which has all the properties set by the UserManager. We construct the email message as a System.Net.Mail.MailMessage object and set the respective properties. We make the mail to be in the form of HTML so that the users can click on the links in the email. The SMTP host for the Windows Live server is ‘smtp.live.com’ and the port is ‘587’. The credentials property of the SmtpClient class should have the Windows Live account credentials of the application developer testing this feature.

This is a sample mail client and can be extended as needed. Users can substitute the Windows Live SMTP server with, for example, SendGrid to send email to the users.

Confirm user email

To send the two-factor login PIN to the user via email, we need to confirm the email address the user provides during registration. The UserManager class exposes methods that let you obtain tokens and confirm user email addresses using these security tokens.

1. We need to first register a token provider which generates and validates tokens to be emailed in the confirmation link to the user. Using this secure token the user can confirm their email address. This token provider property has to be set on the UserManager and hence can be done in the ApplicatioUserManager.Create method.

The IdentityFactoryOptions parameter has a property DataProtectionProvider which is an implementation of the OWIN Data Protection API (DPAPI) feature. This is set in the ‘CreatePerOwinContext<T>’ method in the AppBuildExtensions class. The code can be viewed by reflecting the Microsoft.AspNet.Identity.Owin dll. We can use this implementation for the UserTokenProvider in UserManager.

  1. publicstaticApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
  2.         {
  3.             var manager = newApplicationUserManager(newUserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
  4.  
  5.             // Configure validation logic for passwords
  6.             manager.PasswordValidator = newPasswordValidator
  7.             {
  8.                 RequiredLength = 6,
  9.                 RequireNonLetterOrDigit = false,
  10.                 RequireDigit = false,
  11.                 RequireLowercase = false,
  12.                 RequireUppercase = false,
  13.             };
  14.  
  15.             var dataProtectionProvider = options.DataProtectionProvider;
  16.  
  17.             if (dataProtectionProvider != null)
  18.             {
  19.                 manager.UserTokenProvider = newDataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
  20.             }
  21.  
  22.             return manager;
  23.         }

This registers a single token provider that is used to generate tokens for user email confirmation and resetting password.

2. Next assign the ‘EmailService’ property on the UserManager to the ‘EmailService’ class created in the previous section inside the ‘Create’ method

  1. manager.EmailService = newEmailService();

Next we steps outline the process of modifying VS 2013 RTM templates to use enable the user email confirm feature.

3. In the current project, users register themselves with username and password. We shall change the registration flow to take an additional email field along with the username. Edit the RegisterViewModel to add another property for Email.

  1. publicclassRegisterViewModel
  2.     {
  3.         [Required]
  4.         [Display(Name = "User name")]
  5.         publicstring UserName { get; set; }
  6.  
  7.         [Required]
  8.         [Display(Name = "Email address")]
  9.         publicstring Email { get; set; }
  10.  
  11.         ......
  12.     }

Note: In this example we are setting the username and email separately. You can also choose to set the email as the username. Refer to the Microsoft.Aspnet.Identity.Samples Nuget package for implementing this.

4. In the Register.cshtml page, add a new ‘div’ tag for the email similar to the one for username.

  1. <divclass="form-group">
  2.         @Html.LabelFor(m => m.Email, new { @class = "col-md-2 control-label" })
  3.         <divclass="col-md-10">
  4.             @Html.TextBoxFor(m => m.Email, new { @class = "form-control" })
  5.         </div>
  6.     </div>

5. In the Register post method of the AccountController, set the email from the view model class when creating a user.

  1. var user = newApplicationUser() { UserName = model.UserName, Email = model.Email };

6. In the current registration logic, the user is signed in once the user is registered. We will change the flow to get the email confirmation token and email it to the user. The users need to check their email for the confirmation link and confirm their account. In the Register post method replace the following code:

  1. if (result.Succeeded)
  2.                 {
  3.                     await SignInAsync(user, isPersistent: false);
  4.  
  5.                     return RedirectToAction("Index", "Home");
  6.  
  7.                 }

with the following code for sending the confirmation link to user via email.

  1. if (result.Succeeded)
  2.                 {
  3.                     string code = await UserManager.GetEmailConfirmationTokenAsync(user.Id);
  4.                     var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: Request.Url.Scheme);
  5.                     await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please confirm your account by clicking this link: <a href=\"" + callbackUrl + "\">link</a>");
  6.                     return View("ShowEmail");
  7.                 }

We get the email confirmation token which then used to create an url pointing to the ‘ConfirmEmail’ action in the application. The UserManager.SendEmailSync method called the ‘SendAsync’ method of the ‘EmailService’ to send the email. The user is redirected to a ‘ShowEmail’ page with a message to check the inbox of the user’s email.

7. Add a view ShowEmail.cshtml that displays a message to user to check their email for a link to confirm their email address

  1. <h3>
  2.     Please check your email for a link to confirm your email address
  3. </h3>

8. In step 6, in the code we see that the token generated is included in a URL that the user clicks to confirm the email. The link will point to an ActionMethod ‘ConfirmEmail’ in the AccountController. We need a method to bind the parameters from the URL, verify that this token is valid one for the user and then confirm the email. Add a new method called ConfirmEmail in the AccountController as below

  1. [HttpGet]
  2.         [AllowAnonymous]
  3.         publicasyncTask<ActionResult> ConfirmEmail(string userId, string code)
  4.         {
  5.             if (userId == null || code == null)
  6.             {
  7.                 return View("Error");
  8.             }
  9.  
  10.             IdentityResult result = await UserManager.ConfirmEmailAsync(userId, code);
  11.  
  12.             if (result.Succeeded)
  13.             {
  14.                 return View("ConfirmEmail");
  15.             }
  16.             else
  17.             {
  18.                 AddErrors(result);
  19.                 return View();
  20.             }
  21.         }

The above code binds the ‘userId’ and ‘code’ parameters from the url and calls the method on the UserManager to confirm the email address.

9. Add the corresponding view ConfirmEmail.cshtml to display the messages

  1. <h2>@ViewBag.Title.</h2>
  2.  
  3. <div>
  4.  
  5.     <p>
  6.         Thank you for confirming your email. Please @Html.ActionLink("click here to log in", "Login", "Account", routeValues: null, htmlAttributes: new { id = "loginLink" })
  7.     </p>
  8. </div>

Setting up Two-Factor Authentication in the application

As of now we have set up the email client and added code to confirm the user email. Now we need to include additional action methods and views to complete the two-factor authentication.

In the existing web application, when a user tries to login using the Login page, the local account credentials or the OAuth credentials are validated and the user is logged in. We need to alter the flow to send the PIN via email and wait for the user to enter it.

1. The two-factor authentication providers have to be registered with the UserManager. This can be done in the ApplicationUserManager.Create method. The Identity framework ships with two built in two-factor authentication providers namely EmailTokenProvider and SmsTokenProvider which generate the security PIN and validate it once it is received from the user. For our project we’ll use the EmailTokenProvider<T> provider.

2. We need to register this provider to the list of two-factor authentication providers in UserManager. This can be done in the ‘Create’ method as below

  1. publicstaticApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
  2.         {
  3.             var manager = newApplicationUserManager(newUserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
  4.             manager.PasswordValidator = newPasswordValidator
  5.             {
  6.                 RequiredLength = 6,
  7.                 RequireNonLetterOrDigit = false,
  8.                 RequireDigit = false,
  9.                 RequireLowercase = false,
  10.                 RequireUppercase = false,
  11.             };
  12.             manager.RegisterTwoFactorProvider("EmailCode", newEmailTokenProvider<ApplicationUser>()
  13.             {
  14.                 Subject = "SecurityCode",
  15.                 BodyFormat = "Your security code is {0}"
  16.             });
  17.  
  18.             manager.EmailService = newEmailService();
  19.  
  20.             var dataProtectionProvider = options.DataProtectionProvider;
  21.             if (dataProtectionProvider != null)
  22.             {
  23.                 manager.UserTokenProvider = newDataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
  24.             }
  25.             return manager;
  26.         }

The ‘Subject’ and ‘BodyFormat’ properties format the email message as needed so you can edit them as required.

3. In the Login post action, instead of logging in the local user when the local credentials are valid, we need to generate the secure PIN and send it in the email. The ‘GenerateTwoFactorTokenAsync’ method on the UserManager generates the code and calls the SendAsync method on the EmailService property to send the email with the secure PIN.

  1. var user = await UserManager.FindAsync(model.UserName, model.Password);
  2.                 if (user != null)
  3.                 {
  4.                     await UserManager.GenerateTwoFactorTokenAsync(user.Id, "EmailCode");
  5.                     ...

4. After sending the email, the application needs to set a cookie in the browser. This is an essential authentication step to prevent users who were not authenticated with valid local credentials from directly accessing the page. The userId is set as a claim so that it can be used to query the user object during PIN verification. Create a new private method to set the cookie

  1. privatevoid SetTwoFactorAuthCookie(string userId)
  2.         {
  3.             ClaimsIdentity identity = newClaimsIdentity(DefaultAuthenticationTypes.TwoFactorCookie);
  4.             identity.AddClaim(newClaim(ClaimTypes.NameIdentifier, userId));
  5.             AuthenticationManager.SignIn(identity);
  6.         }

Call this method from in the Login method after the call to send email. Redirect to a new action to wait for the user to enter the PIN

  1. var user = await UserManager.FindAsync(model.UserName, model.Password);
  2.                 if (user != null)
  3.                 {
  4.                     await UserManager.GenerateTwoFactorTokenAsync(user.Id, "EmailCode");
  5.                     SetTwoFactorAuthCookie(user.Id);
  6.                     return RedirectToAction("VerifyCode");
  7.                 }

5. ASP.NET Identity uses OWIN middleware for cookie-based authentication. We need to configure the OWIN cookie middleware to store a two-factor authentication cookie in the request. The cookie middleware in the application is configured during application start via the ConfigureAuth method in Startup.Auth. We can add our code here to configure the cookie middleware to use the two factor cookie for temporary authentication. Copy the below code in the Startup.Auth class

  1. app.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));

6. The code in step 3 and 4 changed the flow when a user logs in using their local credentials. If a user signs in using OAuth credentials like Google or Facebook and it is linked with a local account, we need to enable the two-factor authentication for them too. To do this for the existing application, we need to send the PIN to the email of the user once they are authenticated from the external provider.

In the AccountController, the ‘ExternalLoginCallback’ method is called after the user is authenticated by the OAuth provider. If there is an existing user with these credentials, then the user is signed in. To implement OAuth we need to replace the sign in action with one that sends the secure PIN and waits for the user to enter the PIN. To do this, replace the call to:

  1. await SignInAsync(user, false);
  2.                 return RedirectToLocal("Home");

with the one defined in step 4.

  1. var user = await UserManager.FindAsync(loginInfo.Login);
  2.             if (user != null)
  3.             {
  4.                 await UserManager.GenerateTwoFactorTokenAsync(user.Id, "EmailCode");
  5.                 SetTwoFactorAuthCookie(user.Id);
  6.                 return RedirectToAction("VerifyCode");
  7.             }

This implements two-factor authentication when user logs in with their OAuth credentials too.

7. Add a method to retrieve the UserId from the two factor authentication cookie set on successful login by local user.

  1. privateasyncTask<string> GetTwoFactorUserIdAsync()
  2.         {
  3.             var result = await AuthenticationManager.AuthenticateAsync(DefaultAuthenticationTypes.TwoFactorCookie);
  4.  
  5.             if (result != null && result.Identity != null && !String.IsNullOrEmpty(result.Identity.GetUserId()))
  6.             {
  7.  
  8.                 return result.Identity.GetUserId();
  9.             }
  10.             returnnull;
  11.         }

Create a new action method ‘VerifyCode’ to wait for the PIN from the user. In the get action for this method, verify the userId from the cookie. This way we can validate that the user logged in using valid credentials and was then redirected to the current page. If the cookie is not found, redirect to the Login page for the user to login.

  1. [AllowAnonymous]
  2.         publicasyncTask<ActionResult> VerifyCode()
  3.         {
  4.             if (String.IsNullOrEmpty(await GetTwoFactorUserIdAsync()))
  5.             {
  6.                 return RedirectToAction("Login");
  7.             }
  8.             return View();
  9.         }

8. Add a new cshtml to get the PIN from the user. For convenience, no view model is added to bind the PIN to it. We directly read it from the form element.

  1. @using (Html.BeginForm("VerifyCode", "Account", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
  2. {
  3.  
  4.     <h3>Enter the PIN sent to the email</h3>
  5.  
  6.     <divclass="form-group">
  7.  
  8.         @Html.Label("pinLabel", new { @class = "col-md-2 control-label" })
  9.  
  10.         <divclass="col-md-10">
  11.  
  12.             @Html.TextBox("pin", "", new { @class = "form-control" })
  13.  
  14.         </div>
  15.  
  16.     </div>
  17.  
  18.     <divclass="form-group">
  19.  
  20.         <divclass="col-md-offset-2 col-md-10">
  21.  
  22.             <inputtype="submit"class="btn btn-default"value="Submit"/>
  23.  
  24.         </div>
  25.  
  26.     </div>
  27.  
  28. }

9. We need to validate the PIN entered by the user. This is the final method in the flow to validate the PIN submitted by the user. Add a new method ‘VerifyCode’ with HttpPost attribute which is called when the above form is posted. Here we validate if the userId is present in the cookie, which shows that this is a valid request. Then we call Verify on the UserManager to see if this is a valid PIN for this user. If it is, then we sign in to the application; otherwise we report back the error.

  1. [HttpPost]
  2.         [AllowAnonymous]
  3.         publicasyncTask<ActionResult> VerifyCode(string pin)
  4.         {
  5.             string userId = await GetTwoFactorUserIdAsync();
  6.  
  7.             if (userId == null || String.IsNullOrEmpty(pin))
  8.             {
  9.  
  10.                 return View("Error");
  11.  
  12.             }
  13.             var user = await UserManager.FindByIdAsync(userId);
  14.  
  15.             if (await UserManager.VerifyTwoFactorTokenAsync(user.Id, "EmailCode", pin))
  16.             {
  17.  
  18.                 await SignInAsync(user, false);
  19.  
  20.                 return RedirectToLocal("Home");
  21.  
  22.             }
  23.             else
  24.             {
  25.                 ModelState.AddModelError("", "Invalid code");
  26.             }
  27.             return View();
  28.         }

A note on the two factor PIN generated

Identity uses an implementation of the RFC 6238 Time-based One-Time Password algorithm (TOTP) for generation of the PIN used for two factor authentication. The PIN generated is 6 digits in length and is valid for a period for 180 seconds (3 minutes).

Running the application

Run the application.

1. On the Register page, enter the username and email.

clip_image004

2. Clicking on Register redirects to a page where it says to check the email for a confirmation link.

clip_image006

3. Verify the email for the link. The link might be URL encoded. Use any URL decoder (available online) to decode the URL. Paste it in the browser.

clip_image007

4. Now click the link to go to the log in page. Enter the login credentials. Clicking Login redirects to the page where the application expects the PIN sent in the email.

clip_image008

5. Enter the PIN and hit Submit. The user is logged into the application.

Code

With 2.0.0-beta1 release we are shipping a sample application as a Nuget package (Microsoft.Aspnet.Identity.Samples) which should be installed in an empty ASP.NET web application. This application has the code which implements all features of the 2.0.0-beta1 release and can be used as a template when updating your applications or to try out the new features.

This sample NuGet package also shows how to use SMS for two-factor authentication and other 2.0 features which are not covered in this article. The sample application created using this article is available at link.

Summary

This post shows how you can add user email confirmation and two-factor authentication via email using ASP.NET Identity to an application that was using ASP.NET Identity 1.0. In 2.0 we shipped more features such as Password Reset and two-factor authentication using SMS. All these features can be implemented by following the same flow or by looking at the Microsoft.AspNet.Identity.Samples package.

I hope you have found this walkthrough useful. If you have any questions around ASP.NET Identity or find issues, please feel free to open bugs on the Identity Codeplex site, https://aspnetidentity.codeplex.com/ or ask questions on asp.net/forums or StackOverflow (tag: aspnet-identity). I can also be reached on twitter (@suhasbjoshi).

Thanks to Tom Dykstra and Pranav Rastogi for reviewing the article


  • How can I implement two factor authentication using ASP.NET Identity but with (hardware) token devices without using email?

    Does ASP.NET support this functionality?

  • @Carlos Pineda: ASP.NET Identity does support the functionality. We are working on the sample and will be up shortly.

  • There is no logging of the number of failed logins and the last login time.

    The predecessor system had this.

    There isn't much to help prevent brute force attacks

  • @John Middleton: Thank you for the feedback. The article was written using the 2.0.0-beta1 bits. The 2.0.0 RTM version has the logging failed attempts and prevention against brute force attacks. The last login time can be added on the profile property on the user class

  • @suhabsj Yes ive downloaded the RTM version and the beta samples, I am very impressed!

    Any more guidance on using hardware tokens?

Page 1 of 1 (5 items)