Skip to content

Commit 4763337

Browse files
authored
#955 Print available scheme names in exception message for invalid scheme (#961)
1 parent 231f3a4 commit 4763337

File tree

2 files changed

+177
-15
lines changed

2 files changed

+177
-15
lines changed

src/Microsoft.AspNetCore.Authentication.Core/AuthenticationService.cs

+117-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Linq;
56
using System.Security.Claims;
67
using System.Threading.Tasks;
78
using Microsoft.AspNetCore.Http;
@@ -62,7 +63,7 @@ public virtual async Task<AuthenticateResult> AuthenticateAsync(HttpContext cont
6263
var handler = await Handlers.GetHandlerAsync(context, scheme);
6364
if (handler == null)
6465
{
65-
throw new InvalidOperationException($"No authentication handler is configured to authenticate for the scheme: {scheme}");
66+
throw await CreateMissingHandlerException(scheme);
6667
}
6768

6869
var result = await handler.AuthenticateAsync();
@@ -96,7 +97,7 @@ public virtual async Task ChallengeAsync(HttpContext context, string scheme, Aut
9697
var handler = await Handlers.GetHandlerAsync(context, scheme);
9798
if (handler == null)
9899
{
99-
throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {scheme}");
100+
throw await CreateMissingHandlerException(scheme);
100101
}
101102

102103
await handler.ChallengeAsync(properties);
@@ -124,7 +125,7 @@ public virtual async Task ForbidAsync(HttpContext context, string scheme, Authen
124125
var handler = await Handlers.GetHandlerAsync(context, scheme);
125126
if (handler == null)
126127
{
127-
throw new InvalidOperationException($"No authentication handler is configured to handle the scheme: {scheme}");
128+
throw await CreateMissingHandlerException(scheme);
128129
}
129130

130131
await handler.ForbidAsync(properties);
@@ -155,13 +156,19 @@ public virtual async Task SignInAsync(HttpContext context, string scheme, Claims
155156
}
156157
}
157158

158-
var handler = await Handlers.GetHandlerAsync(context, scheme) as IAuthenticationSignInHandler;
159+
var handler = await Handlers.GetHandlerAsync(context, scheme);
159160
if (handler == null)
160161
{
161-
throw new InvalidOperationException($"No IAuthenticationSignInHandler is configured to handle sign in for the scheme: {scheme}");
162+
throw await CreateMissingSignInHandlerException(scheme);
163+
}
164+
165+
var signInHandler = handler as IAuthenticationSignInHandler;
166+
if (signInHandler == null)
167+
{
168+
throw await CreateMismatchedSignInHandlerException(scheme, handler);
162169
}
163170

164-
await handler.SignInAsync(principal, properties);
171+
await signInHandler.SignInAsync(principal, properties);
165172
}
166173

167174
/// <summary>
@@ -183,13 +190,114 @@ public virtual async Task SignOutAsync(HttpContext context, string scheme, Authe
183190
}
184191
}
185192

