×

Two Factor Authentication with Email in ASP.NET Core

Introduction

We recently had a client who wanted to stand up a web application for their customers to access information in near real-time. The client currently pushes data the customer has interest in via PDF reports but with the new web application, the customers of the client would be able to pull real time information whenever they needed it. One of the first issues that came up while discussing this was how to secure the web application.  Most modern client facing web applications have some form of two factor authentication (2FA).  This provides an additional level of security to the traditional username and password.  This was first implemented using SMS or email but has evolved into a more secure implementation using a Time-based One-time Password Algorithm (TOTP) via an authenticator app like Microsoft Authenticator or Google Authenticator.

Issues with Authenticator Apps

While using TOTP provides a high level of security, there are some issues having your client base use the authenticator apps.  What happens when a user loses the device the authenticator app is installed on?  Well they are out of luck and not able to access your web application.  But they can just restore the backup codes that got generated when they added your web application to the authenticator app on a new device you say.  What percentage of a typical user base takes the time to save those backups codes?  I’m guessing it’s pretty small.  Also, what if the user base of the web application is primarily or partially elderly?  How many of those users even have a smart phone or if they do would be able to figure out how to setup and use an authenticator app?  What if the user wants to access the web application and doesn’t have their device handy?

I’m not writing this to say why you shouldn’t use an authenticator app.  I use them personally and they provide me with a high level of confidence that my data is protected.  However, when you are creating a web application for a client you do want to make an informed decision about how your user base is going to access your application. 

2FA with Email

For a variety of reasons, we decided against using an Authenticator app with this web application and settled on email.  We discussed SMS, and it is implemented much the same as email, but there is an additional cost for a SMS service, and it also requires the user have their phone with them when they access the web application.   So that left us with using email for 2FA.  When the user logs in using their username and password, the application will send them an email with an authentication code.  If the user correctly enters the authentication code, they will be able to access the web application.

Note that Microsoft and many others no longer recommend using SMS or email as a second factor because of the number of attack vectors that exists for this implementation.  It is up to you to consider what data you are trying to protect and how your user base will access the application.

Implementation

Microsoft ASP.NET Identity provides built in support for 2FA.  However, after .NET Core 1.1, official support for 2FA using SMS or email was removed due to security concerns.  With a small amount of custom coding it is possible to still implement using ASP.NET Core Identity.  Note the code examples in this article are for .NET Core 3.1.

If you haven’t already, you need to add ASP.NET Core Identity to your Visual Studio solution or select the option to add it when creating a new project.  For more information on ASP.NET Core Identity, see my blog post.  As a perquisite for getting started, you should have the following completed.

  1. Identity setup configured.
  2. Identity Razor pages scaffolded.
  3. The SQL Server Identity tables created via Entity Framework code first or TSQL script.

The first thing you’ll want to do is look at the Login page, which is by default located in the Identity area at /Identity/Account/Login.  Note that the Identity SignInManager returns an Identity SignInResult which has a property called “RequiresTwoFactor” that we can leverage.  That property gets its value from the “TwoFactorEnabled” column in the ApplicationUser table.  You will want to make sure when you are creating user accounts that you set this column to true or make its default value true in the database schema.  In the code below, we can see if “RequiresTwoFactor” we are redirected to a page “LoginVerifyCode” (Note that by default Identity scaffolds this page as “LoginWith2fa”).

