DEV Community

Cover image for Reduce The 2FA'S Token Provider Length in ASP.NET Core Identity To 4 Digits Instead Of 6 Digits in .NET 7
Mohammed Ahmed Hussien
Mohammed Ahmed Hussien

Posted on

Reduce The 2FA'S Token Provider Length in ASP.NET Core Identity To 4 Digits Instead Of 6 Digits in .NET 7

In this new post I will show you how to work with two factor authentication and how to reduce the token provider length from 6 digits to 4 digits.
In the first step we have to create our sample application from scratch by creating a new Empty ASP.NET Core Application from visual Studio 2022 and using .NET 7.

First of all, to follow with me, please download the complete code form my GitHub repo from AspNetCoreIdentity-IUserTwoFactorTokenProvider

I'm not going here to explain how to create a new user in ASP.NET Core Identity but I will guide you how to accomplish the two-factor authentication step after success login from the user.
And ensure you have one user at least in your Back Store with enabled TowFactorEnabled property to true, see the pic below:

Image description

In the root of the application create a new folder named Services and inside this folder create a new class and interface as you see in the following code snippet (I will explain later...)

namespace AspNetCoreIdentity.Services
{
    public interface ITokenStoreService
    {
        bool StoreToken(string key, string token);
        bool RemoveToken(string key);
        string GetToken(string key);
    }
}
Enter fullscreen mode Exit fullscreen mode

And here the implementation of this interface like below:

using System.Collections.Concurrent;

namespace AspNetCoreIdentity.Services
{
    public class TokenStoreService : ITokenStoreService
    {
        private readonly ConcurrentDictionary<string, string> _tokenProviderStore = new ConcurrentDictionary<string, string>();

        public bool RemoveToken(string key)
        {
            var result = _tokenProviderStore.TryRemove(key, out _);
            return result; 
        }

        public bool StoreToken(string key, string token)
        {
            if (!_tokenProviderStore.ContainsKey(key))
            {
                var result = _tokenProviderStore.TryAdd(key, token);
                return result;
            }
            return false;
        }

