Two Steps Authentication in Asp.net Core Example

Two-step authentication is nowadays common requirement for any web application development, as a developer we must make sure that we allow right user to access data, to do that authentication more secure after first authentication we again produce a OTP (one time password) and send to user by SMS or in email.

Here is the graphical representation of how the multi factor authentication will look like.

two step authentication in asp.net core

Multi factor authentication often termed as MFA, indicates that authentication can have multiple layer to conform user credentials, Two-step Authentication is the part of multifactor authentication, here in example we demonstrate how to perform two step authentication in asp.net core using Identity framework service.

Multi Factor Authentication in Asp.net Core

If you are not familiar with Identity framework, please read my earlier post about how to configure identity service in asp.net core

In this example, we will create an asp.net core razor page application to demonstrate two-step authentication, then same example can be applied on Asp.net core MVC application also.

Here are the steps
  1. Start Visual Studio, create an Asp.net core Razor Page application
  2. Install following assemblies using Nugget package manager
    Microsoft.AspNetCore.Identity
    Microsoft.AspNetCore.Identity.EntityFrameworkCore
    Microsoft.EntityFrameworkCore.Design
    Microsoft.EntityFrameworkCore.SqlServer
    
  3. Create login page with razor form and models in attached code behind file.
    @page
    @model TwoStepsAuthentication.Pages.loginModel
    <h1>Login page</h1>
     
    <div>@Model.ActionMessage</div>
    <form method="post">
        Username:<input asp-for="Username" /> <br />
        Password:<input asp-for="Password" /> <br />
        <input type="submit" value="Login" />
    </form>
    

    We can login with either email or with username, though the actual login method accept username and user object as first parameter, here is how we call the method PasswordSignInAsync.

    var result =  _signInManager.PasswordSignInAsync(UserName, Password, false, false);                   
    Microsoft.AspNetCore.Identity.SignInResult _result = result.Result;
    

    The above example was logging with username and password

    Below is the example of how we can login with email and find the user object.

    var appUser =  _userManager.FindByEmailAsync(Email);
    AppUser _user = appUser.Result;
    var result =  _signInManager.PasswordSignInAsync(_user, Password, false, false);                   
    Microsoft.AspNetCore.Identity.SignInResult _result = result.Result;
    if (_result.Succeeded){ }
    

    on post method write the following logic.

    public async IActionResult OnPost()
    {          
        try
        {
            var appUser = await _userManager.FindByEmailAsync(Username);
            if (appUser != null)
            {
                /*signout and signin with new user*/
                await _signInManager.SignOutAsync();
                var _result = await _signInManager.PasswordSignInAsync(appUser, Password, false, false);
                Microsoft.AspNetCore.Identity.SignInResult result = _result;
                /*if successfull signin, go for next authentication*/
                if (result.Succeeded)
                {
                    if (appUser.TwoFactorEnabled)
                    {
                        // Force TFA page and send OTP using email or SMS
                        return Redirect(string.Format("tfaAuth?username=" + Username));
                    }
                    else
                    {
                        // allow user without TFA
                       return Redirect("controlpanel");
                    }
                                
                }
                else
                    ActionMessage = "Login failed";
            }
            else
                ActionMessage = "No user found!";
        }
        catch (Exception ex)
        {
            ActionMessage = ex.ToString();
        }    
        return Page();      
    }
    

    Here i have removed SMS or email sending code, just to keep this post more focused on two steps authentication, in case you want to know more about SMS and email sending, please refer following posts.

    Two Factor Authentication Check

    Here is the key logic for checking if two factor authentication is enabled for that particular user, if yes, then only we should perform two factor authentication check by sending OTP using SMS or Email, otherwise let the user log in without TFA

    Microsoft.AspNetCore.Identity.SignInResult result = await _signInManager.PasswordSignInAsync(appUser, Password, false, false);
    /*if successfull signin, go for next authentication*/
    if (result.Succeeded)
    {
        if (appUser.TwoFactorEnabled)
        {
            // Force to TFA page and send OTP using email or SMS
            return Redirect(string.Format("tfaAuth?username=" + Username));
        }
        else
              return  Redirect("controlpanel");
    }
    

    As you can see in above code the user property indicate if TwoFactorEnabled is true appUser.TwoFactorEnabled (database table "AspNetUsers", field name "TwoFactorEnabled"), ideally user should have that option to enable or disable two factor authentication as per their choice, but depending on application nature or criticality you also can make the TwoFactorEnabled true for all the user by default.

  4. Create another page to confirm OTP, here "secondauth" (you can give any name you want.)

    This page also will have a form, which will allow user to enter their OTP (one time password) and then submit, if OTP is confirmed then user will be redirected to control panel page, otherwise show the failure error message.

    <h1>Confirm OTP</h1>
    <div>@Model.ActionMessage</div>
    <form method="post">
        OTP : <input asp-for="OTP" /> <br /> 
        <input type="submit" value="Confirm OTP" />
    </form>
    

    On load of this page the OTP will be sent to User email and SMS (if configured), but we should also provide an additional button to send OTP again, in case OTP was not delivered in first attempt.

    public async void OnGet()
    {
    AppUser appUser = await _userManager.FindByEmailAsync(_userName);
    var _twoFactorEnabled = await _userManager.GetTwoFactorEnabledAsync(appUser);
    var tokenEmail = await _userManager.GenerateTwoFactorTokenAsync(appUser, _userManager.Options.Tokens.EmailConfirmationTokenProvider);       
    string messageString = string.Format("Authentication token: {0}", token);
    }
    

    In above code we are generating a token, which is long encrypted string value, this type of value we can send in email with and url and query string, we also can generate a 6 digit number and send in email or SMS , then ask user to type the same number in two step interface we provide in application.

    // This is how you can generate six digits random code
    int getSixDigitCode()
    {
        Random _random = new Random();
        int _aftCode = _random.Next(100001, 999999);
        Session["aftcode"] = _aftCode;
        return _aftCode;
    }
    

    Now the question is where we should store the value, so when user enter the value either by clicking url or typing in interface , we need to compare if the value is correct!

    There can be multiple options like storing in database, in cookie-encrypted format, or in session, Session would be the most secure and easy way to deal with the situation, if you wish learn how to use session in Asp.Net Core application.

    Now before you send two-step authentication code either by email or by SMS, we should check if the user has confirmed email id and confirmed mobile number, below code will help to confirm both credentials.

    if (appUser.EmailConfirmed)
    {
        // Send OTP in email
    }
    if (appUser.PhoneNumberConfirmed)
    {
        // Send OTP in SMS
    }
    

    When user submit the OTP, we need to check if the OTP is correct.

    public  IActionResult OnPost()
    {
    var appUser = await _signInManager.GetTwoFactorAuthenticationUserAsync();
    var authToken = await _userManager.GetAuthenticatorKeyAsync(appUser);
    bool isVerified = await _userManager.VerifyTwoFactorTokenAsync(appUser, "AspNetAuthenticator", authToken);
    return Page();
    }
    

    the method _userManager.GetAuthenticatorKeyAsync(appUser) checks if user record is present in "AspNetUserTokens" table.

  5. On successful confirmation, redirect user to secure control panel

    Now in control panel you can display user data, also let user know if their two factor authentication is enabled, if not, ask them to set that on!

    Here are few lines of code in your razor page will allow you to do the same.

    @using Microsoft.AspNetCore.Authorization
    @using Microsoft.AspNetCore.Identity
    @inject SignInManager&llt;IdentityUser> _signInManager
    @inject UserManager&llt;IdentityUser> _userManager
    @inject IAuthorizationService _authorizationService
    <h1>control panel</h1>
    @if (_signInManager.IsSignedIn(User))
    {
        @if ((_authorizationService.AuthorizeAsync(User, "TwoFactorEnabled")).Result.Succeeded)
        {
            <div>
                You have successfully logged in with two factor authentication.
            </div>
        }
        else
        {
            <div>
                You have NOT Enabled two factor authentication yet.
            </div>
        }
    }
    else
    {
        <div>
            Invalid !
        </div>
    }
    
