Two-Factor Authentication in ASP.NET Core Identity

Two-Factor Authentication in ASP.NET Core Identity

In this article, I will explain How to Implement Two-Factor Authentication in ASP.NET Core Identity. Please read our previous article discussing Adding and Verifying a Phone Number in ASP.NET Core Identity by sending an SMS with a verification code to the phone number.

Two-Factor Authentication in ASP.NET Core Identity

Two-Factor Authentication (2FA) in ASP.NET Core Identity is a security process in which users provide two different authentication factors to verify themselves. This process is done to protect better both the user’s credentials and the resources the user can access. 2FA adds an additional layer of security to the authentication process, making it harder for attackers to access a person’s devices or online accounts because knowing the victim’s password alone is insufficient to pass the authentication check.

Two-Factor Authentication (2FA) requires users to provide two forms of identification before gaining access to their account. The first factor is usually something the user knows, like a password, while the second factor is something the user has, such as a mobile number or email address that can receive a text message.

The two factors in 2FA typically include:

  • Something You Know: This is something only the user would know, like a password, PIN, or pattern.
  • Something You Have: This is something only the user would have, like a smartphone with an OTP (One-Time Password) app, a bank card, or a hardware token.

In some cases, a third factor, “Something You Are,” involves biometrics like fingerprints, retina scans, or voice recognition. This is more common in advanced security systems and is part of what’s known as Multi-Factor Authentication (MFA), which can include two or more factors for authentication.

The idea is that even if one factor (like a password) is compromised, an unauthorized user would still need the second factor to gain access, significantly reducing the chance of unauthorized access.

Two-Factor Authentication Flow:

Let us proceed and understand how the Two-Factor Authentication (2FA) works in ASP.NET Core Identity. As shown in the image below, we need to provide the option to enable and disable two-factor authentication (2FA). If not enabled, it will show Enable Two-Factor Authentication; if enabled, it will show Disable Two-Factor Authentication.

Note: Two-Factor Authentication can only be enabled when the user confirms his Email and Mobile number or either one of them as per your business requirements.

Once the user is logged in and if Two-Factor Authentication (2FA) is not enabled, the user will see the Enable 2FA link as shown in the image below.

Two-Factor Authentication Flow

Once the user clicks on the Enable 2FA link, the system will send a verification token to the user’s registered Phone number and Email ID. Then it will render the following page asking the user to enter the verification code received via Email or Phone number.

Two-Factor Authentication Flow

Once you enter the verification token and click on the Submit button, two-factor authentication must be enabled, and if you verify the database, then you will see the

Two-Factor Authentication in ASP.NET Core Identity

With two-factor authentication enabled, now log in using your credentials as follows:

Two-Factor Authentication in ASP.NET Core Identity

Once you click on the Login button, then it will not log in to the application; rather, it will redirect you to the Two Factor Authentication Page, as shown in the image below. Here, you need to enter the verification token received via Email or Phone number.

How to Implement Two-Factor Authentication in ASP.NET Core Identity

Once you enter the token and click on the Verify button, it will check the token and then login into the application.

Enabling and Disabling Two-Factor Authentication in ASP.NET Core Identity:

Modifying _Layout.cshtml File:

First, modify the _Layout.cshtml view as follows. Here, we are adding a Two-Factor Authentication link. If the user has already enabled the 2FA, it will display Disable 2FA, and if two-factor authentication is not enabled, it will display Enable 2FA. Here, we inject the UserManager service and call the GetUserAsync method to get the user details.

