Skip to content

Commit 9411969

Browse files
committed
AddIdentityBearer + IdentityBearerOptions.ExtractBearerToken
- Extra API cleanup
1 parent 5d2b17e commit 9411969

11 files changed

+261
-180
lines changed

src/Identity/Endpoints/src/ConfigureIdentityEndpointJsonOptions.cs

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/Identity/Endpoints/src/DTO/IdentityEndpointJsonSerializerContext.cs renamed to src/Identity/Endpoints/src/DTO/IdentityEndpointsJsonSerializerContext.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ namespace Microsoft.AspNetCore.Identity.Endpoints.DTO;
88
[JsonSerializable(typeof(RegisterRequest))]
99
[JsonSerializable(typeof(LoginRequest))]
1010
[JsonSerializable(typeof(AccessTokenResponse))]
11-
internal sealed partial class IdentityEndpointJsonSerializerContext : JsonSerializerContext
11+
internal sealed partial class IdentityEndpointsJsonSerializerContext : JsonSerializerContext
1212
{
1313
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Authentication;
5+
using Microsoft.AspNetCore.Identity;
6+
using Microsoft.AspNetCore.Identity.Endpoints;
7+
using Microsoft.AspNetCore.Routing;
8+
9+
namespace Microsoft.Extensions.DependencyInjection;
10+
11+
/// <summary>
12+
/// Extension methods to configure the identity bearer token authentication used by <see cref="IdentityEndpointRouteBuilderExtensions.MapIdentity{TUser}(IEndpointRouteBuilder)"/>.
13+
/// </summary>
14+
public static class IdentityBearerAuthenticationBuilderExtensions
15+
{
16+
/// <summary>
17+
/// Adds identity bearer token authentication. The default scheme is specified by <see cref="IdentityConstants.BearerScheme"/>.
18+
/// <para>
19+
/// Identity bearer tokens can be obtained from endpoints added by <see cref="IdentityEndpointRouteBuilderExtensions.MapIdentity{TUser}(IEndpointRouteBuilder)"/>.
20+
/// </para>
21+
/// </summary>
22+
/// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
23+
/// <param name="configure">Action used to configure the identity bearer token authentication options.</param>
24+
/// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns>
25+
public static AuthenticationBuilder AddIdentityBearer(this AuthenticationBuilder builder, Action<IdentityBearerOptions>? configure)
26+
=> builder.AddScheme<IdentityBearerOptions, IdentityBearerAuthenticationHandler>(IdentityConstants.BearerScheme, configure);
27+
}

src/Identity/Endpoints/src/IdentityBearerAuthenticationHandler.cs

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@
1313

1414
namespace Microsoft.AspNetCore.Identity.Endpoints;
1515

16-
internal sealed class IdentityBearerAuthenticationHandler : SignInAuthenticationHandler<IdentityBearerAuthenticationOptions>
16+
internal sealed class IdentityBearerAuthenticationHandler : SignInAuthenticationHandler<IdentityBearerOptions>
1717
{
1818
private const string BearerTokenPurpose = $"Microsoft.AspNetCore.Identity.Endpoints.IdentityBearerAuthenticationHandler:v1:BearerToken";
1919

20-
private static readonly Task<AuthenticateResult> TokenMissingTask = Task.FromResult(AuthenticateResult.Fail("Token missing"));
21-
private static readonly Task<AuthenticateResult> FailedUnprotectingTokenTask = Task.FromResult(AuthenticateResult.Fail("Unprotect token failed"));
22-
private static readonly Task<AuthenticateResult> TokenExpiredTask = Task.FromResult(AuthenticateResult.Fail("Token expired"));
20+
private static readonly AuthenticateResult TokenMissing = AuthenticateResult.Fail("Token missing");
21+
private static readonly AuthenticateResult FailedUnprotectingToken = AuthenticateResult.Fail("Unprotected token failed");
22+
private static readonly AuthenticateResult TokenExpired = AuthenticateResult.Fail("Token expired");
2323

2424
private readonly IDataProtectionProvider _fallbackDataProtectionProvider;
2525

2626
public IdentityBearerAuthenticationHandler(
27-
IOptionsMonitor<IdentityBearerAuthenticationOptions> optionsMonitor,
27+
IOptionsMonitor<IdentityBearerOptions> optionsMonitor,
2828
ILoggerFactory loggerFactory,
2929
UrlEncoder urlEncoder,
3030
ISystemClock clock,
@@ -40,43 +40,60 @@ private IDataProtectionProvider DataProtectionProvider
4040
private ISecureDataFormat<AuthenticationTicket> BearerTokenProtector
4141
=> Options.BearerTokenProtector ?? new TicketDataFormat(DataProtectionProvider.CreateProtector(BearerTokenPurpose));
4242

43-
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
43+
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
4444
{
4545
// If there's no bearer token, forward to cookie auth.
46-
if (GetBearerTokenOrNull() is not string token)
46+
if (await GetBearerTokenOrNullAsync() is not string token)
4747
{
48-
return Options.BearerTokenMissingFallbackScheme is string fallbackScheme
49-
? Context.AuthenticateAsync(fallbackScheme)
50-
: TokenMissingTask;
48+
return Options.MissingBearerTokenFallbackScheme is string fallbackScheme
49+
? await Context.AuthenticateAsync(fallbackScheme)
50+
: TokenMissing;
5151
}
5252

5353
var ticket = BearerTokenProtector.Unprotect(token);
5454

5555
if (ticket?.Properties?.ExpiresUtc is null)
5656
{
57-
return FailedUnprotectingTokenTask;
57+
return FailedUnprotectingToken;
5858
}
5959

6060
if (Clock.UtcNow >= ticket.Properties.ExpiresUtc)
6161
{
62-
return TokenExpiredTask;
62+
return TokenExpired;
6363
}
6464

65-
return Task.FromResult(AuthenticateResult.Success(ticket));
65+
return AuthenticateResult.Success(ticket);
6666
}
6767

68-
protected override Task HandleChallengeAsync(AuthenticationProperties properties)
68+
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
6969
{
7070
// If there's no bearer token, forward to cookie auth.
71-
if (GetBearerTokenOrNull() is null)
71+
if (await GetBearerTokenOrNullAsync() is null)
7272
{
73-
return Options.BearerTokenMissingFallbackScheme is string fallbackScheme
74-
? Context.AuthenticateAsync(fallbackScheme)
75-
: TokenMissingTask;
73+
if (Options.MissingBearerTokenFallbackScheme is string fallbackScheme)
74+
{
75+
await Context.ForbidAsync(fallbackScheme);
76+
return;
77+
}
78+
}
79+
80+
await base.HandleForbiddenAsync(properties);
81+
}
82+
83+
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
84+
{
85+
// If there's no bearer token, forward to cookie auth.
86+
if (await GetBearerTokenOrNullAsync() is null)
87+
{
88+
if (Options.MissingBearerTokenFallbackScheme is string fallbackScheme)
89+
{
90+
await Context.ChallengeAsync(fallbackScheme);
91+
return;
92+
}
7693
}
7794

7895
Response.Headers.Append(HeaderNames.WWWAuthenticate, "Bearer");
79-
return base.HandleChallengeAsync(properties);
96+
await base.HandleChallengeAsync(properties);
8097
}
8198

8299
protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties)
@@ -97,11 +114,17 @@ protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationPr
97114
protected override Task HandleSignOutAsync(AuthenticationProperties? properties)
98115
=> throw new NotSupportedException($"""
99116
Sign out is not currently supported by identity bearer tokens.
100-
If you want to delete cookies or clear a session, specify "{Options.BearerTokenMissingFallbackScheme}" as the authentication scheme.
117+
If you want to delete cookies or clear a session, specify "{IdentityConstants.ApplicationScheme}" as the authentication scheme.
101118
""");
102119

103-
private string? GetBearerTokenOrNull()
120+
private async ValueTask<string?> GetBearerTokenOrNullAsync()
104121
{
122+
if (Options.ExtractBearerToken is not null &&
123+
await Options.ExtractBearerToken(Context) is string token)
124+
{
125+
return token;
126+
}
127+
105128
var authorization = Request.Headers.Authorization.ToString();
106129

107130
return authorization.StartsWith("Bearer ", StringComparison.Ordinal)

src/Identity/Endpoints/src/IdentityBearerSchemeOptions.cs renamed to src/Identity/Endpoints/src/IdentityBearerOptions.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33

44
using Microsoft.AspNetCore.Authentication;
55
using Microsoft.AspNetCore.DataProtection;
6+
using Microsoft.AspNetCore.Http;
67
using Microsoft.AspNetCore.Routing;
8+
using Microsoft.Extensions.DependencyInjection;
79

810
namespace Microsoft.AspNetCore.Identity.Endpoints;
911

1012
/// <summary>
1113
/// Contains the options used to authenticate using bearer tokens issued by <see cref="IdentityEndpointRouteBuilderExtensions.MapIdentity{TUser}(IEndpointRouteBuilder)"/>.
1214
/// </summary>
13-
public sealed class IdentityBearerAuthenticationOptions : AuthenticationSchemeOptions
15+
public sealed class IdentityBearerOptions : AuthenticationSchemeOptions
1416
{
1517
/// <summary>
1618
/// Controls how much time the bearer token will remain valid from the point it is created.
@@ -31,8 +33,15 @@ public sealed class IdentityBearerAuthenticationOptions : AuthenticationSchemeOp
3133
public IDataProtectionProvider? DataProtectionProvider { get; set; }
3234

3335
/// <summary>
34-
/// If set, authentication and challenges will be forwarded to this scheme only if the request does not contain a bearer token.
35-
/// This is typically set to Usually Identity.Application cookies <see cref="IdentityConstants.ApplicationScheme"/>
36+
/// If set, authentication will be forwarded to this scheme only if the request does not contain a bearer token.
37+
/// This is typically set to <see cref="IdentityConstants.ApplicationScheme"/> ("Identity.Application") the for identity cookies by
38+
/// <see cref="IdentityEndpointsServiceCollectionExtensions.AddIdentityEndpoints{TUser}(IServiceCollection)"/>.
3639
/// </summary>
37-
public string? BearerTokenMissingFallbackScheme { get; set; }
40+
public string? MissingBearerTokenFallbackScheme { get; set; }
41+
42+
/// <summary>
43+
/// If set, this provides the bearer token. If unset, the bearer token is read from the Authorization request header with a "Bearer " prefix.
44+
/// </summary>
45+
public Func<HttpContext, ValueTask<string?>>? ExtractBearerToken { get; set; }
3846
}
47+

src/Identity/Endpoints/src/IdentityEndpointRouteBuilderExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ public static class IdentityEndpointRouteBuilderExtensions
7272
return TypedResults.SignIn(claimsPrincipal, authenticationScheme: scheme);
7373
});
7474

75-
return new IdentityEndpointConventionBuilder(v1);
75+
return new IdentityEndpointsConventionBuilder(v1);
7676
}
7777