Create user for testing authentication

Now to test above scenario, you may need one user to login with, if you don;t have any user in your identity database, then create a new page, and in code behind post method write following code.

Create a new user for two factor authentication where EmailConfirmed=true; and TwoFactor property is enabled user.TwoFactorEnabled = true;

public void OnPost()
    {
        // create a new user
        AppUser user = new AppUser();
            user.UserName = Username;
            user.Email = EmailAddress;
            user.EmailConfirmed = true;
            user.PhoneNumber = PhoneNumber;
            user.PhoneNumberConfirmed = true;
            user.TwoFactorEnabled = true;
        _userManager.CreateAsync(user,Password);
        ActionMessage = $"New user created with username {Username}";
    }

The above "CreateAsync" method will not throw any error message, even if the user is not created due to some error, so instead of writing the above code, try using following property to find out error and check if the user created successfully.

var _result= _userManager.CreateAsync(user, Password);
if (_result.Result.Succeeded)
    ActionMessage = $"New user created with username {Username}";
else
    foreach (IdentityError e in _result.Result.Errors)
    {
        str.Append(e.Description);
    }
    ActionMessage = str.ToString();
Startup Configuration Details

So far, we have seen the flow of how to implement two-factor authentication, but here is another important things you need to know how to register all services in startup.cs file.

