Skip to content

Commit c5e43cf

Browse files
authored
Add refresh token support to BearerTokenHandler (#48595)
1 parent e1c4295 commit c5e43cf

18 files changed

+504
-178
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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+
namespace Microsoft.AspNetCore.Identity.DTO;
5+
6+
internal sealed class RefreshRequest
7+
{
8+
public required string RefreshToken { get; init; }
9+
}

src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Linq;
5+
using Microsoft.AspNetCore.Authentication.BearerToken;
56
using Microsoft.AspNetCore.Authentication.BearerToken.DTO;
67
using Microsoft.AspNetCore.Builder;
78
using Microsoft.AspNetCore.Http;
@@ -10,6 +11,7 @@
1011
using Microsoft.AspNetCore.Identity;
1112
using Microsoft.AspNetCore.Identity.DTO;
1213
using Microsoft.Extensions.DependencyInjection;
14+
using Microsoft.Extensions.Options;
1315

1416
namespace Microsoft.AspNetCore.Routing;
1517

@@ -36,9 +38,9 @@ public static class IdentityApiEndpointRouteBuilderExtensions
3638
// NOTE: We cannot inject UserManager<TUser> directly because the TUser generic parameter is currently unsupported by RDG.
3739
// https://github.com/dotnet/aspnetcore/issues/47338
3840
routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
39-
([FromBody] RegisterRequest registration, [FromServices] IServiceProvider services) =>
41+
([FromBody] RegisterRequest registration, [FromServices] IServiceProvider sp) =>
4042
{
41-
var userManager = services.GetRequiredService<UserManager<TUser>>();
43+
var userManager = sp.GetRequiredService<UserManager<TUser>>();
4244

4345
var user = new TUser();
4446
await userManager.SetUserNameAsync(user, registration.Username);
@@ -53,17 +55,17 @@ public static class IdentityApiEndpointRouteBuilderExtensions
5355
});
5456

5557
routeGroup.MapPost("/login", async Task<Results<UnauthorizedHttpResult, Ok<AccessTokenResponse>, SignInHttpResult>>
56-
([FromBody] LoginRequest login, [FromQuery] bool? cookieMode, [FromServices] IServiceProvider services) =>
58+
([FromBody] LoginRequest login, [FromQuery] bool? cookieMode, [FromServices] IServiceProvider sp) =>
5759
{
58-
var userManager = services.GetRequiredService<UserManager<TUser>>();
60+
var userManager = sp.GetRequiredService<UserManager<TUser>>();
5961
var user = await userManager.FindByNameAsync(login.Username);
6062

6163
if (user is null || !await userManager.CheckPasswordAsync(user, login.Password))
6264
{
6365
return TypedResults.Unauthorized();
6466
}
6567

66-
var claimsFactory = services.GetRequiredService<IUserClaimsPrincipalFactory<TUser>>();
68+
var claimsFactory = sp.GetRequiredService<IUserClaimsPrincipalFactory<TUser>>();
6769
var claimsPrincipal = await claimsFactory.CreateAsync(user);
6870

6971
var useCookies = cookieMode ?? false;
@@ -72,6 +74,27 @@ public static class IdentityApiEndpointRouteBuilderExtensions
7274
return TypedResults.SignIn(claimsPrincipal, authenticationScheme: scheme);
7375
});
7476