7878
// Wrap RouteGroupBuilder with a non-public type to avoid a potential future behavioral breaking change.
79-
private sealed class IdentityEndpointConventionBuilder(RouteGroupBuilder inner) : IEndpointConventionBuilder
79+
private sealed class IdentityEndpointsConventionBuilder(RouteGroupBuilder inner) : IEndpointConventionBuilder
8080
{
8181
private readonly IEndpointConventionBuilder _inner = inner;
8282

src/Identity/Endpoints/src/IdentityEndpointServiceCollectionExtensions.cs

Lines changed: 0 additions & 110 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Http.Json;
5+
using Microsoft.AspNetCore.Identity.Endpoints.DTO;
6+
using Microsoft.Extensions.Options;
7+
8+
namespace Microsoft.AspNetCore.Identity.Endpoints;
9+
10+
internal sealed class IdentityEndpointsJsonOptionsSetup : IConfigureOptions<JsonOptions>
11+
{
12+
public void Configure(JsonOptions options)
13+
{
14+
// Put our resolver in front of the reflection-based one. See ProblemDetailsOptionsSetup for a detailed explanation.
15+
options.SerializerOptions.TypeInfoResolverChain.Insert(0, IdentityEndpointsJsonSerializerContext.Default);
16+
}
17+
}

0 commit comments

Comments
 (0)