@using Microsoft.AspNetCore.Identity
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@{
    var userDetail = await UserManager.GetUserAsync(User);
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - ASPNETCoreIdentityDemo</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/ASPNETCoreIdentityDemo.styles.css" asp-append-version="true" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">

                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
                        </li>

                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="SecureMethod">Secure</a>
                        </li>

                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="NonSecureMethod">Non Secure</a>
                        </li>

                        @if (SignInManager.IsSignedIn(User) && User.IsInRole("Admin"))
                        {
                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" role="button"
                                   data-bs-toggle="dropdown" aria-expanded="false">
                                    Manage
                                </a>
                                <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
                                    <a class="dropdown-item" asp-controller="Administration"
                                       asp-action="ListUsers">Users</a>
                                    <a class="dropdown-item" asp-controller="Administration"
                                       asp-action="ListRoles">Roles</a>
                                </ul>
                            </li>
                        }
                    </ul>
                    <ul class="navbar-nav ml-auto">
                        @*If the user is signed-in display Logout link*@
                        @if (SignInManager.IsSignedIn(User))
                        {
                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMyAccountLink" role="button"
                                   data-bs-toggle="dropdown" aria-expanded="false">
                                    My Account
                                </a>
                                <ul class="dropdown-menu" aria-labelledby="navbarDropdownMyAccountLink">
                                    <a class="dropdown-item" asp-controller="Account"
                                       asp-action="ChangePassword">Set Password</a>
                                    <a class="dropdown-item" asp-controller="Account"
                                       asp-action="UpdateProfile">Update Profile</a>
                                    <a class="dropdown-item" asp-controller="Account"
                                       asp-action="ConfirmPhoneNumber">Confirm Phone Number</a>

                                    @if (userDetail.TwoFactorEnabled)
                                    {
                                        <a class="dropdown-item" asp-controller="Account"
                                           asp-action="ManageTwoFactorAuthentication">Disable 2FA</a>
                                    }
                                    else
                                    {
                                        <a class="dropdown-item" asp-controller="Account"
                                           asp-action="ManageTwoFactorAuthentication">Enable 2FA</a>
                                    }

                                </ul>
                            </li>

                            <li class="nav-item">
                                <form method="post" asp-controller="account" asp-action="logout">
                                    <button type="submit" style="width:auto"
                                            class="nav-link btn btn-link py-0">
                                        Logout @User?.Identity?.Name
                                    </button>
                                </form>
                            </li>
                        }
                        else
                        {
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-controller="account" asp-action="register">
                                    Register
                                </a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark" asp-controller="account" asp-action="login">
                                    Login
                                </a>
                            </li>
                        }
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2023 - ASPNETCoreIdentityDemo - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/jquery-validation/dist/jquery.validate.min.js"></script>
    <script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)

</body>
</html>
Adding ManageTwoFactorAuthentication Get Action Method:

Next, add the ManageTwoFactorAuthentication Get Action Method within the Account Controller. This action method will check whether the user has confirmed the Phone Number and Email ID. If both are confirmed, it will send an OTP to the registered phone number and email ID and render a view where the user needs to enter the OTP.

[Authorize]
[HttpGet]
public async Task<IActionResult> ManageTwoFactorAuthentication()
{
    var user = await userManager.GetUserAsync(User);
    if (user == null)
    {
        return NotFound($"Unable to load user with ID '{userManager.GetUserId(User)}'.");
    }

    //First, we need to check whether the User Phone and Email is confirmed or not
    if (!user.PhoneNumberConfirmed && !user.EmailConfirmed)
    {
        ViewBag.ErrorTitle = "You cannot Enable/Disable Two Factor Authentication";
        ViewBag.ErrorMessage = "Your Phone Number and Email Not Yet Confirmed";
        return View("Error");
    }

    string Message;
    if (user.TwoFactorEnabled)
    {
        Message = "Disable 2FA";
    }
    else
    {
        Message = "Enable 2FA";
    }

    //Generate the Two Factor Authentication Token
    var TwoFactorToken = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider);

    //Send the Token to user Mobile Number and Email Id

    //Sending SMS
    var result = await smsSender.SendSmsAsync(user.PhoneNumber, $"Your Token to {Message} is {TwoFactorToken}");

    //Sending Email
    await emailSender.SendEmailAsync(user.Email, Message, $"Your Token to {Message} is {TwoFactorToken}", false);

    return View(); // View for the user to enter the token
}
Adding ManageTwoFactorAuthentication View

