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.

What is Two-Factor Authentication (2FA)?

Two-Factor Authentication (2FA) is an additional layer of security for online accounts beyond the traditional username and password. Instead of relying on just a username and password, 2FA requires users to provide two forms of verification to confirm their identity:

  • Something You Know: This is typically the Password or PIN. This is the first factor.
  • Something You Have: This could be a code sent to your phone via SMS or an Email address that can receive a text message. This is the second factor.

This method is a part of multi-factor authentication, which ensures that a user is granted access only after successfully presenting two or more pieces of evidence (or factors) to an authentication mechanism. 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.

How Does 2FA Work in Web Applications?

The typical 2FA process in a web application involves the following steps:

  • User Login Attempt: The user enters their username and password (first factor: something they know).
  • Verification of Credentials or First Factor: The application verifies the entered credentials against stored data.
  • Triggering Second Factor: If the first factor (password) is verified, the user is prompted to provide the second factor (something they have). This could be a code sent via SMS or email or generated through a dedicated app like Google Authenticator.
  • Verification of Second Factor: The user enters the code, and the application verifies it. If the second factor is correct, the user is granted access. If the second factor is incorrect or not provided, access is denied.
How to Implement Two-Factor Authentication in ASP.NET Core Identity:

To use Two-Factor Authentication (2FA), the user must first enable it. Secondly, 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. Before understanding how to Implement Two-Factor Authentication (2FA) with ASP.NET Core Identity, let us first understand the flow of Two-Factor Authentication (2FA) with our application.

So, once the user is logged in, we need to provide the user with the option to Enable and Disable Two-Factor Authentication (2FA), as shown in the image below. If not enabled, it will show the Enable Two-Factor Authentication button; if enabled, it will show the Disable Two-Factor Authentication button.

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

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

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

Once you enter the verification token and click on the Submit button, two-factor authentication will be enabled for the user if the token is valid, and you will see the following message:

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

The Email you will receive will look like the one below:

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

You can also verify the database. As you can see, the TwoFactorEnabled column of the AspNetUsers table is set to 1 for the given user.

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

Now, if you check the My Account dropdown list, you will see the Disable 2FA link, as shown in the image below. If you want to disable 2FA for your account, follow the same steps.

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

Login Flow with 2FA:

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

Two-Factor Authentication in ASP.NET Core Identity

With two-factor authentication enabled, once you click the Login button, it will not login you into the application. It will do the first-factor authentication, i.e., checking the username and password in the database. If the credentials are valid, it will send the two-factor authentication code to the user’s confirmed phone number and email ID. Then it will redirect you to the following page, asking you to enter the authentication token.

Two-Factor Authentication in ASP.NET Core Identity

Here, you need to enter the verification token received via Email or Phone number. Once you enter the token and click on the Verify button, it will check the token (verify the second factor), and if the second factor is valid, then it will login you into the application. The email you will receive with the token will look like the one below:

Two-Factor Authentication in ASP.NET Core Identity

Let us proceed and implement this step by step in our application.

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

Let us first understand how to provide the option to enable and disable 2FA.

Modifying _Layout.cshtml File:

First, modify the _Layout.cshtml view as follows. Here, we add a Two-Factor Authentication link to the My Account dropdown list. 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.

@using Microsoft.AspNetCore.Identity
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

