I'm having a weird problem with an ASP.NET Core 2.1 site. When I sign into it, and refresh it 30 minutes later, I always get this exception thrown:
InvalidOperationException: No sign-out authentication handler is registered for the scheme 'Identity.External'. The registered sign-out schemes are: Identity.Application. Did you forget to call AddAuthentication().AddCookies("Identity.External",...)?
It's correct that I don't have Identity.External registered, but I also don't want it registered. Why does it keep trying to sign it out? Here's how I'm registering my cookie:
services.AddAuthentication(
o => {
o.DefaultScheme = IdentityConstants.ApplicationScheme;
}).AddCookie(IdentityConstants.ApplicationScheme,
o => {
o.Events = new CookieAuthenticationEvents {
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
});
services.ConfigureApplicationCookie(
o => {
o.Cookie.Expiration = TimeSpan.FromHours(2);
o.Cookie.HttpOnly = true;
o.Cookie.SameSite = SameSiteMode.Strict;
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.AccessDeniedPath = "/admin";
o.LoginPath = "/admin";
o.LogoutPath = "/admin/sign-out";
o.SlidingExpiration = true;
});
Could someone point me in the right direction on how to resolve this?
UPDATE
Here's the complete code and use process as requested by @Edward in the comments. I'm omitting some parts for brevity.
Startup.cs
public sealed class Startup {
public void ConfigureServices(
IServiceCollection services) {
// ...
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddApplicationIdentity();
services.AddScoped<ApplicationSignInManager>();
services.Configure<IdentityOptions>(
o => {
o.Password.RequiredLength = 8;
o.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
o.Lockout.MaxFailedAccessAttempts = 5;
});
services.ConfigureApplicationCookie(
o => {
o.Cookie.Name = IdentityConstants.ApplicationScheme;
o.Cookie.Expiration = TimeSpan.FromHours(2);
o.Cookie.HttpOnly = true;
o.Cookie.SameSite = SameSiteMode.Strict;
o.Cookie.SecurePolicy = CookieSecurePolicy.Always;
o.AccessDeniedPath = "/admin";
o.LoginPath = "/admin";
o.LogoutPath = "/admin/sign-out";
o.SlidingExpiration = true;
});
// ...
}
public void Configure(
IApplicationBuilder app) {
// ...
app.UseAuthentication();
// ...
}
}
ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions {
public static IdentityBuilder AddApplicationIdentity(
this IServiceCollection services) {
services.AddAuthentication(
o => {
o.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
o.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
o.DefaultForbidScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignInScheme = IdentityConstants.ApplicationScheme;
o.DefaultSignOutScheme = IdentityConstants.ApplicationScheme;
}).AddCookie(IdentityConstants.ApplicationScheme,
o => {
o.Events = new CookieAuthenticationEvents {
OnValidatePrincipal = SecurityStampValidator.ValidatePrincipalAsync
};
});
services.TryAddScoped<SignInManager<User>, ApplicationSignInManager>();
services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
services.TryAddScoped<IdentityErrorDescriber>();
services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<User>>();
services.TryAddScoped<IUserClaimsPrincipalFactory<User>, UserClaimsPrincipalFactory<User>>();
services.TryAddScoped<UserManager<User>>();
services.TryAddScoped<IUserStore<User>, ApplicationUserStore>();
return new IdentityBuilder(typeof(User), services);
}
}
DefaultController.cs
[Area("Admin")]
public sealed class DefaultController :
AdminControllerBase {
[HttpPost, AllowAnonymous]
public async Task<IActionResult> SignIn(
SignIn.Command command) {
var result = await Mediator.Send(command);
if (result.Succeeded) {
return RedirectToAction("Dashboard", new {
area = "Admin"
});
}
return RedirectToAction("SignIn", new {
area = "Admin"
});
}
[HttpGet, ActionName("sign-out")]
public async Task<IActionResult> SignOut() {
await Mediator.Send(new SignOut.Command());
return RedirectToAction("SignIn", new {
area = "Admin"
});
}
}
SignIn.cs
public sealed class SignIn {
public sealed class Command :
IRequest<SignInResult> {
public string Password { get; set; }
public string Username { get; set; }
}
public sealed class CommandHandler :
HandlerBase<Command, SignInResult> {
private ApplicationSignInManager SignInManager { get; }
public CommandHandler(
DbContext context,
ApplicationSignInManager signInManager)
: base(context) {
SignInManager = signInManager;
}
protected override SignInResult Handle(
Command command) {
var result = SignInManager.PasswordSignInAsync(command.Username, command.Password, true, false).Result;
return result;
}
}
}
SignOut.cs
public sealed class SignOut {
public sealed class Command :
IRequest {
}
public sealed class CommandHandler :
HandlerBase<Command> {
private ApplicationSignInManager SignInManager { get; }
public CommandHandler(
DbContext context,
ApplicationSignInManager signInManager)
: base(context) {
SignInManager = signInManager;
}
protected override async void Handle(
Command command) {
await SignInManager.SignOutAsync();
}
}
}
There's all the relevant code, from how I configure the identity to how I sign in and out. I'm still at a loss of why Identity.External is coming into the picture when I never asked for it.
Technically the SignIn and SignOut classes can be removed and their functionality merged into the DefaultController, however I opt into keeping them to keep the application structure consistent.
SignInManager.PasswordSignInAsyncmethod for signing in because I need to check the result and theSignInManager.SingOutAsyncfor signing out.