186-
var handler = await Handlers.GetHandlerAsync(context, scheme) as IAuthenticationSignOutHandler;
193+
var handler = await Handlers.GetHandlerAsync(context, scheme);
187194
if (handler == null)
188195
{
189-
throw new InvalidOperationException($"No IAuthenticationSignOutHandler is configured to handle sign out for the scheme: {scheme}");
196+
throw await CreateMissingSignOutHandlerException(scheme);
197+
}
198+
199+
var signOutHandler = handler as IAuthenticationSignOutHandler;
200+
if (signOutHandler == null)
201+
{
202+
throw await CreateMismatchedSignOutHandlerException(scheme, handler);
203+
}
204+
205+
await signOutHandler.SignOutAsync(properties);
206+
}
207+
208+
private async Task<Exception> CreateMissingHandlerException(string scheme)
209+
{
210+
var schemes = string.Join(", ", (await Schemes.GetAllSchemesAsync()).Select(sch => sch.Name));
211+
212+
var footer = $" Did you forget to call AddAuthentication().Add[SomeAuthHandler](\"{scheme}\",...)?";
213+
214+
if (string.IsNullOrEmpty(schemes))
215+
{
216+
return new InvalidOperationException(
217+
$"No authentication handlers are registered." + footer);
218+
}
219+
220+
return new InvalidOperationException(
221+
$"No authentication handler is registered for the scheme '{scheme}'. The registered schemes are: {schemes}." + footer);
222+
}
223+
224+
private async Task<string> GetAllSignInSchemeNames()
225+
{
226+
return string.Join(", ", (await Schemes.GetAllSchemesAsync())
227+
.Where(sch => typeof(IAuthenticationSignInHandler).IsAssignableFrom(sch.HandlerType))
228+
.Select(sch => sch.Name));
229+
}
230+
231+
private async Task<Exception> CreateMissingSignInHandlerException(string scheme)
232+
{
233+
var schemes = await GetAllSignInSchemeNames();
234+
235+
// CookieAuth is the only implementation of sign-in.
236+
var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?";
237+
238+
if (string.IsNullOrEmpty(schemes))
239+
{
240+
return new InvalidOperationException(
241+
$"No sign-in authentication handlers are registered." + footer);
242+
}
243+
244+
return new InvalidOperationException(
245+
$"No sign-in authentication handler is registered for the scheme '{scheme}'. The registered sign-in schemes are: {schemes}." + footer);
246+
}
247+
248+
private async Task<Exception> CreateMismatchedSignInHandlerException(string scheme, IAuthenticationHandler handler)
249+
{
250+
var schemes = await GetAllSignInSchemeNames();
251+
252+
var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for SignInAsync. ";
253+
254+
if (string.IsNullOrEmpty(schemes))
255+
{
256+
// CookieAuth is the only implementation of sign-in.
257+
return new InvalidOperationException(mismatchError
258+
+ $"Did you intended to call AddAuthentication().AddCookies(\"Cookies\") and SignInAsync(\"Cookies\",...)?");
259+
}
260+
261+
return new InvalidOperationException(mismatchError + $"The registered sign-in schemes are: {schemes}.");
262+
}
263+
264+
private async Task<string> GetAllSignOutSchemeNames()
265+
{
266+
return string.Join(", ", (await Schemes.GetAllSchemesAsync())
267+
.Where(sch => typeof(IAuthenticationSignOutHandler).IsAssignableFrom(sch.HandlerType))
268+
.Select(sch => sch.Name));
269+
}
270+
271+
private async Task<Exception> CreateMissingSignOutHandlerException(string scheme)
272+
{
273+
var schemes = await GetAllSignOutSchemeNames();
274+
275+
var footer = $" Did you forget to call AddAuthentication().AddCookies(\"{scheme}\",...)?";
276+
277+
if (string.IsNullOrEmpty(schemes))
278+
{
279+
// CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
280+
return new InvalidOperationException($"No sign-out authentication handlers are registered." + footer);
281+
}
282+
283+
return new InvalidOperationException(
284+
$"No sign-out authentication handler is registered for the scheme '{scheme}'. The registered sign-out schemes are: {schemes}." + footer);
285+
}
286+
287+
private async Task<Exception> CreateMismatchedSignOutHandlerException(string scheme, IAuthenticationHandler handler)
288+
{
289+
var schemes = await GetAllSignOutSchemeNames();
290+
291+
var mismatchError = $"The authentication handler registered for scheme '{scheme}' is '{handler.GetType().Name}' which cannot be used for {nameof(SignOutAsync)}. ";
292+
293+
if (string.IsNullOrEmpty(schemes))
294+
{
295+
// CookieAuth is the most common implementation of sign-out, but OpenIdConnect and WsFederation also support it.
296+
return new InvalidOperationException(mismatchError
297+
+ $"Did you intended to call AddAuthentication().AddCookies(\"Cookies\") and {nameof(SignOutAsync)}(\"Cookies\",...)?");
190298
}
191299

192-
await handler.SignOutAsync(properties);
300+
return new InvalidOperationException(mismatchError + $"The registered sign-out schemes are: {schemes}.");
193301
}
194302
}
195303
}