Next, add a view named ManageTwoFactorAuthentication.cshtml within the Views/Account folder and then copy and paste the following code. This is the view that is going to render a text box accepting the OTP received via SMS and Phone number.

@{
    ViewData["Title"] = "Manage Two Factor Authentication";
}

<h1>Manage Two Factor Authentication</h1>

<hr />
<div class="row">
    <div class="col-md-6">
        <form method="post" asp-action="ManageTwoFactorAuthentication" asp-controller="Account">
            <div class="form-group">
                <label>Enter the Verification Token Send to Your Phone Number and Email</label>
                <input type="text" name="Token" id="Token" class="form-control" />
            </div>

            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
    </div>
</div>
Adding ManageTwoFactorAuthentication Post Action Method:

Next, add the ManageTwoFactorAuthentication Post Action Method within the Account Controller. This method will verify the token and then enable or disable the Two-Factor Authentication (2FA). The following code is self-explained, so please go through the comment line for a better understanding.

[Authorize]
[HttpPost]
public async Task<IActionResult> ManageTwoFactorAuthentication(string Token)
{
    var user = await userManager.GetUserAsync(User);
    if (user == null)
    {
        return NotFound($"Unable to load user with ID '{userManager.GetUserId(User)}'.");
    }

    var result = await userManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider, Token);
    if (result)
    {
        // Token is valid
        if (user.TwoFactorEnabled)
        {
            user.TwoFactorEnabled = false;
            ViewBag.Message = "You have Sucessfully Disabled Two Factor Authentication";
        }
        else
        {
            user.TwoFactorEnabled = true;
            ViewBag.Message = "You have Sucessfully Enabled Two Factor Authentication";
        }

        await userManager.UpdateAsync(user);

        // Redirect to success page 
        return View("TwoFactorAuthenticationSuccessful");
    }
    else
    {
        // Handle invalid token
        ViewBag.ErrorTitle = "Unable to Enable/Disable Two Factor Authentication";
        ViewBag.ErrorMessage = "Either the Token is Expired or you entered some wrong information";
        return View("Error");
    }
}
Adding TwoFactorAuthenticationSuccessful View

Next, add a view named TwoFactorAuthenticationSuccessful.cshtml within the Views/Account folder and then copy and paste the following code.

@{
    ViewData["Title"] = "TwoFactorAuthenticationSuccessful";
}

<h1>@ViewBag.Message </h1>

As of now, we have discussed how to enable and disable Two-factor authentication in ASP.NET Core Identity. Now, let us proceed and understand how to handle Two-factor authentication when a user login.

Handling 2FA at the time of Login in ASP.NET Core Identity

Implementing Two-Factor Authentication (2FA) at the time of login in an ASP.NET Core application involves modifying the login process to include an additional step for users with 2FA enabled. Let us proceed and see how we can implement this.

Modify Post Login Action to Check for 2FA.

First, within the login post-action method of the account controller, we need to check if the user has 2FA enabled. If they do, redirect them to a separate view for 2FA verification.