<!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" />
</head>
<body class="d-flex flex-column min-vh-100">
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-light shadow-sm">
            <div class="container-fluid">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">
                    <img src="~/images/logo.png" alt="Logo" style="height:40px;">
                </a>
                <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
                        aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarNav">
                    <ul class="navbar-nav me-auto">
                        <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") || User.IsInRole("SuperAdmin")))
                        {
                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" href="#" id="manageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                    Manage
                                </a>
                                <ul class="dropdown-menu" aria-labelledby="manageDropdown">
                                    <li><a class="dropdown-item" asp-controller="Administration" asp-action="ListUsers">Users</a></li>
                                    <li><a class="dropdown-item" asp-controller="Administration" asp-action="ListRoles">Roles</a></li>
                                </ul>
                            </li>
                        }
                    </ul>
                    <ul class="navbar-nav">
                        @if (SignInManager.IsSignedIn(User))
                        {
                            // Fetch the current user
                            var currentUser = await UserManager.GetUserAsync(User);

                            // Check if the user has a password
                            bool hasPassword = await UserManager.HasPasswordAsync(currentUser);

                            // Check if 2FA is enabled
                            bool is2FAEnabled = await UserManager.GetTwoFactorEnabledAsync(currentUser);

                            <!-- Display username -->
                            <li class="nav-item">
                                <span class="nav-link">Hello, @User.Identity.Name!</span>
                            </li>

                            <li class="nav-item dropdown">
                                <a class="nav-link dropdown-toggle" href="#" id="accountDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
                                    My Account
                                </a>

                                <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="accountDropdown">
                                    @{
                                        if (!hasPassword)
                                        {
                                            // Show "Set Password" link if no local password is set
                                            <li>
                                                <a class="dropdown-item" asp-controller="Account" asp-action="SetPassword">Set Password</a>
                                            </li>
                                        }
                                        else
                                        {
                                            // Show "Change Password" link if a local password is set
                                            <li>
                                                <a class="dropdown-item" asp-controller="Account" asp-action="ChangePassword">Change Password</a>
                                            </li>
                                        }

                                        // Show 2FA link based on the user's 2FA status
                                        if (is2FAEnabled)
                                        {
                                            // Show "Disable 2FA" when 2FA is enabled
                                            <li>
                                                <a class="dropdown-item" asp-controller="Account" asp-action="ManageTwoFactorAuthentication">Disable 2FA</a>
                                            </li>
                                        }
                                        else
                                        {
                                            // Show "Enable 2FA" when 2FA is disabled
                                            <li>
                                                <a class="dropdown-item" asp-controller="Account" asp-action="ManageTwoFactorAuthentication">Enable 2FA</a>
                                            </li>
                                        }
                                    }
                                    <li>
                                        <a class="dropdown-item" asp-controller="Account"
                                           asp-action="ConfirmPhoneNumber">Verify Phone Number</a>
                                    </li>
                                    <li>
                                        <form method="post" asp-controller="Account" asp-action="Logout" style="display:inline;">
                                            <button type="submit" class="dropdown-item">Sign Out</button>
                                        </form>
                                    </li>
                                </ul>
                            </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 flex-grow-1">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="bg-light text-center text-lg-start border-top py-3 mt-auto">
        <div class="container">
            <p class="mb-0 text-muted">
                &copy; 2024 ASPNETCoreIdentityDemo -
                <a asp-area="" asp-controller="Home" asp-action="Privacy" class="text-decoration-none">Privacy</a>
            </p>
        </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/dist/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 following 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 anyone is confirmed, it will send an OTP to the registered phone number and Email ID and render a view where the user must enter the OTP.

[Authorize]
[HttpGet]
public async Task<IActionResult> ManageTwoFactorAuthentication()
{
    var user = await userManager.GetUserAsync(User);
    if (user == null)
    {
        return RedirectToAction("Login", "Account");
    }

    // First, ensure the user's email and phone number are confirmed
    if (!user.PhoneNumberConfirmed || !user.EmailConfirmed)
    {
        ViewBag.ErrorTitle = "Two-Factor Authentication Setup Error";
        ViewBag.ErrorMessage = "You cannot enable or disable Two-Factor Authentication because your phone number or email is not yet confirmed.";
        return View("Error");
    }

    // Determine the action (Enable or Disable 2FA)
    string actionMessage = user.TwoFactorEnabled ? "Disable Two-Factor Authentication" : "Enable Two-Factor Authentication";

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

    // Send the token via SMS if the phone number is confirmed
    if (!string.IsNullOrEmpty(user.PhoneNumber) && user.PhoneNumberConfirmed)
    {
        string smsMessage = $"Your security code to {actionMessage.ToLower()} is: {twoFactorToken}. Please do not share this code with anyone.";
        await smsSender.SendSmsAsync(user.PhoneNumber, smsMessage);
    }

    // Send the token via Email if the email is confirmed
    if (!string.IsNullOrEmpty(user.Email) && user.EmailConfirmed)
    {
        string emailSubject = $"{actionMessage} - Security Code";
        string emailBody = $@"
                    <p>Hello {user.FirstName} {user.LastName},</p>
                    <p>We received a request to {actionMessage.ToLower()} for your account.</p>
                    <p>Your security code is: <strong>{twoFactorToken}</strong></p>
                    <p>If you did not request this, please ignore this message. Otherwise, use the code to complete your action.</p>
                    <p>Thank you,<br/>Your Dot Net Tutorials Team</p>";

        await emailSender.SendEmailAsync(user.Email, emailSubject, emailBody, IsBodyHtml: true);
    }

    // Notify the user that the token has been sent
    return View(); // View for the user to enter the token
}
Creating Two Factor Token View Model:

Next, create a class file named TwoFactorTokenViewModel.cs within the Models folder and copy and paste the following code. This model will take the token from the user to enable and disable Two-Factor Authentication.

using System.ComponentModel.DataAnnotations;
namespace ASPNETCoreIdentityDemo.Models
{
    public class TwoFactorTokenViewModel
    {
        [Required(ErrorMessage = "Security code is required.")]
        [Display(Name = "Security Code")]
        public string TwoFactorToken { get; set; }
    }
}
Adding Manage Two-Factor Authentication View

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

@model ASPNETCoreIdentityDemo.Models.TwoFactorTokenViewModel

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

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <!-- Page Header -->
            <div class="text-center mb-4">
                <h1 class="display-6">Two-Factor Authentication</h1>
                <p class="text-muted">
                    Secure your account with an additional layer of protection. Enter the verification code sent to your registered phone number and email address.
                </p>
            </div>

            <!-- Card for Verification -->
            <div class="card shadow-sm">
                <div class="card-body">
                    <h5 class="card-title text-center">Verification Required</h5>
                    <p class="text-center text-muted">
                        Please enter the 6-digit verification code to proceed.
                    </p>

                    <!-- Display Success/Error Message -->
                    @if (TempData["ErrorMessage"] != null)
                    {
                        <div class="alert alert-danger text-center">
                            @TempData["ErrorMessage"]
                        </div>
                    }

                    <!-- Verification Form -->
                    <form method="post" asp-action="ManageTwoFactorAuthentication" asp-controller="Account">
                        <div class="mb-3">
                            <label asp-for="TwoFactorToken" class="form-label"></label>
                            <input asp-for="TwoFactorToken" class="form-control" placeholder="Enter your code" maxlength="6" />
                            <span asp-validation-for="TwoFactorToken" class="text-danger"></span>
                        </div>

                        <!-- Submit Button -->
                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary btn-lg">
                                Verify
                            </button>
                        </div>
                    </form>
                </div>
            </div>

            <!-- Footer Note -->
            <div class="text-center mt-4">
                <p class="text-muted">
                    Didn't receive the code? <a asp-action="ManageTwoFactorAuthentication" asp-controller="Account" class="text-primary">Resend Verification Code</a>
                </p>
            </div>
        </div>
    </div>
</div>
Adding ManageTwoFactorAuthentication Post Action Method:

Next, add the following ManageTwoFactorAuthentication Post Action Method within the Account Controller. Based on the current status, this method will verify the token and enable or disable the Two-Factor Authentication (2FA). That means if the current status is enabled, then it will disable 2FA, and if the current status is disabled, then it will enable 2FA. The following code is self-explained, so please read the comment line for a better understanding.

[Authorize]
[HttpPost]
public async Task<IActionResult> ManageTwoFactorAuthentication(TwoFactorTokenViewModel model)
{
    // Validate the model
    if (!ModelState.IsValid)
    {
        TempData["ErrorMessage"] = "Please enter a valid security code.";
        return View(model);
    }

    // Fetch the user
    var user = await userManager.GetUserAsync(User);
    if (user == null)
    {
        TempData["ErrorMessage"] = "User session expired. Please log in again.";
        return RedirectToAction("Login", "Account");
    }

    // Verify the token
    var isTokenValid = await userManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider, model.TwoFactorToken);
    if (isTokenValid)
    {
        // Toggle Two-Factor Authentication status
        user.TwoFactorEnabled = !user.TwoFactorEnabled;
        await userManager.UpdateAsync(user);

        // Set a success message
        TempData["SuccessMessage"] = user.TwoFactorEnabled
            ? "Two-Factor Authentication has been successfully enabled for your account."
            : "Two-Factor Authentication has been successfully disabled for your account.";

        return RedirectToAction("TwoFactorAuthenticationSuccessful");
    }

    // Handle invalid token
    TempData["ErrorMessage"] = "The security code is invalid or has expired. Please try again.";
    return View(model);
}
Adding TwoFactorAuthenticationSuccessful Action Method:

Please add the following TwoFactorAuthenticationSuccessful Action method within the Account Controller. This action method will render the Two Factor Authentication Successful view.

[Authorize]
[HttpGet]
public IActionResult TwoFactorAuthenticationSuccessful()
{
    return View();
}
Adding TwoFactorAuthenticationSuccessful View

Next, add a view named TwoFactorAuthenticationSuccessful.cshtml within the Views/Account folder and copy and paste the following code. This view will render a message to the user indicating whether the Two-Factor Authentication is enabled or disabled.

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

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-8 text-center">
            <h1 class="display-6 text-success">Success!</h1>
            <p class="text-muted">
                @TempData["SuccessMessage"]
            </p>
            <a href="/" class="btn btn-primary btn-lg mt-3">Go to Home</a>
        </div>
    </div>
</div>

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 logs in.

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

Handling 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, we need to check if the user has 2FA enabled within the login Post-action method of the Account controller. If they do, redirect them to a separate view for 2FA verification. So, please modify the Login Post action method of the Account Controller as follows. If 2FA is enabled, we send an OPT to the user’s confirmed Email and Phone number.

[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
    if (ModelState.IsValid)
    {
        // Find user by email
        var user = await userManager.FindByEmailAsync(model.Email);
        if (user == null)
        {
            // Generic error message to avoid disclosing details
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            return View(model);
        }

        // Check password first to avoid leaking information about email confirmation
        var passwordCheck = await userManager.CheckPasswordAsync(user, model.Password);
        if (!passwordCheck)
        {
            // Generic error message for invalid credentials
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            return View(model);
        }

        // Password is valid, now check email confirmation
        if (!user.EmailConfirmed)
        {
            ModelState.AddModelError(string.Empty, "Your email address is not confirmed. Please confirm your email to log in.");
            model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
            return View(model);
        }

        // Now SignIn the User
        var result = await signInManager.PasswordSignInAsync(user, model.Password, model.RememberMe, lockoutOnFailure: true);

        if (result.Succeeded)
        {
            // Redirect user after successful sign-in
            if (!string.IsNullOrEmpty(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
            {
                return Redirect(model.ReturnUrl);
            }

            return RedirectToAction(nameof(HomeController.Index), "Home");
        }
        else if (result.RequiresTwoFactor)
        {
            // Handle two-factor authentication

            // Generate a 2FA token either using DefaultPhoneProvider or DefaultEmailProvider
            // Which provider we use here, same we need to use while doing the verification
            var TwoFactorAuthenticationToken = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);
            //var TwoFactorAuthenticationToken3 = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider);

            // Send SMS if the phone number is confirmed
            if (!string.IsNullOrEmpty(user.PhoneNumber) && user.PhoneNumberConfirmed)
            {
                var smsMessage = $"Your Two-Factor Authentication code is: {TwoFactorAuthenticationToken}. Please use this code to log in.";
                await smsSender.SendSmsAsync(user.PhoneNumber, smsMessage);
            }

            // Send Email if the email is confirmed
            if (!string.IsNullOrEmpty(user.Email) && user.EmailConfirmed)
            {
                var emailSubject = "Two-Factor Authentication Code";
                var emailBody = $@"
                            <p>Hello {user.FirstName} {user.LastName},</p>
                            <p>Your Two-Factor Authentication code is: <strong>{TwoFactorAuthenticationToken}</strong></p>
                            <p>If you did not request this code, please contact our support team immediately.</p>
                            <p>Thank you,<br/>Your Dot Net Tutorials Team</p>";
                await emailSender.SendEmailAsync(user.Email, emailSubject, emailBody, IsBodyHtml: true);
            }

            // Redirect to Two-Factor Authentication verification page with data
            return RedirectToAction("VerifyTwoFactorToken", "Account", new { model.Email, model.ReturnUrl, model.RememberMe });
        }
        else if (result.IsLockedOut)
        {
            // Handle lockout scenario
            ModelState.AddModelError(string.Empty, "Your account is locked. Please try again later.");
        }
        else
        {
            // Generic error message for invalid credentials
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
        }
    }

    // If we got this far, something failed; redisplay the form
    model.ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
    return View(model);
}
Creating the Verify Two-Factor Token View Model

Next, create a class file named VerifyTwoFactorTokenViewModel.cs within the Models folder and copy and paste the following code. This model will hold the data required for Two Factor Authentication.

using System.ComponentModel.DataAnnotations;
namespace ASPNETCoreIdentityDemo.Models
{
    public class VerifyTwoFactorTokenViewModel
    {
        [Required]
        public string Email { get; set; }
        public string? ReturnUrl { get; set; }
        public bool RememberMe { get; set; }
        [Required(ErrorMessage = "Security code is required.")]
        [Display(Name = "Two-Factor Code")]
        [DataType(DataType.Text)]
        [StringLength(6, MinimumLength = 6, ErrorMessage = "The security code must be 6 digits.")]
        public string TwoFactorCode { get; set; }
    }
}
Creating VerifyTwoFactorToken Get Action Method:

Next, we need to create an action method to handle the 2FA verification. So, please add the following VerifyTwoFactorToken GET action method within the Account Controller. This action method will render a page where the user will enter the OTP received via Email or SMS.

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

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

Next, create a view named VerifyTwoFactorToken.cshtml within the Views/Account folder and copy and paste the following code. This view will display one text box and ask the user to enter the OTP received by Email and SMS for Two-Factor Authentication verification.

@model VerifyTwoFactorTokenViewModel

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

<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <!-- Page Header -->
            <div class="text-center mb-4">
                <h1 class="display-6">Two-Factor Authentication</h1>
                <p class="text-muted">
                    For your security, we’ve sent a verification code to your registered email and phone number.
                    Enter the code below to complete the login process.
                </p>
            </div>

            <!-- Card for Verification Form -->
            <div class="card shadow-sm">
                <div class="card-body">
                    <h5 class="card-title text-center">Verify Your Code</h5>

                    <!-- Validation Summary -->
                    <div class="mb-3">
                        @if (!ViewData.ModelState.IsValid)
                        {
                            <div asp-validation-summary="All" class="alert alert-danger" role="alert" style="display:none;"></div>
                        }
                    </div>

                    <!-- Verification Form -->
                    <form method="post" asp-action="VerifyTwoFactorToken" asp-controller="Account">
                        @* Hidden Fields *@
                        <input asp-for="Email" type="hidden" />
                        <input asp-for="ReturnUrl" type="hidden" />
                        <input asp-for="RememberMe" type="hidden" />

                        <div class="mb-3">
                            <input asp-for="TwoFactorCode" class="form-control" placeholder="Enter your verification code" />
                            <span asp-validation-for="TwoFactorCode" class="text-danger"></span>
                        </div>

                        <div class="d-grid">
                            <button type="submit" class="btn btn-primary btn-lg">Verify</button>
                        </div>
                    </form>
                </div>
            </div>

            <!-- Footer Note -->
            <div class="text-center mt-4">
                <p class="text-muted">
                    Didn't receive the code?
                    <a asp-action="ResendTwoFactorToken" asp-controller="Account" asp-route-Email="@Model.Email" asp-route-ReturnUrl="@Model.ReturnUrl" asp-route-RememberMe="@Model.RememberMe" class="text-primary">Resend Verification Code</a>
                </p>
            </div>
        </div>
    </div>
</div>
Creating VerifyTwoFactorToken Post Action Method:

Next, we need to add another post-action method to our Account controller to handle the 2FA verification. So, add the following VerifyTwoFactorToken Post action method within the Account Controller. This action will take the authentication token (i.e., OTP) from the user and then validate the token. If validation is successful, then it will sign-in the user and redirect to the Return URL or Home page.

[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 using the same provider which is used to create the Token
    var result = await userManager.VerifyTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider, 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);
}
Creating ResendTwoFactorToken Post Action Method:

