Google Authenticator Service in .NET Core

Posted on: September 12, 2016 1:43:16 AM
I'm in the process of converting my websites from ASP.NET 4 to .NET Core and one thing some of my sites use for added security is two factor authentication. .NET has a very nice built in handler for many types of authentication, including two factor; however, there was not any native support for Google Authenticator. Rather than try to switch my two factor to something already supported, I decided to take on the challenge of adding a new type. There is little to no documentation on how to do this as of my writing this, so it took a lot of research and digging into the source code of many .NET Core libraries. It was my goal to have the framework handle most of the leg work rather than do it myself.
GoogleAuthenticatorService.cs
    public class GoogleAuthenticatorService<TUser> : TotpSecurityStampBasedTokenProvider<TUser> where TUser : class
    {
        private static readonly DateTime UNIX_EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

        public override Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<TUser> manager, TUser user)
        {
            return manager.GetTwoFactorEnabledAsync(user);
        }

        public string GenerateSecret()
        {
            byte[] buffer = new byte[9];

            using (var rng = RandomNumberGenerator.Create())
            {
                rng.GetBytes(buffer);
            }

            return Convert.ToBase64String(buffer).Substring(0, 10).Replace('/', '0').Replace('+', '1');
        }

        public string GetCode(string secret, long counter)
        {
            return GeneratePassword(secret, counter);
        }

        public string GetCode(string secret)
        {
            return GetCode(secret, GetCurrentCounter());
        }

        public long GetCurrentCounter()
        {
            return GetCurrentCounter(DateTime.UtcNow, UNIX_EPOCH, 30);
        }

        public bool IsValid(string secret, string code, int checkAdjacentIntervals = 1)
        {
            if (code == GetCode(secret))
                return true;

            for (int i = 1; i <= checkAdjacentIntervals; i++)
            {
                if (code == GetCode(secret, GetCurrentCounter() + i))
                    return true;

                if (code == GetCode(secret, GetCurrentCounter() - i))
                    return true;
            }

            return false;
        }

        public override async Task<bool> ValidateAsync(string purpose, string token, UserManager<TUser> manager, TUser user)
        {
            return IsValid(await manager.GetSecurityStampAsync(user), token);
        }

        private string GeneratePassword(string secret, long iterationNumber, int digits = 6)
        {
            byte[] counter = BitConverter.GetBytes(iterationNumber);

            if (BitConverter.IsLittleEndian)
                Array.Reverse(counter);

            byte[] key = Encoding.ASCII.GetBytes(secret);
            byte[] hash;

            using (HMACSHA1 hmac = new HMACSHA1(key))
            {
                hash = hmac.ComputeHash(counter);
            }

            int offset = hash[hash.Length - 1] & 0xf;

            int binary =
                ((hash[offset] & 0x7f) << 24)
                | ((hash[offset + 1] & 0xff) << 16)
                | ((hash[offset + 2] & 0xff) << 8)
                | (hash[offset + 3] & 0xff);

            int password = binary % (int)Math.Pow(10, digits);

            return password.ToString(new string('0', digits));
        }

        private long GetCurrentCounter(DateTime now, DateTime epoch, int timeStep)
        {
            return (long)(now - epoch).TotalSeconds / timeStep;
        }
    }
The biggest thing I want to point out in this class is the base class TotpSecurityStampBasedTokenProvider<TUser> This is the class that allows us to register the class as a TokenProvider within .NET Core.
Startup.cs
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton(typeof(IConfigurationRoot), Configuration);

            // Add framework services.
            services.AddApplicationInsightsTelemetry(Configuration);

           services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<AuthDbContext>()
                .AddTokenProvider<GoogleAuthenticatorService<ApplicationUser>>("Google");

            services.AddMvc();

            services.Configure<IdentityOptions>(options =>
            {
                options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(10);
                options.Lockout.MaxFailedAccessAttempts = 10;
            });
            services.Configure<CookieAuthenticationOptions>(options =>
            {
                options.LoginPath = new PathString("/Admin/Login");
            });
        }
You can see that we register the new TokenProvider and name it "Google".
AdminController.cs
        [AllowAnonymous, HttpPost, ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginViewModel loginModel, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            if (ModelState.IsValid)
            {
                var result = await _signInManager.PasswordSignInAsync(loginModel.Username, loginModel.Password, false, true);

                if (result.Succeeded)
                {
                    return RedirectToLocal(returnUrl);
                }
                else if (result.RequiresTwoFactor)
                {
                    return View("TwoAuth");
                }
                else if (result.IsLockedOut)
                {
                    return View("Login");
                }
            }

            loginModel.Password = null;
            return View(loginModel);
        }

        [AllowAnonymous]
        public async Task<IActionResult> TwoAuth(string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
            if (user == null)
            {
                return View("Login");
            }

            return View();
        }

        [AllowAnonymous, ValidateAntiForgeryToken, HttpPost]
        public async Task<IActionResult> TwoAuth(string code, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
            if (user != null)
            {
                var result = await _signInManager.TwoFactorSignInAsync("Google", code, false, false);

                if (result.Succeeded)
                {
                    return RedirectToLocal(returnUrl);
                }
                else if (result.IsLockedOut)
                {
                    return View("Login");
                }
            }

            return View();
        }
Here is a snippet of my AdminController that requires an authenticated user. As you can see, it checks if two factor auth is required. If it is, it tells it where to go to get the code. All of the details are taken care of using the built-in SignInManager.

Comments


No comments yet, be the first to leave one.

Leave a Comment