// attempt to sign the user in
                   var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, true, lockoutOnFailure: true);
                    if (result.IsLockedOut)
                    {
                        _identityLogger.Log
                            .ForContext("UserName", Input.UserName)
                            .ForContext("RequestUri", Request.GetUri())
                            .Warning($"User {Input.UserName} account locked out.");
                        return RedirectToPage("./Lockout");
                    }
                    else if (result.IsNotAllowed)
                    {
                        // user not allowed to sign in because email address is not confirmed
                        ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                        return Page();
                    }
                    else if (result.RequiresTwoFactor)
                        // redirect to email verification code page
                        return RedirectToPage("LoginVerifyCode", new { id = Input.UserName, returnUrl = returnUrl });
                    else if (result.Succeeded)
                    {

So now that the user has been redirected to the verify code page, the first thing we want to do in the Get is to get a reference to the ApplicationUser by calling the FindByNameAsync in the UserManager class.  This ensures the username we passed from the login page is indeed a valid user and we need the reference to the user to generate a 2FA token anyway.

The next step is to call GenerateTwoFactorTokenAsync in the UserManager class which will generate a unique 6 digit code for the user that is trying to login.  The method takes two parameters, a reference to the ApplicationUser and the provider for the token (SMS or Email).

Once you have the token, you will include that in an email you send to the user.  I have created a custom class to send the verification code via the email implementation I have selected for this application.  (The details of the class implementation are beyond the scope of this article, but it’s a simple SmtpClient implementation).

// make sure the user email is valid
                var user = await _userManager.FindByNameAsync(id);
                if (user == null)
                    return RedirectToPage("/Error/StatusCode", new {code = "403"});

                Input = new InputModel
                {
                    Email = user.Email,
                    UserName = user.UserName,
                    ReturnUrl = returnUrl
                };

                // generate the 2fa token
                var token = await _userManager.GenerateTwoFactorTokenAsync(user, "Email");

                // send the user the 2fa token via email
                var emailResult = await _userManagerService.SendVerificationCodeEmail(user.Email, token, _emailerService, _emailLogger);
                if (!string.IsNullOrEmpty(emailResult))
                {
                    AssignStatusMessage("There was an error sending the verification code email.", StatusMessageTypeEnum.Warning);
                    AssignStatusMessageTempData();
                }

So now the user should have received the email with the 2FA token.  They enter the verification code and click the submit button on the verification page, and we will need to verify that code in the Post.  To do so, we leverage the TwoFactorSignInAsync method in the SignInManager class.  This method takes four parameters.

  1. The Provider which sent the token. In this case it’s Email.
  2. The token the user received via email and typed in.
  3. A flag indicating if the sign in cookie should persist after the browser closes. I would recommend setting this to false (especially on a shared computer).
  4. A flag indicating if the current browser should be remembered. More on that later.

The TwoFactorSignInAsync method returns a SignInResult that we can use to determine if the token is valid.  If it returns “Succeeded”, we are good to let the user proceed into the application.  If not, display an error message and ask the user if they would like to have the token sent again.

if (ModelState.IsValid)
                {
                    try
                    {
                        var result = await _signInManager.TwoFactorSignInAsync("Email", Input.EmailCode, false, Input.RememberComputer);
                        if (result.Succeeded)
                        {
                            _identityLogger.Log
                                .ForContext("UserName", Input.UserName)
                                .ForContext("RequestUri", Request.GetUri())
                                .Information($"User {Input.UserName} has verified their 2FA email code.");

                            return Redirect(Input.ReturnUrl ?? "/");
                        }
                    }
                    catch (Exception ex)
                    {

As an additional convenience to the user, you can choose to have the 2FA sign in be remembered on the user’s browser.  This is certainly a security concern that bears some thought.  While it’s a convenience for the user not to have to enter their 2FA code every time they login, there are scenarios where you would never want to allow this (think a shared public computer).  As you can see in the code above, we pass in the value of an input control presented to the user that lets them decide to be remembered on the browser.  If value from that control is passed as true to the TwoFactorSignInAsync method for the rememberClient property, the 2FA will be persisted via a cookie.

Conclusion

2FA should be a requirement for any public facing web application you develop.  You do have options in how you implement 2FA so take into consideration your security needs and your user base when making that decision.

Further Reading

Two-factor authentication using SMS and email with ASP.NET Identity
Two-factor authentication with SMS in ASP.NET Core
Multi-factor authentication in ASP.NET Core

How Can We Help You?

We are experts in developing custom solutions for web, mobile, and desktop applications. From security and scalability to performance and UX , we've got you covered. Talk to us today and see what we can do!

John HadzimaCore Contributor

John Hadzima is a Solutions Architect and Team Lead at Marathon Consulting. A graduate of Old Dominion University with a BSBA in Information Systems, he has been working as a developer in Hampton Roads for over 20 years with a focus on delivering solutions using the Microsoft Stack.  When not coding, John enjoys surfing, skiing, biking, golfing, travel, and spending time with his two boys.