Skip to content

Add refresh token support to BearerTokenHandler #48595

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

Merged
merged 3 commits into from
Jun 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/Identity/Core/src/DTO/RefreshRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.AspNetCore.Identity.DTO;

internal sealed class RefreshRequest
{
public required string RefreshToken { get; init; }
}
33 changes: 28 additions & 5 deletions src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using Microsoft.AspNetCore.Authentication.BearerToken;
using Microsoft.AspNetCore.Authentication.BearerToken.DTO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
Expand All @@ -10,6 +11,7 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.DTO;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Routing;

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

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

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

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

var claimsFactory = services.GetRequiredService<IUserClaimsPrincipalFactory<TUser>>();
var claimsFactory = sp.GetRequiredService<IUserClaimsPrincipalFactory<TUser>>();
var claimsPrincipal = await claimsFactory.CreateAsync(user);

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

routeGroup.MapPost("/refresh", async Task<Results<UnauthorizedHttpResult, Ok<AccessTokenResponse>, SignInHttpResult, ChallengeHttpResult>>
([FromBody] RefreshRequest refreshRequest, [FromServices] IOptionsMonitor<BearerTokenOptions> optionsMonitor, [FromServices] TimeProvider timeProvider, [FromServices] IServiceProvider sp) =>
{
var signInManager = sp.GetRequiredService<SignInManager<TUser>>();
var identityBearerOptions = optionsMonitor.Get(IdentityConstants.BearerScheme);
var refreshTokenProtector = identityBearerOptions.RefreshTokenProtector ?? throw new ArgumentException($"{nameof(identityBearerOptions.RefreshTokenProtector)} is null", nameof(optionsMonitor));
var refreshTicket = refreshTokenProtector.Unprotect(refreshRequest.RefreshToken);

// Reject the /refresh attempt with a 401 if the token expired or the security stamp validation fails
if (refreshTicket?.Properties?.ExpiresUtc is not { } expiresUtc ||
timeProvider.GetUtcNow() >= expiresUtc ||
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);
});

return new IdentityEndpointsConventionBuilder(routeGroup);
}

Expand Down

This file was deleted.

This file was deleted.

38 changes: 38 additions & 0 deletions src/Identity/Core/src/IdentityAuthenticationBuilderExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.BearerToken;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Identity;

/// <summary>
/// Extension methods to enable bearer token authentication for use with identity.
/// </summary>
public static class IdentityAuthenticationBuilderExtensions
{
/// <summary>
/// Adds cookie authentication.
/// </summary>
/// <param name="builder">The current <see cref="AuthenticationBuilder"/> instance.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddIdentityBearerToken<TUser>(this AuthenticationBuilder builder)
where TUser : class, new()
=> builder.AddIdentityBearerToken<TUser>(o => { });

/// <summary>
/// Adds the cookie authentication needed for sign in manager.
/// </summary>
/// <param name="builder">The current <see cref="AuthenticationBuilder"/> instance.</param>
/// <param name="configureOptions">Action used to configure the bearer token handler.</param>
/// <returns>The <see cref="AuthenticationBuilder"/>.</returns>
public static AuthenticationBuilder AddIdentityBearerToken<TUser>(this AuthenticationBuilder builder, Action<BearerTokenOptions> configureOptions)
where TUser : class, new()
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(configureOptions);

return builder.AddBearerToken(IdentityConstants.BearerScheme, configureOptions);
}
}
20 changes: 20 additions & 0 deletions src/Identity/Core/src/IdentityBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.BearerToken;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -79,6 +83,22 @@ public static IdentityBuilder AddSignInManager(this IdentityBuilder builder)
return builder;
}

/// <summary>
/// Adds configuration ans services needed to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
/// but does not configure authentication. Call <see cref="BearerTokenExtensions.AddBearerToken(AuthenticationBuilder, Action{BearerTokenOptions}?)"/> and/or
/// <see cref="IdentityCookieAuthenticationBuilderExtensions.AddIdentityCookies(AuthenticationBuilder)"/> to configure authentication separately.
/// </summary>
/// <param name="builder">The <see cref="IdentityBuilder"/>.</param>
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);

builder.AddSignInManager();
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JsonOptions>, IdentityEndpointsJsonOptionsSetup>());
return builder;
}

// Set TimeProvider from DI on all options instances, if not already set by tests.
private sealed class PostConfigureSecurityStampValidatorOptions : IPostConfigureOptions<SecurityStampValidatorOptions>
{
Expand Down
74 changes: 74 additions & 0 deletions src/Identity/Core/src/IdentityServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

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

/// <summary>
/// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
/// and configures authentication to support identity bearer tokens and cookies.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
where TUser : class, new()
=> services.AddIdentityApiEndpoints<TUser>(_ => { });

/// <summary>
/// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
/// and configures authentication to support identity bearer tokens and cookies.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="configure">Configures the <see cref="IdentityOptions"/>.</param>
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
where TUser : class, new()
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(configure);

services
.AddAuthentication(IdentityConstants.BearerAndApplicationScheme)
.AddScheme<AuthenticationSchemeOptions, CompositeIdentityHandler>(IdentityConstants.BearerAndApplicationScheme, null, compositeOptions =>
{
compositeOptions.ForwardDefault = IdentityConstants.BearerScheme;
compositeOptions.ForwardAuthenticate = IdentityConstants.BearerAndApplicationScheme;
})
.AddIdentityBearerToken<TUser>()
.AddIdentityCookies();

return services.AddIdentityCore<TUser>(o =>
{
o.Stores.MaxLengthForKeys = 128;
configure(o);
})
.AddApiEndpoints();
}

/// <summary>
/// Configures the application cookie.
/// </summary>
Expand Down Expand Up @@ -141,4 +187,32 @@ public void PostConfigure(string? name, SecurityStampValidatorOptions options)
options.TimeProvider ??= TimeProvider;
}
}

private sealed class CompositeIdentityHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder)
: SignInAuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var bearerResult = await Context.AuthenticateAsync(IdentityConstants.BearerScheme);

// Only try to authenticate with the application cookie if there is no bearer token.
if (!bearerResult.None)
{
return bearerResult;
}

// Cookie auth will return AuthenticateResult.NoResult() like bearer auth just did if there is no cookie.
return await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme);
}

protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties)
{
throw new NotImplementedException();
}

protected override Task HandleSignOutAsync(AuthenticationProperties? properties)
{
throw new NotImplementedException();
}
}
}
Loading