Search code examples
asp.net-coreasp.net-core-identitytwo-factor-authenticationasp.net-core-3.1

How can I combine external logins and two-factor authentication in ASP.NET Core Identity?


I'm implementing authentication and authorization for a simple web application built with ASP.NET Core 3.1. So far, ASP.NET Core Identity meets the requirements except for one: the default implementation of external logins (in this case, Google) does not seem to work with the default implementation of Two-Factor Authentication.

When I log in using a local account (with 2FA configured), I am prompted for the TOTP token. When I log in using a Google account, there is no prompt for the second factor. I have spend a few days researching this (e.g. learn.microsoft.com, Google, and here on SO) but found nothing useful. How can I get ASP.NET Core Identity to use Two-Factor Authentication for users who log in with Google?


Solution

  • Prerequisites:

    • An ASP.NET Core project using ASP.NET Core Identity with one or more external login providers already configured and working, e.g. Google.

    Here's how I got it working.

    1. Use the Scaffold Identity procedure to override the Account/ExternalLogin page. (I also added that page to the Wayback Machine. Thanks, Internet Archive!)
    2. Open the ExternalLogin.cshtml.cs file and locate the OnGetCallbackAsync method.
    3. Find the call to _signInManager.ExternalLoginSignInAsync() and change the bypassTwoFactor parameter from true to false.
    4. Find the conditional statements which check the result of ExternalLoginSignInAsync(), and add this code:

          if (result.RequiresTwoFactor)
          {
              return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = false });
          }
      

    Putting it all together, you should have something like this:

        // Sign in the user with this external login provider if the user already has a login.
        var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : false);
        if (result.Succeeded)
        {
            _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider);
            return LocalRedirect(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = false });
        }
        if (result.IsLockedOut)
        {
            return RedirectToPage("./Lockout");
        }
        else
        {
            // existing code omitted for brevity
        }
    

    Note: Since I was unable to find any documentation or examples on how to do this, I accept that my implementation may not be optimal. If you know a better way, please share!