No sign-out authentication handler is registered for the scheme 'Identity.TwoFactorUserId'

Viewed 6494

ASP.NET Core 2.2 web app using code migrated from full fat MVC app.

My AccountController contains this simple code for its Logout route.

await this.SignInManager.SignOutAsync();
return this.RedirectToAction(nameof(Landing.HomeController.Index), "Home");

But this gives.

No sign-out authentication handler is registered for the scheme 'Identity.TwoFactorUserId'.

Pretty confusing given that I've never mentioned 2FA in my code, and Google login is working.

serviceCollection
    .AddIdentityCore<MyUser>(identityOptions =>
    {
        identityOptions.SignIn.RequireConfirmedEmail = false;
    })
    .AddUserStore<MyUserStore>()
    .AddSignInManager<SignInManager<MyUser>>();

serviceCollection.AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddCookie(IdentityConstants.ApplicationScheme, options =>
    {
        options.SlidingExpiration = true;
    })
    .AddGoogle(googleOptions =>
    {
        this.Configuration.Bind("OAuth2:Providers:Google", googleOptions);

        googleOptions.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub", "string");
    })
    .AddExternalCookie();
4 Answers

As a complement to @Luke's answer:

The reason why SignInManager::SignOutAsync() throws is this method will also sign out the TwoFactorUserIdScheme behind the scenes:

public virtual async Task SignOutAsync()
{
    await Context.SignOutAsync(IdentityConstants.ApplicationScheme);
    await Context.SignOutAsync(IdentityConstants.ExternalScheme);
    await Context.SignOutAsync(IdentityConstants.TwoFactorUserIdScheme);
}

(See source code)

Typically, these tree authentication schemes are registered automatically by AddIdentity<TUser, TRole>():

public static IdentityBuilder AddIdentity<TUser, TRole>(
    this IServiceCollection services,
    Action<IdentityOptions> setupAction)
{
    services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
        options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
        options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    ...
    .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    });
    ... // other services
}

(See source code )

However, you added the Identity services by AddIdentityCore<>() instead of the AddIdentity<>().

Because the AddIdentityCore<>() doesn't register a TwoFactorUserIdScheme scheme (see source code) automatically, there's no associated CookieAuthenticationHandler for TwoFactorUserIdScheme. As a result, it throws.


How to solve

In order to work with SignInManager.SignOutAsync(), according to above description, we need ensure a <scheme>-<handler> map has been registed for TwoFactorUserIdScheme .

So I change your code as below, now it works fine for me:

serviceCollection.AddAuthentication(IdentityConstants.ApplicationScheme)
    .AddCookie(IdentityConstants.ApplicationScheme, options =>
    {
        options.SlidingExpiration = true;
    })
    .AddCookie(IdentityConstants.TwoFactorUserIdScheme, o =>
    {
        o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
        o.ExpireTimeSpan = TimeSpan.FromMinutes(5);
    })
    .AddGoogle(googleOptions =>
    {
        this.Configuration.Bind("OAuth2:Providers:Google", googleOptions);
        googleOptions.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "sub", "string");
    })
    .AddExternalCookie();

Also you can create your own SignInManager<MyUser> and override sign out as you need

public class CustomSignInManager : SignInManager<MyUser>
{
    public override async Task SignOutAsync()
    {    
        await Context.SignOutAsync(IdentityConstants.ApplicationScheme);
        await Context.SignOutAsync(GoogleDefaults.AuthenticationScheme);
    }
}

Then change AddSignInManager<SignInManager<MyUser>>() to AddSignInManager<CustomSignInManager>() in your Startup class

serviceCollection
    .AddIdentityCore<MyUser>(identityOptions =>
    {
        identityOptions.SignIn.RequireConfirmedEmail = false;
    })
    .AddUserStore<MyUserStore>()
    .AddSignInManager<CustomSignInManager>();

Do not use the SignOutAsync method on a SignInManager<T> you've injected into the controller. Instead, use the method on the HttpContext which takes a scheme argument. I don't know why.

Below code works for me , use the same AuthenticationScheme that you use while "AddAuthentication" in startup.cs

[HttpGet("signout")]
[AllowAnonymous]
public async Task signout()
{
    await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);

    var prop = new AuthenticationProperties
    {
        RedirectUri = "/logout-complete"
    };
    // after signout this will redirect to your provided target
    await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, prop);
}

[HttpGet("logout-complete")]
[AllowAnonymous]
public string logoutComplete()
{
    return "logout-complete";
}
Related