Skip to content

Commit bd5ffbe

Browse files
committed
Add refresh token support to BearerTokenHandler
- Integrate with identity to check security stamp and refresh user from store
1 parent 2806968 commit bd5ffbe

19 files changed

+580
-159
lines changed

src/Http/Authentication.Abstractions/src/AuthenticationProperties.cs

+10
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class AuthenticationProperties
1616
internal const string IsPersistentKey = ".persistent";
1717
internal const string RedirectUriKey = ".redirect";
1818
internal const string RefreshKey = ".refresh";
19+
internal const string RefreshTokenKey = ".refreshToken";
1920
internal const string UtcDateTimeFormat = "r";
2021

2122
/// <summary>
@@ -116,6 +117,15 @@ public bool? AllowRefresh
116117
set => SetBool(RefreshKey, value);
117118
}
118119

120+
/// <summary>
121+
/// If set, the token must be valid for sign in to continue.
122+
/// </summary>
123+
public string? RefreshToken
124+
{
125+
get => GetParameter<string?>(RefreshTokenKey);
126+
set => SetParameter<string?>(RefreshTokenKey, value);
127+
}
128+
119129
/// <summary>
120130
/// Get a string value from the <see cref="Items"/> collection.
121131
/// </summary>

src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
Microsoft.AspNetCore.Authentication.AuthenticationFailureException
33
Microsoft.AspNetCore.Authentication.AuthenticationFailureException.AuthenticationFailureException(string? message) -> void
44
Microsoft.AspNetCore.Authentication.AuthenticationFailureException.AuthenticationFailureException(string? message, System.Exception? innerException) -> void
5+
Microsoft.AspNetCore.Authentication.AuthenticationProperties.RefreshToken.get -> string?
6+
Microsoft.AspNetCore.Authentication.AuthenticationProperties.RefreshToken.set -> void
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

+17
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Linq;
5+
using System.Security.Claims;
6+
using Microsoft.AspNetCore.Authentication;
57
using Microsoft.AspNetCore.Authentication.BearerToken.DTO;
68
using Microsoft.AspNetCore.Builder;
79
using Microsoft.AspNetCore.Http;
@@ -18,6 +20,7 @@ namespace Microsoft.AspNetCore.Routing;
1820
/// </summary>
1921
public static class IdentityApiEndpointRouteBuilderExtensions
2022
{
23+
2124
/// <summary>
2225
/// Add endpoints for registering, logging in, and logging out using ASP.NET Core Identity.
2326
/// </summary>
@@ -72,6 +75,20 @@ public static class IdentityApiEndpointRouteBuilderExtensions
7275
return TypedResults.SignIn(claimsPrincipal, authenticationScheme: scheme);
7376
});
7477

78+
routeGroup.MapPost("/refresh", Results<UnauthorizedHttpResult, Ok<AccessTokenResponse>, SignInHttpResult>
79+
([FromBody] RefreshRequest refreshRequest) =>
80+
{
81+
// This is the minimal principal that IsAuthenticated. The BearerTokenHander will recreate the full principal
82+
// from the refresh token if it is able. The sign in will fail without an identity name.
83+
var refreshPrincipal = new ClaimsPrincipal(new ClaimsIdentity(IdentityConstants.BearerScheme));
84+
var properties = new AuthenticationProperties
85+
{
86+
RefreshToken = refreshRequest.RefreshToken
87+
};
88+
89+
return TypedResults.SignIn(refreshPrincipal, properties, IdentityConstants.BearerScheme);
90+
});
91+
7592
return new IdentityEndpointsConventionBuilder(routeGroup);
7693
}
7794

src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs

-34
This file was deleted.

src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs

-78
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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, bearerOptions =>
37+
{
38+
bearerOptions.Events.OnSigningIn = HandleSigningIn<TUser>;
39+
configureOptions(bearerOptions);
40+
});
41+
}
42+
43+
private static async Task HandleSigningIn<TUser>(SigningInContext signInContext)
44+
where TUser : class, new()
45+
{
46+
// Only validate the security stamp and refresh the user from the store during /refresh
47+
// not during the initial /login when the Principal is already newly created from the store.
48+
if (signInContext.Properties.RefreshToken is null)
49+
{
50+
return;
51+
}
52+
53+
var signInManager = signInContext.HttpContext.RequestServices.GetRequiredService<SignInManager<TUser>>();
54+
55+
// Reject the /refresh attempt if the security stamp validation fails which will result in a 401 challenge.
56+
if (await signInManager.ValidateSecurityStampAsync(signInContext.Principal) is not TUser user)
57+
{
58+
signInContext.Principal = null;
59+
return;
60+
}
61+
62+
signInContext.Principal = await signInManager.CreateUserPrincipalAsync(user);
63+
}
64+
}

src/Identity/Core/src/IdentityBuilderExtensions.cs

+20
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

+74
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)