77+
routeGroup.MapPost("/refresh", async Task<Results<UnauthorizedHttpResult, Ok<AccessTokenResponse>, SignInHttpResult, ChallengeHttpResult>>
78+
([FromBody] RefreshRequest refreshRequest, [FromServices] IOptionsMonitor<BearerTokenOptions> optionsMonitor, [FromServices] TimeProvider timeProvider, [FromServices] IServiceProvider sp) =>
79+
{
80+
var signInManager = sp.GetRequiredService<SignInManager<TUser>>();
81+
var identityBearerOptions = optionsMonitor.Get(IdentityConstants.BearerScheme);
82+
var refreshTokenProtector = identityBearerOptions.RefreshTokenProtector ?? throw new ArgumentException($"{nameof(identityBearerOptions.RefreshTokenProtector)} is null", nameof(optionsMonitor));
83+
var refreshTicket = refreshTokenProtector.Unprotect(refreshRequest.RefreshToken);
84+
85+
// Reject the /refresh attempt with a 401 if the token expired or the security stamp validation fails
86+
if (refreshTicket?.Properties?.ExpiresUtc is not { } expiresUtc ||
87+
timeProvider.GetUtcNow() >= expiresUtc ||
88+
await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not TUser user)
89+
90+
{
91+
return TypedResults.Challenge();
92+
}
93+
94+
var newPrincipal = await signInManager.CreateUserPrincipalAsync(user);
95+
return TypedResults.SignIn(newPrincipal, authenticationScheme: IdentityConstants.BearerScheme);
96+
});
97+
7598
return new IdentityEndpointsConventionBuilder(routeGroup);
7699
}
77100

src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs

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

src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs

Lines changed: 0 additions & 78 deletions
This file was deleted.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.Authentication.BearerToken;
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
namespace Microsoft.AspNetCore.Identity;
9+
10+
/// <summary>
11+
/// Extension methods to enable bearer token authentication for use with identity.
12+
/// </summary>
13+
public static class IdentityAuthenticationBuilderExtensions
14+
{
15+
/// <summary>
16+
/// Adds cookie authentication.
17+
/// </summary>
18+
/// <param name="builder">The current <see cref="AuthenticationBuilder"/> instance.</param>
19+
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
20+
public static AuthenticationBuilder AddIdentityBearerToken<TUser>(this AuthenticationBuilder builder)
21+
where TUser : class, new()
22+
=> builder.AddIdentityBearerToken<TUser>(o => { });
23+
24+
/// <summary>
25+
/// Adds the cookie authentication needed for sign in manager.
26+
/// </summary>
27+
/// <param name="builder">The current <see cref="AuthenticationBuilder"/> instance.</param>
28+
/// <param name="configureOptions">Action used to configure the bearer token handler.</param>
29+
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
30+
public static AuthenticationBuilder AddIdentityBearerToken<TUser>(this AuthenticationBuilder builder, Action<BearerTokenOptions> configureOptions)
31+
where TUser : class, new()
32+
{
33+
ArgumentNullException.ThrowIfNull(builder);
34+
ArgumentNullException.ThrowIfNull(configureOptions);
35+
36+
return builder.AddBearerToken(IdentityConstants.BearerScheme, configureOptions);
37+
}
38+
}

src/Identity/Core/src/IdentityBuilderExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using Microsoft.AspNetCore.Authentication;
6+
using Microsoft.AspNetCore.Authentication.BearerToken;
7+
using Microsoft.AspNetCore.Http.Json;
8+
using Microsoft.AspNetCore.Routing;
59
using Microsoft.Extensions.DependencyInjection;
610
using Microsoft.Extensions.DependencyInjection.Extensions;
711
using Microsoft.Extensions.Options;
@@ -79,6 +83,22 @@ public static IdentityBuilder AddSignInManager(this IdentityBuilder builder)
7983
return builder;
8084
}
8185