Next, add the following ResendTwoFactorToken action method to the account controller. This method will Resend the Two Factor Authentication token.

[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ResendTwoFactorToken(string Email, string ReturnUrl, bool RememberMe)
{
    var user = await userManager.FindByEmailAsync(Email);
    if (user == null)
    {
        return RedirectToAction("Login", "Account");
    }

    // Generate a 2FA token either using DefaultPhoneProvider or DefaultEmailProvider
    // Which provider we use here, same we need to use while doing the verification
    var TwoFactorAuthenticationToken = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultEmailProvider);
    //var TwoFactorAuthenticationToken3 = await userManager.GenerateTwoFactorTokenAsync(user, TokenOptions.DefaultPhoneProvider);

    // Send SMS if the phone number is confirmed
    if (!string.IsNullOrEmpty(user.PhoneNumber) && user.PhoneNumberConfirmed)
    {
        var smsMessage = $"Your Two-Factor Authentication code is: {TwoFactorAuthenticationToken}. Please use this code to log in.";
        await smsSender.SendSmsAsync(user.PhoneNumber, smsMessage);
    }

    // Send Email if the email is confirmed
    if (!string.IsNullOrEmpty(user.Email) && user.EmailConfirmed)
    {
        var emailSubject = "Two-Factor Authentication Code";
        var emailBody = $@"
                            <p>Hello {user.FirstName} {user.LastName},</p>
                            <p>Your Two-Factor Authentication code is: <strong>{TwoFactorAuthenticationToken}</strong></p>
                            <p>If you did not request this code, please contact our support team immediately.</p>
                            <p>Thank you,<br/>Your Dot Net Tutorials Team</p>";
        await emailSender.SendEmailAsync(user.Email, emailSubject, emailBody, IsBodyHtml: true);
    }

    // Redirect to Two-Factor Authentication verification page with data
    return RedirectToAction("VerifyTwoFactorToken", "Account", new { Email, ReturnUrl, RememberMe });
}

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

With 2FA, we can significantly improve the security of our ASP.NET Core MVC application. By integrating ASP.NET Core Identity and enabling Two-Factor Authentication, we can ensure that user accounts are better protected from unauthorized access.

In the next article, I will discuss How to Implement Account Lockout 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 *