(C# ASP.NET Core) Login with 2-Factor Authentication

Consider a user who has enabled 2-factor authentication as explained in the previous tutorial. Such a user is required to authenticate twice before he can access the pages of a website. The first authentication is through the common login page, and then immediately after that he is redirected to a second login page where he has to enter a validation code obtained from his Google, Microsoft or IOS authenticator app. The purpose of this tutorial is to complete the second login page - also called 2-factor login page.
(Rev. 30-Sep-2022)

Categories | About |     |  

Parveen,

Recall the sequence

Let us first have a re-cap of the common login page that we completed in the starting tutorials of this chapter.

Open the solution explorer and locate the file Login.cshtml.cs. Double click to open this file so that we can examine the code that we wrote earlier.

This is the Login.cshtml.cs file.

// Areas -> Auth -> Pages 
// Login.cshtml.cs 
// See the linked video for explanation 
using Microsoft.AspNetCore.Authorization;

using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.Mvc.RazorPages;

using System.ComponentModel.DataAnnotations;

namespace MyRazorApp.Areas.Auth.Pages
{

  [AllowAnonymous]
  public class LoginModel : PageModel
  {

    [BindProperty]
    [Required]
    [DataType(DataType.EmailAddress)]
    public String? UserID { get; set; }

    // password 
    [BindProperty]
    [Required]
    [DataType(DataType.Password)]
    public String? UserPassword { get; set; }

    // remember me 
    [BindProperty]
    public Boolean RememberMe { get; set; }
    private readonly SignInManager<IdentityUser> _sm;

    public LoginModel(SignInManager<IdentityUser> sm)
    {

      _sm = sm;

    }

    public async Task<IActionResult> OnPostAsync(string? returnUrl)
    {

      if (ModelState.IsValid)
      {

        var result = await _sm.PasswordSignInAsync(
            UserID, UserPassword, RememberMe, lockoutOnFailure: true);

        if (result.Succeeded)
        {

          return LocalRedirect(returnUrl ?? "~/");

        }

        else if (result.RequiresTwoFactor)
        {

          // ModelState.AddModelError( 
          // string.Empty, 
          // "2-factor authentication to be implemented later."); 

          return RedirectToPage("mfa/LoginWith2fa",
              new { area = "auth",
              rememberMe = RememberMe,
              returnUrl = returnUrl
            });

        }

        else if (result.IsLockedOut)
        {

          // to implement later 
          // throw new NotImplementedException(); 

          return RedirectToPage("Lockout", new { area = "auth" });

        }

        else 
        {

          // display the count of failed attempts 
          IdentityUser user = await _sm.UserManager.FindByEmailAsync(UserID);

          String message = "Login failed.";

          if (null != user)
          {

            int fail = await _sm.UserManager.GetAccessFailedCountAsync(user);

            int total = _sm.Options.Lockout.MaxFailedAccessAttempts;

            message += $" Unsuccessful attempts {fail} of {total}";

          }

          // add to validation summary 
          ModelState.AddModelError(string.Empty, message);

        }

      }

      return Page();

    }

  }

}

First we have the LoginModel class. Scrolling down, we can see that we added some bind properties.

Then we have the OnPost method that executes when the user posts the login form.

The PasswordSignInAsync method is used to perform the password verification. Then we have an if-else ladder to handle the various outcomes of this function. As you can see, we already had an else condition for 2-factor authentication.

Remove the placeholder code and add a redirect to the second login page. If you are following our ASP.NET Core course, then the source code can be found in the downloads attached to this video.

So let us now add this second login page.

Video Explanation (see it happen!)

Please watch the following youtube video:

Adding the 2-Factor Login Page

Open the solution explorer and locate the folder called Mfa under the Auth area. Right click to add a page called LoginWith2fa. Double-click to open the markup file so that we can have a look at the markup.

// Areas -> Auth -> Pages -> Mfa  
// LoginWith2fa.cshtml 

@page

@model MyRazorApp.Areas.Auth.Pages.Mfa.LoginWith2faModel

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<h1>2-Factor Login</h1>

<div style="color:red" asp-validation-summary="All"></div>

<form method="post">

  <input asp-for="RememberMe" type="hidden" />

  <input asp-for="ReturnUrl" type="hidden" />

  <table>

    <tr>

      <td>

        <label asp-for="TwoFactorCode"></label>

      </td>

      <td>

        <input asp-for="TwoFactorCode" />

      </td>

      <td>
      
        <span asp-validation-for="TwoFactorCode"></span>
      
      </td>

    </tr>

    <tr>

      <td>
      </td>
      
      <td>

        <input asp-for="RememberMe" />

        <label asp-for="RememberMe"></label>

      </td>

      <td>

        <span asp-validation-for="RememberMe"></span>

      </td>

    </tr>

    <tr>

      <td colspan="2" style="text-align:right">

        <input type="submit" value="login!" />

      </td>

    </tr>

  </table>

</form>


  

First we have the directives for page, model and addTagHelper. Then we have a tag for presenting a validation summary. After that we have a form. First we have hidden inputs for RememberMe and ReturnUrl.

After that we have an input textbox for 2-factor code. The user brings this code from his authenticator app.

Then we have an input for remember me, and lastly, we have the submit button. So, basically, this form collects the two factor code from the user.

Now let us have a look at the backing class.

Again, open the solution explorer and locate the backing class for the LoginWith2fa page. Double click and open this file.

// Areas -> Auth -> Pages -> Mfa 
// LoginWith2fa.cshtml.cs 
using Microsoft.AspNetCore.Identity;

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.Mvc.RazorPages;

using System.ComponentModel.DataAnnotations;

namespace MyRazorApp.Areas.Auth.Pages.Mfa
{

  public class LoginWith2faModel : PageModel
  {

    private readonly SignInManager<IdentityUser> _sm;

    public LoginWith2faModel(SignInManager<IdentityUser> sm)
    {

      _sm = sm;

    }


    // return url 
    [BindProperty]
    public string? ReturnUrl { get; set; }

    // remember me 
    [BindProperty]
    public bool RememberMe { get; set; }

    // 2-factor code 
    [BindProperty]
    [Required]
    public string TwoFactorCode { get; set; } = default!;

    // OnGet 
    public async Task<IActionResult> OnGetAsync(
          bool rememberMe, string? returnUrl = null)
    {

      // Ensure the user has gone through the 
      // first login page 
      var user = await _sm.GetTwoFactorAuthenticationUserAsync();

      if (user == null)
      {

        // error 
        return NotFound();

      }

      ReturnUrl = returnUrl;

      RememberMe = rememberMe;

      return Page();

    }

    // OnPost 
    public async Task<IActionResult> OnPostAsync()
    {

      if (!ModelState.IsValid)
      {

        return Page();

      }

      String returnUrl = ReturnUrl ?? Url.Content("~/");

      // Ensure the user has gone through the 
      // first login page 
      var user = await _sm.GetTwoFactorAuthenticationUserAsync();

      if (user == null)
      {

        // error 
        return NotFound();

      }

      var authCode = TwoFactorCode
        .Replace(" ", string.Empty)
        .Replace("-", string.Empty);

      var result = await _sm.TwoFactorAuthenticatorSignInAsync(authCode,
              RememberMe, RememberMe);

      if (result.Succeeded)
      {

        return LocalRedirect(returnUrl);

      }

      else if (result.IsLockedOut)
      {

        return RedirectToPage("./Lockout");

      }

      else 
      {

        ModelState.AddModelError(string.Empty, "Invalid auth code.");

        return Page();

      }

    }

  }

}

First we have the namespace directives. Then we have the class LoginWith2faModel.

Constructor based dependency injection has been used to cache SignInManager as a readonly member.

Then we have the properties for ReturnUrl and RememberMe. We also have a bind property for the two factor code.

The OnGet method executes when the page loads for the first time. The first thing we do here is to use the SignInManager to ensure that the user has passed the first login step.

Then the return url and remember me properties are set. These will bind to the hidden fields of the form and will be used in the OnPost method.

Next we have the OnPost method. This method is called when the user posts this login form. First we use the sign in manager to ensure that the user has passed the first login step. Then we extract the 2 factor code posted by the user. Spaces and hyphens are removed.

The TwoFactorAuthenticatorSignInAsync method of the sign in manager is used to perform the authentication. An if-else ladder is used to handle the various outcomes.

If the user succeeded, then he is redirected to the return url. And, if his account is locked out, then he is redirected to the Lockout page for further steps. Recall that we have already discussed and completed the account lockout process.

Run the Project

Let us now run the project and verify.

Run the app and open the home page. Click on the Click to View Restricted Page link and open the login form. Enter your login and password for the first step of the login process. Please ensure that you have already enabled two factor authentication for the account you use. Please follow the previous tutorial to learn more about enabling two factor authentication.

Click the submit button - you are redirected to the second login page now!

Open the authenticator app on your mobile phone, and enter the code that you see there. The code is valid for 30 seconds.

If everything goes fine you will be authenticated and redirected to the restricted page. This verifies that we have successfully implemented 2-factor authentication. Thanks!


This Blog Post/Article "(C# ASP.NET Core) Login with 2-Factor Authentication" by Parveen is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.