        public string GetToken(string key)
        {
            var result = _tokenProviderStore.TryGetValue(key, out string token );
            if(result)
                return token;
            return null;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Make sure to register TokenStoreService.cs class at Program.cs file like below:

builder.Services.AddSingleton<ITokenStoreService, TokenStoreService>();
Enter fullscreen mode Exit fullscreen mode

This class will help us to store the generated token provider in the ConcurrentDictionary

In the root of the application create a new folder named Helpers and inside this folder create a new class named CustomTwoFactorTokenProvider.cs this class has the following signature:

using AspNetCoreIdentity.Models;
using AspNetCoreIdentity.Services;
using Microsoft.AspNetCore.Identity;
using System;
using System.Threading.Tasks;

namespace AspNetCoreIdentity.Helpers
{
    public class CustomTwoFactorTokenProvider 
    : IUserTwoFactorTokenProvider<ApplicationUser>
    {
        private readonly ITokenStoreService _tokenStoreService;
        public CustomTwoFactorTokenProvider(ITokenStoreService tokenStoreService)
        {
            _tokenStoreService = tokenStoreService;
        }
        public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<ApplicationUser> manager, ApplicationUser user)
        {
            return Task.FromResult(manager.SupportsUserTwoFactor);
        }

        public Task<string> GenerateAsync(string purpose, UserManager<ApplicationUser> manager, ApplicationUser user)
        {
            Random random = new Random();
            int token = random.Next(1000, 9999);

            if (_tokenStoreService.StoreToken(user.Id, token.ToString()))
                return Task.FromResult(token.ToString());

            return Task.FromResult(string.Empty);
        }

        public Task<bool> ValidateAsync(string purpose, string token, UserManager<ApplicationUser> manager, ApplicationUser user)
        {
            var storedToken = _tokenStoreService.GetToken(user.Id);
            if (storedToken != null)
            {
                bool result = storedToken.Equals(token);
                _tokenStoreService.RemoveToken(user.Id);

                return Task.FromResult(result);
            }
            return Task.FromResult(false);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

When your application supports the 2FA the ASP.NET Core Identity will help you to generate a token by calling a method from the UserManager object named GenerateTwoFactorTokenAsync this method will generate a code that contains 6 digits, and as you working in this field (Software programming) you will meet at least one customer, who don't like this approach, that enforce the user to insert 6 digits to complete their login process, and maybe he will ask you to reduce the number to 4 digits.
So how you can fix that? And from where the token that has contains 6 digits come?
When you register the ASP.NET Core Identity at the level of your application inside the Program.cs class we inform the ASP.NET Core Identity to accept the default token provider by add this extension AddDefaultTokenProviders to the AddIdentity extension the AddDefaultTokenProviders extension has all token providers that our applications need to work with them like working with Email Confirmation Provider and Reset Password Token Provider and Registration any type of Authenticators Provider ( like google Authenticator, you can read my article about how to configure Google Authenticator with ASP.NET Core Identity from here.
So, in our case we need to work with TwoFactorUserId Provider and this will enforce our users to complete their login process in two steps.
ASP.NET Core Identity provide us with an interface name IUserTwoFactorTokenProvider and by inheriting from this interface we can change the default implementation of the TwoFactorUserId Provider as you see in the above code and this interface has three members like below:

Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user);
Task<string> GenerateAsync(string purpose, UserManager<TUser> manager, TUser user);
Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user);
Enter fullscreen mode Exit fullscreen mode

The first method CanGenerateTwoFactorTokenAsync will be calling when we call the GetTwoFactorEnabledAsync method from the UserManager object and this method (GetTwoFactorEnabledAsync) will return IList of the all available providers for the current user that he wants to login in our application, and the type of the provider in our case is Identity.TwoFactorUserId beacuse when we register the ASP.NET Core Identity we use the custom two factor authentication provider with name Identity.TwoFactorUserId, like so in the Program.cs file:

builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    options.SignIn.RequireConfirmedAccount = true;
    options.SignIn.RequireConfirmedPhoneNumber = true;
    options.Password.RequireDigit = true;
    options.Password.RequireLowercase = true;
    options.Password.RequiredLength = 5;
    options.Password.RequireUppercase = false;
    options.Password.RequireNonAlphanumeric = false;
    options.Lockout.MaxFailedAccessAttempts = 5;
    options.User.RequireUniqueEmail = true;
}).AddRoles<IdentityRole>().AddEntityFrameworkStores<BaseDbContext>()
.AddTokenProvider<CustomTwoFactorTokenProvider>(IdentityConstants.TwoFactorUserIdScheme);

builder.Services.AddAuthentication(opt =>
{
    opt.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
}).AddCookie(options =>
{
    options.LoginPath = "/Accounts/Login";
});
Enter fullscreen mode Exit fullscreen mode

Keep your eyes open at our default token provider here, we use the AddTokenProvider instead of the AddDefaultTokenProviders

.AddTokenProvider<CustomTwoFactorTokenProvider>(IdentityConstants.TwoFactorUserIdScheme);
Enter fullscreen mode Exit fullscreen mode

The CanGenerateTwoFactorTokenAsync metho it will depend on your business and as you can see here, I return result of Boolean (true) if the current has TowFactorEnabled's property set to true in the back store (SQL Server in my case), feel free to apply any business fits your needs.

So, after we return IList of the all available providers we will generating the token provider by calling GenerateTwoFactorTokenAsync method from the UserManager object and in the same way this method will call GenerateAsync method internally from the CustomTwoFactorTokenProvider.cs class after that we sending the token provier to our user (by SMS or Email service) then, the user will apply this token (code) in a completely new screen, and your job is to validate the token that coming from the user with the one that you're generate it and sending it to that user, so here you will call the VerifyTwoFactorTokenAsync method from the UserManager object also, and by calling this method it will call the ValidateAsync from the our custom class CustomTwoFactorTokenProvider.cs that has implements the IUserTwoFactorTokenProvider interface.
So, we need a to store the custom token provider that we generate and send it to our user in a safe place, for that reason I create the TokenStoreService.cs class that you see it at the beginning of this article. Be sure to remove the token provider from the ConcurrentDictionary after validation it to avoids any duplication of the key in that dictionary.

Let us give it a try by firing the application and navigate to the https:localhost:port/Home/Home/SecureContent

Here is the Login screen:

Image description

And as you can see here, I dropped the token provider to Debug output to five myself a chance to apply it in the two-step screen

Image description
The code is 2607

And here is the two-step screen:

Image description
And last but not least the SecureContent page with my name as current logged-in user

Image description

Oldest comments (0)