[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model, string? ReturnUrl)
{
    //If Model Login Failed, we also need to show the External Login Providers 
    model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

    if (ModelState.IsValid)
    {
        //First Fetch the User Details by Email Id
        var user = await userManager.FindByEmailAsync(model.Email);

        //Then Check if User Exists, EmailConfirmed and Password Is Valid
        //CheckPasswordAsync: Returns a flag indicating whether the given password is valid for the specified user.
        if (user != null && !user.EmailConfirmed &&
                    (await userManager.CheckPasswordAsync(user, model.Password)))
        {
            ModelState.AddModelError(string.Empty, "Email not confirmed yet");
            return View(model);
        }

        // The last boolean parameter lockoutOnFailure indicates if the account should be locked on failed login attempt. 
        // On every failed login attempt AccessFailedCount column value in AspNetUsers table is incremented by 1. 
        // When the AccessFailedCount reaches the configured MaxFailedAccessAttempts which in our case is 5,
        // the account will be locked and LockoutEnd column is populated.
        // After the account is lockedout, even if we provide the correct username and password,
        // PasswordSignInAsync() method returns Lockedout result and
        // the login will not be allowed for the duration the account is locked.
        var result = await signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: true);

        if (result.Succeeded)
        {
            // Handle successful login
            // Check if the ReturnUrl is not null and is a local URL
            if (!string.IsNullOrEmpty(ReturnUrl) && Url.IsLocalUrl(ReturnUrl))
            {
                return Redirect(ReturnUrl);
            }
            else
            {
                // Redirect to default page
                return RedirectToAction("Index", "Home");
            }
        }
        if (result.RequiresTwoFactor)
        {
            // Handle two-factor authentication case
            // Generate a 2FA token, send that token to user Email and Phone Number
            // and redirect to the 2FA verification view
            var TwoFactorAuthenticationToken = await userManager.GenerateTwoFactorTokenAsync(user, "Email");

            //Sending SMS
            await smsSender.SendSmsAsync(user.PhoneNumber, $"Your 2FA Token is {TwoFactorAuthenticationToken}");

            //Sending Email
            await emailSender.SendEmailAsync(user.Email, "2FA Token", $"Your 2FA Token is {TwoFactorAuthenticationToken}", false);

            return RedirectToAction("VerifyTwoFactorToken", "Account", new { model.Email, ReturnUrl, model.RememberMe, TwoFactorAuthenticationToken });
        }
        if (result.IsLockedOut)
        {
            //It's important to inform users when their account is locked.
            //This can be done through the UI or by sending an email notification.
            await SendAccountLockedEmail(model.Email);
            return View("AccountLocked");
        }
        else
        {
            // Handle failure
            // Get the number of attempts left
            var attemptsLeft = userManager.Options.Lockout.MaxFailedAccessAttempts - await userManager.GetAccessFailedCountAsync(user);

            ModelState.AddModelError(string.Empty, $"Invalid Login Attempt. Remaining Attempts : {attemptsLeft}");
            return View(model);
        }
    }

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

Create a class named VerifyTwoFactorTokenViewModel.cs and then copy and paste the following code. This class is going to hold the data required for Two Factor Authentication.

using System.ComponentModel.DataAnnotations;
namespace ASPNETCoreIdentityDemo.Models
{
    public class VerifyTwoFactorTokenViewModel
    {
        public string? Email { get; set; }
        public string? ReturnUrl { get; set; }
        public bool RememberMe { get; set; }
        public string? Token { get; set; }
        [Required]
        [Display(Name = "Two Factor Code")]
        public string? TwoFactorCode { get; set; }
    }
}
Creating VerifyTwoFactorToken Get Action Method:

Create an action method to handle the 2FA verification. You will need a view where the user can input their 2FA code. So, add the following VerifyTwoFactorToken get action method within the Account Controller.

[HttpGet]
[AllowAnonymous]
public IActionResult VerifyTwoFactorToken(string Email, string ReturnUrl, bool RememberMe, string TwoFactorAuthenticationToken)
{
    VerifyTwoFactorTokenViewModel model = new VerifyTwoFactorTokenViewModel()
    {
        RememberMe = RememberMe,
        Email = Email,
        ReturnUrl = ReturnUrl,
        Token = TwoFactorAuthenticationToken
    };

    return View(model);
}
Create a View for 2FA Verification.

Create a view named VerifyTwoFactorToken.cshtml within the Views/Account folder, then copy and paste the following code.

@model VerifyTwoFactorTokenViewModel

@{
    ViewData["Title"] = "VerifyTwoFactorToken";
}