public void ConfigureServices(IServiceCollection services)
{
        services.AddDbContext<AppIdentityDbContext>
            (options => options.UseSqlServer(DbConnection.ConnectionString2));


    services.AddIdentity<AppUser, IdentityRole>(
      options => {
	      options.SignIn.RequireConfirmedAccount = true;
      }
    )
    .AddEntityFrameworkStores<AppIdentityDbContext>()
    .AddTokenProvider<DataProtectorTokenProvider<AppUser>>
    (TokenOptions.DefaultProvider);


        services.Configure<IdentityOptions>(options =>
        {
            options.Lockout.MaxFailedAccessAttempts = 3;
            options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
        });


        #region Email and SMS(keep either one) 
        services.AddSingleton<IEmailSender, EmailSender>();
        services.AddSingleton<ISMSSender, TwilioSMSSender>();
        services.AddSingleton<ISMSSender, AspSMSSender>();
        services.Configure<SMSoptions>(Configuration);
        #endregion
        services.AddScoped<IUserClaimsPrincipalFactory<AppUser>,
            AdditionalUserClaimsPrincipalFactory>();
        services.AddAuthorization(options => 
            options.AddPolicy("TwoFactorEnabled", 
            x => x.RequireClaim("amr", "mfa")));
        services.AddRazorPages();
}

In above startup configuration code, you need to understand following things

  • How to add identity user AppUser class
  • Register email sender and SMS sender class.
  • Add policy with name TwoFactorEnabled
    services.AddIdentity<AppUser, IdentityRole>(
      options => {
    	  options.SignIn.RequireConfirmedAccount = true;
      }
    )
    .AddEntityFrameworkStores<AppIdentityDbContext>()
    .AddTokenProvider<DataProtectorTokenProvider<AppUser>>
    (TokenOptions.DefaultProvider);
    
    
    services.AddAuthorization(options => 
        options.AddPolicy("TwoFactorEnabled", 
        x => x.RequireClaim("amr", "mfa")));
    
  • Another very useful feature to lockout user for sometime on multiple failure attempt, identity framework provide a ready to use mechanism to achieve the same.

    services.Configure<IdentityOptions>(options =>
    {
        options.Lockout.MaxFailedAccessAttempts = 3;
        options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
    });
    
You may be interested to read following posts
Asp.Net Core C# Examples | Join Asp.Net MVC Course