Hi, in this post I will show you how to setup ASP.NET Core Identity 2FA by using Google authenticator.
2FA is a technique and more secure way and enforce the users to access their accounts with two steps.
Our Example
I suppose you have a ready application with ASP.NET Core Identity with Register and Login functionality (without 2FA). And also (I suppose...) you have a tested user that has TwoFactorEnabled assigned to false in you Database.
So let's started.
First, we need to enable the 2FA in our application, in ASP.NET Core Identity you can achieve that by three ways.
- Phone
- Authenticator, and this is what we are going to do in this post.
To let user work with 2FA we need a setup page that give the user the ability to enable the 2FA for his account.
So, create a new class with this signature:
public class ConfigureTwoFactorAuthenticatorModel
{
/// <summary>
/// The key that we have to store it in our Database
/// </summary>
public string AuthenticatorKey { get; set; }
/// <summary>
/// Generated key the user must insert it in his authenticator app
/// You can use any QRCode library the facilate the reading of this key instead of typing it.
/// Try the QRCode js library
/// </summary>
public string Code { get; set; }
}
Ok, now we have to create an Action Method with html page.
[HttpGet]
[Authorize]
public async Task<IActionResult> TwoFactorAuthenticationSetupPage()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
// Redirect user to any page you want...
return RedirectToAction("/");
}
else
{
var authenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user);
if (authenticatorKey == null)
{
await _userManager.ResetAuthenticatorKeyAsync(user);
authenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user);
}
// For demo purpose writh the key to attached debugger
Debug.WriteLine(authenticatorKey);
return View(new ConfigureTwoFactorAuthenticatorModel { AuthenticatorKey = authenticatorKey });
}
}
The purpose of this action is to create an AuthenticatorKey for current user, by calling _userManager.GetAuthenticatorKeyAsync(user) this method will retrieve the key from the back store and the return of GetAuthenticatorKeyAsync method is string so if the return type is null generate a new authenticator key by calling the new method ResetAuthenticatorKeyAsync and call again GetAuthenticatorKeyAsync method to retrieve the key after saved it in back store. We drop the authenticator key to writeline of debugger for demo purpose (here you have to use an QRCode library to let your users scan the image and type the code is the code's field in the html page (coming next). Pretty easy!
Before digging into html page of the [TwoFactorAuthenticationSetupPage] action method let us finished from the HTTPOST method, the code is below:
[HttpPost]
[Authorize]
public async Task<IActionResult> TwoFactorAuthenticationSetupPage(ConfigureTwoFactorAuthenticatorModel model)
{
if (ModelState.IsValid)
{
var user = await _userManager.GetUserAsync(User);
bool isValidCode = await _userManager.VerifyTwoFactorTokenAsync(user,
_userManager.Options.Tokens.AuthenticatorTokenProvider,
model.Code);
if (isValidCode)
{
await _userManager.SetTwoFactorEnabledAsync(user, true);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("", "Error in Code");
return View(model);
}
}
ModelState.AddModelError("", "General Error");
return View(model);
}
This method is straightforward , check the model state and validate the token by calling VerifyTwoFactorTokenAsync the second argument of this method specify the name of the authenticator. which is come from the AddDefaultTokenProviders,the extension you added to your startup class when you configure the ASP.NET Core Identity.
Now the time for the html page ( no need to explain it, it's pretty easy to get it's grasp)
@model Platform.ViewModel.ConfigureTwoFactorAuthenticatorModel
@{
ViewData["Title"] = "TwoFactorAuthenticationSetupPage";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<div class="row justify-content-md-center" style="margin-top:130px;">
<div class="col-md-5">
<div class="card">
<h5 class="card-header bg-dark text-white">TwoFactorAuthenticationSetupPage</h5>
<div class="card-body">
<form class="px-4 py-3" asp-action="TwoFactorAuthenticationSetupPage" asp-controller="Account" method="post" id="frmLogin">
<div asp-validation-summary="All" class="text-danger"></div>
<input type="hidden" asp-for="AuthenticatorKey" />
<div class="form-group">
<input class="form-control" asp-for="Code">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
</div>
</div>
</div>
Let' run the app and see what happened?!
This is the AuthenticatorKey that we write it to Debugger.
This is the code the Google Authenticator App generated to me after I inserted the AuthenticatorKey to Google Authenticator App.
This is the AuthenticatorKey saved to BackStore in AspNetUserTokens Table.
So, now we prepare our app and registration to work with the any Authenticator app. The next step is Signing with Authenticator App.
First, let me paste my LoginModel.cs class
public class LoginModel
{
[Required]
public string Email { get; set; }
[Required]
public string Password { get; set; }
}
Straightforward class that let user provide his email and password.
And this is the Login [HTTPPOST] Action Method
[AllowAnonymous]
[ValidateAntiForgeryToken]
[HttpPost]
public async Task<IActionResult> Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid)
{
AppUser user = await _userManager.FindByEmailAsync(model.Email);
if(user!= null)
{
await _signInManager.SignOutAsync();
Microsoft.AspNetCore.Identity.SignInResult result =
await _signInManager.PasswordSignInAsync(user, model.Password, false, false);
if (result.RequiresTwoFactor)
{
return RedirectToAction("LoginTwoStep", new { returnUrl = returnUrl });
}
if (result.Succeeded)
{
return Redirect(returnUrl ?? "/");
}
}
ModelState.AddModelError(nameof(LoginModel.Email), "Invalid user name or password");
}
return View(model);
}
Here after user looged in we check if he has 2FA enabled by calling this property RequiresTwoFactor and if the result of this property return true so we have to redirect the user to LoginTwoStep Action Method.
and here is the HTTPGET and HTTPPOST of the LoginTwoStep Action method
[HttpGet]
public IActionResult LoginTwoStep(string returnUrl)
{
return View(new LoginTwoStepModel { ReturnUrl = returnUrl});
}
[HttpPost]
public async Task<IActionResult> LoginTwoStep(LoginTwoStepModel model)
{
if (ModelState.IsValid)
{
var user = await _signInManager.GetTwoFactorAuthenticationUserAsync();
if(user!= null)
{
var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(model.Token, true, false);
if (result.Succeeded)
{
return Redirect(model.ReturnUrl);
}
// show the error to user
ModelState.AddModelError("", "General Error");
return View(model);
}
}
// show the error to user
ModelState.AddModelError("", "General Error");
return View(model);
}
In the POST method we return an user for the current 2FA login by calling GetTwoFactorAuthenticationUserAsync method and if this method return null we have to cancel the operation of the login otherwise we have to complete the process of the login as you see in the next pictures
Here I have to insert the code from the Google Authenticator App, see next picture:
Sorry for the dirty pic, because I can't take an screenshot from the Google Authenticator App I took a photo by other phone.
In the HTTPPOST of the Login Action Method, instead of using:
if (result.RequiresTwoFactor)
{
return RedirectToAction("LoginTwoStep", new { returnUrl = returnUrl });
}
You can check by the following code:
if (await _userManager.GetTwoFactorEnabledAsync(user))
{
var validProvider = await _userManager.GetValidTwoFactorProvidersAsync(user);
if (validProvider.Contains(_userManager.Options.Tokens.AuthenticatorTokenProvider))
{
return RedirectToAction("LoginTwoStep", new { returnUrl = returnUrl });
}
}
Here first we check if the current user has enabled the 2FA in back store (database), then we made a second check if the available 2FA is AuthenticatorTokenProvider and if so then redirect the user to the LoginTowStep Action Method.