<h1>Verify Two Factor Token</h1>

<hr />
<div class="row">
    <div class="col-md-6">
        <form method="post" asp-action="VerifyTwoFactorToken" asp-controller="Account">
            <input asp-for="Email" type="hidden" />
            <input asp-for="ReturnUrl" type="hidden" />
            <input asp-for="Token" type="hidden" />
            <input asp-for="RememberMe" type="hidden" />

            <div class="form-group">
                <label>Enter the Verification Token Send to Your Phone Number and Email</label>
                <label asp-for="TwoFactorCode"></label>
                <input asp-for="TwoFactorCode" class="form-control" />
                <span asp-validation-for="TwoFactorCode" class="text-danger"></span>
            </div>

            <button type="submit" class="btn btn-primary">Verify</button>
        </form>
    </div>
</div>
Creating VerifyTwoFactorToken Post Action Method:

Add an action in your account controller to handle the 2FA verification. So, add the following VerifyTwoFactorToken post-action method within the Account Controller.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> VerifyTwoFactorToken(VerifyTwoFactorTokenViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var user = await userManager.FindByEmailAsync(model.Email);
    if (user == null)
    {
        ModelState.AddModelError(string.Empty, "Invalid Login Attempt.");
        return View(model);
    }

    // Validate the 2FA token
    var result = await userManager.VerifyTwoFactorTokenAsync(user, "Email", model.TwoFactorCode);
    if (result)
    {
        // Sign in the user and redirect
        await signInManager.SignInAsync(user, isPersistent: model.RememberMe);

        // Check if the ReturnUrl is not null and is a local URL
        if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
        {
            return Redirect(model.ReturnUrl);
        }
        else
        {
            // Redirect to default page
            return RedirectToAction("Index", "Home");
        }
    }

    ModelState.AddModelError(string.Empty, "Invalid verification code.");
    return View(model);
}

Now, run the application and test it, and it should work as expected.

When should you use two-factor authentication in ASP.NET Core?

Two-Factor Authentication (2FA) is a critical security feature that adds an extra layer of protection beyond just a username and password. In ASP.NET Core applications, especially those that handle sensitive data or personal user information, implementing 2FA is highly recommended in various scenarios:

  • User Login: The most common use case for 2FA is during login. After the user enters their username and password, they are prompted to provide a second authentication factor, which could be a code sent via SMS, email, or generated by an authenticator app.
  • Sensitive Operations: For actions that involve high-risk operations, such as changing passwords, updating personal information, or performing financial transactions, 2FA can provide additional security. This ensures that even if a user’s password is compromised, unauthorized changes to critical account details or unauthorized transactions are still guarded against.
  • Accessing Restricted Areas: In applications where certain areas or functionalities are restricted to specific user roles (like admin panels), enabling 2FA for accessing these areas adds an extra layer of security.
  • Compliance Requirements: Certain industries have regulatory requirements that mandate the use of 2FA. For instance, applications dealing with healthcare data might need to comply with HIPAA, while financial applications might need to adhere to PCI DSS standards.
  • After Suspicious Activity Detection: If your application detects suspicious activity (like login attempts from unusual locations or devices), prompting the user for 2FA can help verify the access attempt’s legitimacy.
  • User Preference: Some applications offer 2FA as an optional security feature that users can enable if they wish to secure their accounts further.
  • During Password Recovery: Implementing 2FA during the password recovery process ensures that the account owner’s request for a password reset is genuine.
  • API Access: For applications that provide API access, especially those exposing sensitive data or functionality, enforcing 2FA can add a layer of security for API consumers.

In the next article, I will discuss How to Implement the Password Expiration Policy in ASP.NET Core Identity with an Example. In this article, I explain how to implement two-factor authentication in ASP.NET Core Identity. I hope you enjoy this article on implementing two-factor authentication in ASP.NET Core Identity.

Leave a Reply

Your email address will not be published. Required fields are marked *