86+
/// <summary>
87+
/// Adds configuration ans services needed to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
88+
/// but does not configure authentication. Call <see cref="BearerTokenExtensions.AddBearerToken(AuthenticationBuilder, Action{BearerTokenOptions}?)"/> and/or
89+
/// <see cref="IdentityCookieAuthenticationBuilderExtensions.AddIdentityCookies(AuthenticationBuilder)"/> to configure authentication separately.
90+
/// </summary>
91+
/// <param name="builder">The <see cref="IdentityBuilder"/>.</param>
92+
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
93+
public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder)
94+
{
95+
ArgumentNullException.ThrowIfNull(builder);
96+
97+
builder.AddSignInManager();
98+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JsonOptions>, IdentityEndpointsJsonOptionsSetup>());
99+
return builder;
100+
}
101+
82102
// Set TimeProvider from DI on all options instances, if not already set by tests.
83103
private sealed class PostConfigureSecurityStampValidatorOptions : IPostConfigureOptions<SecurityStampValidatorOptions>
84104
{

src/Identity/Core/src/IdentityServiceCollectionExtensions.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics.CodeAnalysis;
5+
using System.Security.Claims;
6+
using System.Text.Encodings.Web;
7+
using Microsoft.AspNetCore.Authentication;
58
using Microsoft.AspNetCore.Authentication.Cookies;
69
using Microsoft.AspNetCore.Http;
710
using Microsoft.AspNetCore.Identity;
11+
using Microsoft.AspNetCore.Routing;
812
using Microsoft.Extensions.DependencyInjection.Extensions;
13+
using Microsoft.Extensions.Logging;
914
using Microsoft.Extensions.Options;
1015

1116
namespace Microsoft.Extensions.DependencyInjection;
@@ -109,6 +114,47 @@ public static class IdentityServiceCollectionExtensions
109114
return new IdentityBuilder(typeof(TUser), typeof(TRole), services);
110115
}
111116

117+
/// <summary>
118+
/// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
119+
/// and configures authentication to support identity bearer tokens and cookies.
120+
/// </summary>
121+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
122+
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
123+
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
124+
where TUser : class, new()
125+
=> services.AddIdentityApiEndpoints<TUser>(_ => { });
126+
127+
/// <summary>
128+
/// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
129+
/// and configures authentication to support identity bearer tokens and cookies.
130+
/// </summary>
131+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
132+
/// <param name="configure">Configures the <see cref="IdentityOptions"/>.</param>
133+
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
134+
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
135+
where TUser : class, new()
136+
{
137+
ArgumentNullException.ThrowIfNull(services);
138+
ArgumentNullException.ThrowIfNull(configure);
139+
140+
services
141+
.AddAuthentication(IdentityConstants.BearerAndApplicationScheme)
142+
.AddScheme<AuthenticationSchemeOptions, CompositeIdentityHandler>(IdentityConstants.BearerAndApplicationScheme, null, compositeOptions =>
143+
{
144+
compositeOptions.ForwardDefault = IdentityConstants.BearerScheme;
145+
compositeOptions.ForwardAuthenticate = IdentityConstants.BearerAndApplicationScheme;
146+
})
147+
.AddIdentityBearerToken<TUser>()
148+
.AddIdentityCookies();
149+
150+
return services.AddIdentityCore<TUser>(o =>
151+
{
152+
o.Stores.MaxLengthForKeys = 128;
153+
configure(o);
154+
})
155+
.AddApiEndpoints();
156+
}
157+
112158
/// <summary>
113159
/// Configures the application cookie.
114160
/// </summary>
@@ -141,4 +187,32 @@ public void PostConfigure(string? name, SecurityStampValidatorOptions options)
141187
options.TimeProvider ??= TimeProvider;
142188
}
143189
}
190+
191+
private sealed class CompositeIdentityHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder)
192+
: SignInAuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
193+
{
194+
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
195+
{
196+
var bearerResult = await Context.AuthenticateAsync(IdentityConstants.BearerScheme);
197+
198+
// Only try to authenticate with the application cookie if there is no bearer token.
199+
if (!bearerResult.None)
200+
{
201+
return bearerResult;
202+
}
203+
204+
// Cookie auth will return AuthenticateResult.NoResult() like bearer auth just did if there is no cookie.
205+
return await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme);
206+
}
207+
208+
protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties)
209+
{
210+
throw new NotImplementedException();
211+
}
212+
213+
protected override Task HandleSignOutAsync(AuthenticationProperties? properties)
214+
{
215+
throw new NotImplementedException();
216+
}
217+
}
144218
}

0 commit comments

Comments
 (0)