test/Microsoft.AspNetCore.Authentication.Core.Test/AuthenticationServiceTests.cs

+60-6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,51 @@ namespace Microsoft.AspNetCore.Authentication
1212
{
1313
public class AuthenticationServiceTests
1414
{
15+
[Fact]
16+
public async Task AuthenticateThrowsForSchemeMismatch()
17+
{
18+
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
19+
{
20+
o.AddScheme<BaseHandler>("base", "whatever");
21+
}).BuildServiceProvider();
22+
var context = new DefaultHttpContext();
23+
context.RequestServices = services;
24+
25+
await context.AuthenticateAsync("base");
26+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.AuthenticateAsync("missing"));
27+
Assert.Contains("base", ex.Message);
28+
}
29+
30+
[Fact]
31+
public async Task ChallengeThrowsForSchemeMismatch()
32+
{
33+
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
34+
{
35+
o.AddScheme<BaseHandler>("base", "whatever");
36+
}).BuildServiceProvider();
37+
var context = new DefaultHttpContext();
38+
context.RequestServices = services;
39+
40+
await context.ChallengeAsync("base");
41+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.ChallengeAsync("missing"));
42+
Assert.Contains("base", ex.Message);
43+
}
44+
45+
[Fact]
46+
public async Task ForbidThrowsForSchemeMismatch()
47+
{
48+
var services = new ServiceCollection().AddOptions().AddAuthenticationCore(o =>
49+
{
50+
o.AddScheme<BaseHandler>("base", "whatever");
51+
}).BuildServiceProvider();
52+
var context = new DefaultHttpContext();
53+
context.RequestServices = services;
54+
55+
await context.ForbidAsync("base");
56+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.ForbidAsync("missing"));
57+
Assert.Contains("base", ex.Message);
58+
}
59+
1560
[Fact]
1661
public async Task CanOnlySignInIfSupported()
1762
{
@@ -26,9 +71,13 @@ public async Task CanOnlySignInIfSupported()
2671
context.RequestServices = services;
2772

2873
await context.SignInAsync("uber", new ClaimsPrincipal(), null);
29-
await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("base", new ClaimsPrincipal(), null));
74+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("base", new ClaimsPrincipal(), null));
75+
Assert.Contains("uber", ex.Message);
76+
Assert.Contains("signin", ex.Message);
3077
await context.SignInAsync("signin", new ClaimsPrincipal(), null);
31-
await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("signout", new ClaimsPrincipal(), null));
78+
ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync("signout", new ClaimsPrincipal(), null));
79+
Assert.Contains("uber", ex.Message);
80+
Assert.Contains("signin", ex.Message);
3281
}
3382

3483
[Fact]
@@ -45,7 +94,9 @@ public async Task CanOnlySignOutIfSupported()
4594
context.RequestServices = services;
4695

4796
await context.SignOutAsync("uber");
48-
await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync("base"));
97+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync("base"));
98+
Assert.Contains("uber", ex.Message);
99+
Assert.Contains("signout", ex.Message);
49100
await context.SignOutAsync("signout");
50101
await context.SignOutAsync("signin");
51102
}
@@ -64,8 +115,10 @@ public async Task ServicesWithDefaultIAuthenticationHandlerMethodsTest()
64115
await context.AuthenticateAsync();
65116
await context.ChallengeAsync();
66117
await context.ForbidAsync();
67-
await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
68-
await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
118+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignOutAsync());
119+
Assert.Contains("cannot be used for SignOutAsync", ex.Message);
120+
ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
121+
Assert.Contains("cannot be used for SignInAsync", ex.Message);
69122
}
70123

71124
[Fact]
@@ -119,7 +172,8 @@ public async Task ServicesWithDefaultSignOutMethodsTest()
119172
await context.ChallengeAsync();
120173
await context.ForbidAsync();
121174
await context.SignOutAsync();
122-
await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
175+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => context.SignInAsync(new ClaimsPrincipal()));
176+
Assert.Contains("cannot be used for SignInAsync", ex.Message);
123177
}
124178

125179
[Fact]

0 commit comments

Comments
 (0)