-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Add token refresh endpoints to identity #47228
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
|
API Review Notes:
We ran out of time before we could discuss more minor details like where the AuthenticationProperties.RefereshToken should live (assuming we keep passing the refresh token to sign in this way). I'll experiment with extracting the |
Updated API Proposal: Proposed API// Microsoft.AspNetCore.Identity.dll
namespace Microsoft.Extensions.DependencyInjection;
- public static class IdentityApiEndpointsServiceCollectionExtensions
- {
- public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
- where TUser : class, new();
- public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
- where TUser : class, new();
- }
public static class IdentityServiceCollectionExtensions
{
+ public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
+ where TUser : class, new();
+ public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
+ where TUser : class, new();
}
namespace Microsoft.AspNetCore.Identity;
- public static class IdentityApiEndpointsIdentityBuilderExtensions
- {
- public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder);
- }
public static class IdentityBuilderExtensions
{
+ public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder);
}
+ public static class IdentityAuthenticationBuilderExtensions
+ {
+ public static AuthenticationBuilder AddIdentityBearerToken<TUser>(this AuthenticationBuilder builder)
+ where TUser : class, new()
+ public static AuthenticationBuilder AddIdentityBearerToken<TUser>(this AuthenticationBuilder builder, Action<BearerTokenOptions> configure)
+ where TUser : class, new()
+ } // Microsoft.AspNetCore.Authentication.BearerToken.dll
namespace Microsoft.AspNetCore.Authentication.BearerToken;
public sealed class BearerTokenOptions : AuthenticationSchemeOptions
{
public TimeSpan BearerTokenExpiration { get; set; } = TimeSpan.FromHours(1);
+ public TimeSpan RefreshTokenExpiration { get; set; } = TimeSpan.FromDays(14);
public ISecureDataFormat<AuthenticationTicket>? BearerTokenProtector { get; set; }
+ public ISecureDataFormat<AuthenticationTicket>? RefreshTokenProtector { get; set; }
} Usage examplesThe implementation of app.MapPost("/refresh", async (RefreshRequest refreshRequest, IOptionsMonitor<BearerTokenOptions> optionsMonitor, TimeProvider timeProvider, SignInManager<TUser> signInManager) =>
{
var identityBearerOptions = optionsMonitor.Get(IdentityConstants.BearerAndApplicationScheme);
var refreshTokenProtector = identityBearerOptions.RefreshTokenProtector ?? throw new ArgumentException();
var refreshTicket = refreshTokenProtector.Unprotect(refreshRequest.RefreshToken);
if (refreshTicket?.Properties?.ExpiresUtc is not { } expiresUtc || timeProvider.GetUtcNow() >= expiresUtc)
{
return TypedResults.Challenge();
}
// Reject the /refresh attempt if the security stamp validation fails which will result in a 401 challenge.
if (await signInManager.ValidateSecurityStampAsync(refreshTicket.Principal) is not TUser user)
{
return TypedResults.Challenge();
}
var newPrincipal = await signInManager.CreateUserPrincipalAsync(user);
return TypedResults.SignIn(newPrincipal, authenticationScheme: IdentityConstants.BearerScheme);
}); Alternative Designs
|
Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:
|
API Review Note:
API Approved! // Microsoft.AspNetCore.Identity.dll
namespace Microsoft.Extensions.DependencyInjection;
- public static class IdentityApiEndpointsServiceCollectionExtensions
- {
- public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
- where TUser : class, new();
- public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
- where TUser : class, new();
- }
public static class IdentityServiceCollectionExtensions
{
+ public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
+ where TUser : class, new();
+ public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
+ where TUser : class, new();
}
namespace Microsoft.AspNetCore.Identity;
- public static class IdentityApiEndpointsIdentityBuilderExtensions
- {
- public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder);
- }
public static class IdentityBuilderExtensions
{
+ public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder);
}
+ public static class IdentityAuthenticationBuilderExtensions
+ {
+ public static AuthenticationBuilder AddIdentityBearerToken<TUser>(this AuthenticationBuilder builder)
+ where TUser : class, new()
+ public static AuthenticationBuilder AddIdentityBearerToken<TUser>(this AuthenticationBuilder builder, Action<BearerTokenOptions> configure)
+ where TUser : class, new()
+ } // Microsoft.AspNetCore.Authentication.BearerToken.dll
namespace Microsoft.AspNetCore.Authentication.BearerToken;
public sealed class BearerTokenOptions : AuthenticationSchemeOptions
{
public TimeSpan BearerTokenExpiration { get; set; } = TimeSpan.FromHours(1);
+ public TimeSpan RefreshTokenExpiration { get; set; } = TimeSpan.FromDays(14);
- public ISecureDataFormat<AuthenticationTicket>? BearerTokenProtector { get; set; }
+ public ISecureDataFormat<AuthenticationTicket> BearerTokenProtector { get; set; }
+ public ISecureDataFormat<AuthenticationTicket> RefreshTokenProtector { get; set; }
} |
Very nice! |
Background and Motivation
The default timeout for bearer tokens in the BearerTokenHandler is one hour, and forcing a new login every hour is not acceptable. Unlike with cookies, we cannot implicitly refresh the bearer token by attaching an outgoing header to authenticated request to non-Identity endpoints, so we need to add a refresh token and a new /refresh endpoint that accepts the refresh_token as part of the JSON request body and returns a new bearer token and new refresh token identical to the initial /login response.
As part of this process, when paired with Identity, it will verify the security stamp (if any) and reload the ClaimsPrincipal from the database via the user store. Identity does this via a
BearerTokenOptions.OnSigningIn
callback.Proposed API
Usage Examples
In the default cookie + bearer token case where you call the
AddIdentityApiEndpoints
IServiceCollection
extension method, the server code is unchanged from the #47227 example modulo the API renames.But now, if you want to enable only bearer tokens but not cookies, you should call the new
AddIdentityBearerToken<TUser>()
AuthenticationBuilder
extension method rather than callingAddBearerToken(IdentityConstants.BearerScheme)
like before so that identity can wire up the security stamp validation and the user store reloading during refresh as part of theBearerTokenOptions.OnSigningIn
callback.If you want to manually refresh the access token with a refresh token without the
MapIdentityApi
endpoints, you can callHttpContext.SignIn
yourself withAuthenticationProperties.RefreshToken
set:And if you manually want to do something like security stamp validation, or updating the ClaimsPrincipal to something different than when it was created, you can hook
OnSigningIn
manually:Or the OnSigningIn can go full manual mode and create the tokens itself:
Client
Assume
httpClient
,username
andpassword
are already initialized.Alternative Designs
We could create a new derived class for
AuthenticationProperties
similar toOAuthChallengeProperties
orGoogleChallengeProperties
calledBearerTokenProperties
as suggested by @kevinchalet in the PR at https://github.com/dotnet/aspnetcore/pull/48595/files#r1220367158. We'd probably want to add apublic static readonly string RefereshTokenKey
for consistency and to be able to look for tokens on arbitraryAuthenticationProperties
.We could then consider adding
public new BearerTokenProperties Properties { get; set; }
toSigningInContext
to hide the property with the same name from theProperitesContext
grandparent type, but that'd probably require at least shallow copying the properties from a passed-inAuthenticationProperties
. The other handlers with custom properties don't appear to do this.We could consider not approving
SigningInContext.AccessToken
andSigningInContext.RefreshToken
and say the one true way to customize tokens is withBearerTokenOptions.TokenProtector
. It's a little less flexible because you don't have easy access to the HttpContext in anISecureDataFormat<AuthenticationTicket>
, but identity doesn't need this functionality. We could always add it later.Risks
I think the risk is minimal. This is all either moving API added in preview4 or adding new API to support the refresh token scenario.
The text was updated successfully, but these errors were encountered: