Search code examples
c#asp.net.netapiasp.net-core

SignInManager always fails for TwoFactorAuthenticatorSignInAsync() and TwoFactorSignInAsync()


Background

I'm building an API with .NET 6 where I'm currently trying to implement two factor authentication using Google Authenticator. It seems to be working fine up until the final part where I actually try to sign in using the code provided by the authenticator app.

Code

2FA Setup - Part 1:

[HttpGet("TwoFactorAuthSetup/{email}")]
public async Task<IActionResult> GetTwoFactorAuthSetup(string email)
{
    ...
    var isTwoFactorAuthEnabled = await _userManager.GetTwoFactorEnabledAsync(user);
    var authenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user);
    //var authenticatorKey = await _userManager.GenerateNewAuthenticatorKey(user);

    if (authenticatorKey == null)
    {
        await _userManager.ResetAuthenticatorKeyAsync(user);
        authenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user);
    }
    ...

2FA Setup - Part 2:

[HttpPost("TwoFactorAuthSetup")]
public async Task<IActionResult> SetupTwoFactorAuth([FromBody] TwoFactorAuthSetupDto model)
{
    var user = await _userManager.FindByEmailAsync(model.Email);

    var isCodeValid = await _userManager
        .VerifyTwoFactorTokenAsync(user,
          _userManager.Options.Tokens.AuthenticatorTokenProvider,
          model.Code);

    if (!isCodeValid)
    {
        return BadRequest("Invalid code");
    }

    await _userManager.SetTwoFactorEnabledAsync(user, true);
    ...

2FA Login:

[HttpPost("login-2fa")]
[AllowAnonymous]
public async Task<IActionResult> LoginWithTwoFactorAuth(TwoFactorAuthSetupDto model)
{
    var user = await _userManager.FindByEmailAsync(model.Email);
    //user = await _signInManager.GetTwoFactorAuthenticationUserAsync();

    if (user == null)
    {
        return Unauthorized();
    }

    //var isValid = await _userManager.VerifyTwoFactorTokenAsync(user, _userManager.Options.Tokens.AuthenticatorTokenProvider, model.Code);
    var result = await _signInManager.TwoFactorSignInAsync(_userManager.Options.Tokens.AuthenticatorTokenProvider, model.Code, true, false);
    var result2 = await _signInManager.TwoFactorAuthenticatorSignInAsync( model.Code, true, false);

Both 2FA Setup - Part 1 and 2FA Setup - Part 2 work as expected; I'm able to successfully setup the authenticatorKey in my app, and the 6-digit code gets accepted by my controller.

The issue arises when trying to sign in using the authenticator app. Both result and result2 always end up with Succeeded = false. When I try to use the _userManager.VerifyTwoFactorTokenAsync method instead, it does show me that the code I use is valid. I'm unsure of how to proceed.


Solution

  • I figured it out. In case you have a similar issue, consider the following two steps -

    • Use _signInManager.PasswordSignInAsync to sign in the user. This drops a cookie that helps remember the user in case they had ticked the remember me.

    • Use var user = _signInManager.GetTwoFactorAuthenticationuserAsync() which checks for the presence of the Identity.TwoFactorUserId cookie.

    I made few mistakes earlier:

    • I used _signInManager.CheckPasswordSignInAsync instead of _signInManager.PasswordSignInAsync.
    • I mistakenly used var user = await _userManager.FindByEmailAsync(model.Email); to get the user for which 2 factor auth login had to be performed. This provides a security risk. I since replaced this line with await _signInManager.GetTwoFactorAuthenticationUserAsync().

    With the changes mentioned above it now works as intended.