The two-step verification is a process where a user enters credentials, and after successful password validation,  receives an OTP (one-time-password) via email or SMS. Then, they enter that OTP in the Two-Step Verification form on our site to log in successfully.

We just have to make one thing clear before we start. Even though many people think that this type of verification is based on something you know (the password) and something you have (device to access Email or SMS), that’s not the case. The ownership of the device is not part of the verification process, on the other hand, the OTP is. Therefore, the OTP is still something we know and it raises some security concerns over email or SMS. But it is still more secure than a one-time password process.

So, in this article, we are going to learn how to implement a two-step verification process in our project by using ASP.NET Core Identity.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
To download the source code for this project, visit the Two-Step Verification with ASP.NET Core Identity repository.

To navigate through the entire series, visit the ASP.NET Core Identity series page.

Code Preparation for Two-Step Verification Process

Before we continue, we have to make sure that our user has an email confirmed and a two factor enabled. If we check our only user in the database, we are going to see this is the case:

email confirmed two factor enabled - two-factor verification

If these columns are not set to true (1), you can set them manually.

Now, we can modify the Login action:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(UserLoginModel userModel, string returnUrl = null)
{
    //code removed for clarity reasons
    var result = await _signInManager.PasswordSignInAsync(userModel.Email, userModel.Password, userModel.RememberMe, lockoutOnFailure: true);
    if (result.Succeeded)
    {
        return RedirectToLocal(returnUrl);
    }

    if(result.RequiresTwoFactor)
    {
        return RedirectToAction(nameof(LoginTwoStep), new { userModel.Email, userModel.RememberMe, returnUrl});
    }
    //code removed for clarity reasons
}

So, one of the properties that our result variable contains is RequiresTwoFactor. The PasswordSignInAsync method will set that property to true if the TwoFactorEnabled column for the current user is set to true, and the Succeeded property will be set to false. Therefore, we check if the RequiresTwoFactor property is true and if it is, we redirect a user to a different action with the email, rememberMe and returnUrl parameters.

It is important to mention that as soon as a user gets redirected to the LoginTwoStep action, the new Identity.TwoFactorUserId cookie will be created in our browser. This cookie contains important data about the current user.

Before we create additional actions, let’s create a new model class for the LoginTwoStep action:

public class TwoStepModel
{
    [Required]
    [DataType(DataType.Text)]
    public string TwoFactorCode { get; set; }
    public bool RememberMe { get; set; }
}

Now, we can add two required actions in the Account controller:

[HttpGet]
public async Task<IActionResult> LoginTwoStep(string email, bool rememberMe, string returnUrl = null)
{
    return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginTwoStep(TwoStepModel twoStepModel, string returnUrl = null)
{
    return View();
}

Excellent.

We have prepared everything for the two-step verification process. So, let’s implement it.

Implementation of the Two-Step Verification Process

It’s time to modify the GET LoginTwoStep action:

[HttpGet]
public async Task<IActionResult> LoginTwoStep(string email, bool rememberMe, string returnUrl = null)
{
    var user = await _userManager.FindByEmailAsync(email);
    if (user == null)
    {
        return View(nameof(Error));
    }

    var providers = await _userManager.GetValidTwoFactorProvidersAsync(user);
    if (!providers.Contains("Email"))
    {
        return View(nameof(Error));
    }

    var token = await _userManager.GenerateTwoFactorTokenAsync(user, "Email");

    var message = new Message(new string[] { email }, "Authentication token", token, null);
    await _emailSender.SendEmailAsync(message);

    ViewData["ReturnUrl"] = returnUrl;
    return View();
}

We check if the current user exists in the database. If that’s not the case, we display the error page. But if we find a user, we have to check is there a provider for Email because we want to send our two-step code using an email message. After that check, we just create a token with the GenerateTwoFactorTokenAsync method and send an email message.

Now, let’s just create a view for this action:

@model IdentityByExamples.Models.TwoStepModel

<h1>LoginTwoStep</h1>
<hr />

<p> Please enter your authentication code in the field below</p>

<div class="row">
    <div class="col-md-4">
        <form asp-action="LoginTwoStep" asp-route-returnUrl ="@ViewData["ReturnUrl"]">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="TwoFactorCode" class="control-label"></label>
                <input asp-for="TwoFactorCode" class="form-control" />
                <span asp-validation-for="TwoFactorCode" class="text-danger"></span>
            </div>
            <input asp-for="RememberMe" type="hidden" />
            <div class="form-group">
                <input type="submit" value="Log in" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @await Html.PartialAsync("_ValidationScriptsPartial")
}

This process is quite familiar.

LoginTwoStep POST Implementation

Finally, let’s modify the POST LoginTwoStep action:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> LoginTwoStep(TwoStepModel twoStepModel, string returnUrl = null)
{
    if (!ModelState.IsValid)
    {
        return View(twoStepModel);
    }

    var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
    if(user == null)
    {
        return RedirectToAction(nameof(Error));
    }

    var result = await _signInManager.TwoFactorSignInAsync("Email", twoStepModel.TwoFactorCode, twoStepModel.RememberMe, rememberClient: false);
    if(result.Succeeded)
    {
        return RedirectToLocal(returnUrl);
    }
    else if(result.IsLockedOut)
    {
        //Same logic as in the Login action
        ModelState.AddModelError("", "The account is locked out");
        return View();
    }
    else
    {
        ModelState.AddModelError("", "Invalid Login Attempt");
        return View();
    }
}

So, first, we check the model validity. If it’s valid, we use the GetTwoFactorAuthenticationUserAsync method to get the current user. We do that with the help of our Identity.TwoFactorUserId cookie, created in the first part of this article. This will prove to us that the user indeed went through all the verification steps to get to this point. If we find that user, we use the TwoFactorSignInAsync method to verify the TwoFactorToken value and sign in the user.

If the result is successful, we use the returnUrl parameter to redirect the user. Otherwise, we apply additional checks on the result variable and force appropriate actions.

We don’t want to repeat the same code – that’s why you see the comment in the code sample. But it would be a good practice to extract the code from the Lockout part in the Login action in its own method and then just call that method in the Login and LoginTwoStep actions.

Testing the Entire Process

With everything in place, we can test our functionality.

As soon as we enter valid credentials, we are going to see a new view:

two step view - two-step verification

Then if we check our email:

two-step verification email code

We can see a new token.

Finally, after we enter that token in the input field, we are going to be redirected either to the home view or the protected view. This depends on what we tried to access without authentication:

two step token success

Take notice that the amr property (Authentication Method Reference) now has the mfa value which stands for Multiple-factor Authentication.

Conclusion

So, in this article, we’ve learned how to use a two-step verification process to authenticate a user by using an email provider. But, we don’t have to use only email provider. SMS is also an option that can be used for the process. The same logic applies to the SMS provider.

In the next article, we are going to learn about external accounts and how to handle external identity providers.

So, stay tuned.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!