diff --git a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs index 9a8223d013..ea9bb9d135 100644 --- a/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs +++ b/src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -62,7 +63,7 @@ namespace Microsoft.AspNetCore.Authentication var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { - throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}"); + throw await CreateMissingHandlerException(scheme); } var result = await handler.AuthenticateAsync(); @@ -96,7 +97,7 @@ namespace Microsoft.AspNetCore.Authentication var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { - throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {scheme}"); + throw await CreateMissingHandlerException(scheme); } await handler.ChallengeAsync(properties); @@ -124,7 +125,7 @@ namespace Microsoft.AspNetCore.Authentication var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { - throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {scheme}"); + throw await CreateMissingHandlerException(scheme); } await handler.ForbidAsync(properties); @@ -155,13 +156,19 @@ namespace Microsoft.AspNetCore.Authentication } } - var handler = await Handlers.GetHandlerAsync(context, scheme) as IAuthenticationSignInHandler; + var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { - throw new InvalidOperationException($"No IAuthenticationSignInHandler is configured to handle sign in for the scheme: {scheme}"); + throw await CreateMissingSignInHandlerException(scheme); } - await handler.SignInAsync(principal, properties); + var signInHandler = handler as IAuthenticationSignInHandler; + if (signInHandler == null) + { + throw await CreateMismatchedSignInHandlerException(scheme, handler); + } + + await signInHandler.SignInAsync(principal, properties); } /// @@ -183,13 +190,114 @@ namespace Microsoft.AspNetCore.Authentication } } - var handler = await Handlers.GetHandlerAsync(context, scheme) as IAuthenticationSignOutHandler; + var handler = await Handlers.GetHandlerAsync(context, scheme); if (handler == null) { - throw new InvalidOperationException($"No IAuthenticationSignOutHandler is configured to handle sign out for the scheme: {scheme}"); + throw await CreateMissingSignOutHandlerException(scheme); } - await handler.SignOutAsync(properties); + var signOutHandler = handler as IAuthenticationSignOutHandler; + if (signOutHandler == null) + { + throw await CreateMismatchedSignOutHandlerException(scheme, handler); + } + + await signOutHandler.SignOutAsync(properties); + } + + private async Task CreateMissingHandlerException(string scheme) + { + var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name)); + + var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?"; + + if (string.IsNullOrEmpty(schemes)) + { + return new InvalidOperationException( + $"No authentication handlers are registered." + footer); + } + + return new InvalidOperationException( + $"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer); + } + + private async Task GetAllSignInSchemeNames() + { + return string.Join(", ", (await Schemes.GetAllSchemesAsync()) + .Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType)) + .Select(sch => sch.Name)); + } + + private async Task CreateMissingSignInHandlerException(string scheme) + { + var schemes = await GetAllSignInSchemeNames(); + + // CookieAuth is the only implementation of sign-in. + var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; + + if (string.IsNullOrEmpty(schemes)) + { + return new InvalidOperationException( + $"No sign-in authentication handlers are registered." + footer); + } + + return new InvalidOperationException( + $"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer); + } + + private async Task CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler) + { + var schemes = await GetAllSignInSchemeNames(); + + var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. "; + + if (string.IsNullOrEmpty(schemes)) + { + // CookieAuth is the only implementation of sign-in. + return new InvalidOperationException(mismatchError + + $"Did you intended to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?"); + } + + return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}."); + } + + private async Task GetAllSignOutSchemeNames() + { + return string.Join(", ", (await Schemes.GetAllSchemesAsync()) + .Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType)) + .Select(sch => sch.Name)); + } + + private async Task CreateMissingSignOutHandlerException(string scheme) + { + var schemes = await GetAllSignOutSchemeNames(); + + var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?"; + + if (string.IsNullOrEmpty(schemes)) + { + // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. + return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer); + } + + return new InvalidOperationException( + $"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer); + } + + private async Task CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler) + { + var schemes = await GetAllSignOutSchemeNames(); + + var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. "; + + if (string.IsNullOrEmpty(schemes)) + { + // CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it. + return new InvalidOperationException(mismatchError + + $"Did you intended to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?"); + } + + return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}."); } } } diff --git a/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationServiceTests.cs b/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationServiceTests.cs index 292c56f50c..e21ea40d51 100644 --- a/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationServiceTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationServiceTests.cs @@ -12,6 +12,51 @@ namespace Microsoft.AspNetCore.Authentication { public class AuthenticationServiceTests { + [Fact] + public async Task AuthenticateThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.AuthenticateAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.AuthenticateAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task ChallengeThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ChallengeAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.ChallengeAsync("missing")); + Assert.Contains("base", ex.Message); + } + + [Fact] + public async Task ForbidThrowsForSchemeMismatch() + { + var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o => + { + o.AddScheme("base", "whatever"); + }).BuildServiceProvider(); + var context = new DefaultHttpContext(); + context.RequestServices = services; + + await context.ForbidAsync("base"); + var ex = await Assert.ThrowsAsync(() => context.ForbidAsync("missing")); + Assert.Contains("base", ex.Message); + } + [Fact] public async Task CanOnlySignInIfSupported() { @@ -26,9 +71,13 @@ namespace Microsoft.AspNetCore.Authentication context.RequestServices = services; await context.SignInAsync("uber", new ClaimsPrincipal(), null); - await Assert.ThrowsAsync(() => context.SignInAsync("base", new ClaimsPrincipal(), null)); + var ex = await Assert.ThrowsAsync(() => context.SignInAsync("base", new ClaimsPrincipal(), null)); + Assert.Contains("uber", ex.Message); + Assert.Contains("signin", ex.Message); await context.SignInAsync("signin", new ClaimsPrincipal(), null); - await Assert.ThrowsAsync(() => context.SignInAsync("signout", new ClaimsPrincipal(), null)); + ex = await Assert.ThrowsAsync(() => context.SignInAsync("signout", new ClaimsPrincipal(), null)); + Assert.Contains("uber", ex.Message); + Assert.Contains("signin", ex.Message); } [Fact] @@ -45,7 +94,9 @@ namespace Microsoft.AspNetCore.Authentication context.RequestServices = services; await context.SignOutAsync("uber"); - await Assert.ThrowsAsync(() => context.SignOutAsync("base")); + var ex = await Assert.ThrowsAsync(() => context.SignOutAsync("base")); + Assert.Contains("uber", ex.Message); + Assert.Contains("signout", ex.Message); await context.SignOutAsync("signout"); await context.SignOutAsync("signin"); } @@ -64,8 +115,10 @@ namespace Microsoft.AspNetCore.Authentication await context.AuthenticateAsync(); await context.ChallengeAsync(); await context.ForbidAsync(); - await Assert.ThrowsAsync(() => context.SignOutAsync()); - await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + var ex = await Assert.ThrowsAsync(() => context.SignOutAsync()); + Assert.Contains("cannot be used for SignOutAsync", ex.Message); + ex = await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + Assert.Contains("cannot be used for SignInAsync", ex.Message); } [Fact] @@ -119,7 +172,8 @@ namespace Microsoft.AspNetCore.Authentication await context.ChallengeAsync(); await context.ForbidAsync(); await context.SignOutAsync(); - await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + var ex = await Assert.ThrowsAsync(() => context.SignInAsync(new ClaimsPrincipal())); + Assert.Contains("cannot be used for SignInAsync", ex.Message); } [Fact]