diff --git a/src/Identity/Core/src/DTO/RefreshRequest.cs b/src/Identity/Core/src/DTO/RefreshRequest.cs new file mode 100644 index 000000000000..d435822a6ad1 --- /dev/null +++ b/src/Identity/Core/src/DTO/RefreshRequest.cs @@ -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; } +} diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs index 11a40a5e74be..c65182c1aa2f 100644 --- a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs +++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs @@ -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; @@ -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; @@ -36,9 +38,9 @@ public static class IdentityApiEndpointRouteBuilderExtensions // NOTE: We cannot inject UserManager directly because the TUser generic parameter is currently unsupported by RDG. // https://github.com/dotnet/aspnetcore/issues/47338 routeGroup.MapPost("/register", async Task> - ([FromBody] RegisterRequest registration, [FromServices] IServiceProvider services) => + ([FromBody] RegisterRequest registration, [FromServices] IServiceProvider sp) => { - var userManager = services.GetRequiredService>(); + var userManager = sp.GetRequiredService>(); var user = new TUser(); await userManager.SetUserNameAsync(user, registration.Username); @@ -53,9 +55,9 @@ public static class IdentityApiEndpointRouteBuilderExtensions }); routeGroup.MapPost("/login", async Task, SignInHttpResult>> - ([FromBody] LoginRequest login, [FromQuery] bool? cookieMode, [FromServices] IServiceProvider services) => + ([FromBody] LoginRequest login, [FromQuery] bool? cookieMode, [FromServices] IServiceProvider sp) => { - var userManager = services.GetRequiredService>(); + var userManager = sp.GetRequiredService>(); var user = await userManager.FindByNameAsync(login.Username); if (user is null || !await userManager.CheckPasswordAsync(user, login.Password)) @@ -63,7 +65,7 @@ public static class IdentityApiEndpointRouteBuilderExtensions return TypedResults.Unauthorized(); } - var claimsFactory = services.GetRequiredService>(); + var claimsFactory = sp.GetRequiredService>(); var claimsPrincipal = await claimsFactory.CreateAsync(user); var useCookies = cookieMode ?? false; @@ -72,6 +74,27 @@ public static class IdentityApiEndpointRouteBuilderExtensions return TypedResults.SignIn(claimsPrincipal, authenticationScheme: scheme); }); + routeGroup.MapPost("/refresh", async Task, SignInHttpResult, ChallengeHttpResult>> + ([FromBody] RefreshRequest refreshRequest, [FromServices] IOptionsMonitor optionsMonitor, [FromServices] TimeProvider timeProvider, [FromServices] IServiceProvider sp) => + { + var signInManager = sp.GetRequiredService>(); + 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); } diff --git a/src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs deleted file mode 100644 index c8dc72997a5f..000000000000 --- a/src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -// 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.AspNetCore.Http.Json; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Identity; - -/// -/// extension methods to support . -/// -public static class IdentityApiEndpointsIdentityBuilderExtensions -{ - /// - /// Adds configuration ans services needed to support - /// but does not configure authentication. Call and/or - /// to configure authentication separately. - /// - /// The . - /// The . - public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder) - { - ArgumentNullException.ThrowIfNull(builder); - - builder.AddSignInManager(); - builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, IdentityEndpointsJsonOptionsSetup>()); - return builder; - } -} diff --git a/src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs deleted file mode 100644 index e24ba288c345..000000000000 --- a/src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Microsoft.Extensions.DependencyInjection; - -/// -/// Default extensions to for . -/// -public static class IdentityApiEndpointsServiceCollectionExtensions -{ - /// - /// Adds a set of common identity services to the application to support - /// and configures authentication to support identity bearer tokens and cookies. - /// - /// The . - /// The . - [RequiresUnreferencedCode("Authentication middleware does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")] - public static IdentityBuilder AddIdentityApiEndpoints(this IServiceCollection services) - where TUser : class, new() - => services.AddIdentityApiEndpoints(_ => { }); - - /// - /// Adds a set of common identity services to the application to support - /// and configures authentication to support identity bearer tokens and cookies. - /// - /// The . - /// Configures the . - /// The . - [RequiresUnreferencedCode("Authentication middleware does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")] - public static IdentityBuilder AddIdentityApiEndpoints(this IServiceCollection services, Action configure) - where TUser : class, new() - { - ArgumentNullException.ThrowIfNull(services); - ArgumentNullException.ThrowIfNull(configure); - - services.AddAuthentication(IdentityConstants.BearerAndApplicationScheme) - .AddScheme(IdentityConstants.BearerAndApplicationScheme, null, o => - { - o.ForwardDefault = IdentityConstants.BearerScheme; - o.ForwardAuthenticate = IdentityConstants.BearerAndApplicationScheme; - }) - .AddBearerToken(IdentityConstants.BearerScheme) - .AddIdentityCookies(); - - return services.AddIdentityCore(o => - { - o.Stores.MaxLengthForKeys = 128; - configure(o); - }) - .AddApiEndpoints(); - } - - private sealed class CompositeIdentityHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) - : PolicySchemeHandler(options, logger, encoder) - { - protected override async Task 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); - } - } -} diff --git a/src/Identity/Core/src/IdentityAuthenticationBuilderExtensions.cs b/src/Identity/Core/src/IdentityAuthenticationBuilderExtensions.cs new file mode 100644 index 000000000000..684c0795c45e --- /dev/null +++ b/src/Identity/Core/src/IdentityAuthenticationBuilderExtensions.cs @@ -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; + +/// +/// Extension methods to enable bearer token authentication for use with identity. +/// +public static class IdentityAuthenticationBuilderExtensions +{ + /// + /// Adds cookie authentication. + /// + /// The current instance. + /// The . + public static AuthenticationBuilder AddIdentityBearerToken(this AuthenticationBuilder builder) + where TUser : class, new() + => builder.AddIdentityBearerToken(o => { }); + + /// + /// Adds the cookie authentication needed for sign in manager. + /// + /// The current instance. + /// Action used to configure the bearer token handler. + /// The . + public static AuthenticationBuilder AddIdentityBearerToken(this AuthenticationBuilder builder, Action configureOptions) + where TUser : class, new() + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(configureOptions); + + return builder.AddBearerToken(IdentityConstants.BearerScheme, configureOptions); + } +} diff --git a/src/Identity/Core/src/IdentityBuilderExtensions.cs b/src/Identity/Core/src/IdentityBuilderExtensions.cs index ffefe1fde029..1e2be3b64c7d 100644 --- a/src/Identity/Core/src/IdentityBuilderExtensions.cs +++ b/src/Identity/Core/src/IdentityBuilderExtensions.cs @@ -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; @@ -79,6 +83,22 @@ public static IdentityBuilder AddSignInManager(this IdentityBuilder builder) return builder; } + /// + /// Adds configuration ans services needed to support + /// but does not configure authentication. Call and/or + /// to configure authentication separately. + /// + /// The . + /// The . + public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder); + + builder.AddSignInManager(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, IdentityEndpointsJsonOptionsSetup>()); + return builder; + } + // Set TimeProvider from DI on all options instances, if not already set by tests. private sealed class PostConfigureSecurityStampValidatorOptions : IPostConfigureOptions { diff --git a/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs b/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs index 7d58e00ee764..7d5153c80fb7 100644 --- a/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs +++ b/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs @@ -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; @@ -109,6 +114,47 @@ public static class IdentityServiceCollectionExtensions return new IdentityBuilder(typeof(TUser), typeof(TRole), services); } + /// + /// Adds a set of common identity services to the application to support + /// and configures authentication to support identity bearer tokens and cookies. + /// + /// The . + /// The . + public static IdentityBuilder AddIdentityApiEndpoints(this IServiceCollection services) + where TUser : class, new() + => services.AddIdentityApiEndpoints(_ => { }); + + /// + /// Adds a set of common identity services to the application to support + /// and configures authentication to support identity bearer tokens and cookies. + /// + /// The . + /// Configures the . + /// The . + public static IdentityBuilder AddIdentityApiEndpoints(this IServiceCollection services, Action configure) + where TUser : class, new() + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configure); + + services + .AddAuthentication(IdentityConstants.BearerAndApplicationScheme) + .AddScheme(IdentityConstants.BearerAndApplicationScheme, null, compositeOptions => + { + compositeOptions.ForwardDefault = IdentityConstants.BearerScheme; + compositeOptions.ForwardAuthenticate = IdentityConstants.BearerAndApplicationScheme; + }) + .AddIdentityBearerToken() + .AddIdentityCookies(); + + return services.AddIdentityCore(o => + { + o.Stores.MaxLengthForKeys = 128; + configure(o); + }) + .AddApiEndpoints(); + } + /// /// Configures the application cookie. /// @@ -141,4 +187,32 @@ public void PostConfigure(string? name, SecurityStampValidatorOptions options) options.TimeProvider ??= TimeProvider; } } + + private sealed class CompositeIdentityHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : SignInAuthenticationHandler(options, logger, encoder) + { + protected override async Task 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(); + } + } } diff --git a/src/Identity/Core/src/PublicAPI.Unshipped.txt b/src/Identity/Core/src/PublicAPI.Unshipped.txt index b917f951ab88..9bd478611156 100644 --- a/src/Identity/Core/src/PublicAPI.Unshipped.txt +++ b/src/Identity/Core/src/PublicAPI.Unshipped.txt @@ -1,15 +1,16 @@ #nullable enable -Microsoft.AspNetCore.Identity.IdentityApiEndpointsIdentityBuilderExtensions +Microsoft.AspNetCore.Identity.IdentityAuthenticationBuilderExtensions Microsoft.AspNetCore.Identity.SecurityStampValidator.SecurityStampValidator(Microsoft.Extensions.Options.IOptions! options, Microsoft.AspNetCore.Identity.SignInManager! signInManager, Microsoft.Extensions.Logging.ILoggerFactory! logger) -> void Microsoft.AspNetCore.Identity.SecurityStampValidator.TimeProvider.get -> System.TimeProvider! Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions.TimeProvider.get -> System.TimeProvider? Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions.TimeProvider.set -> void Microsoft.AspNetCore.Identity.TwoFactorSecurityStampValidator.TwoFactorSecurityStampValidator(Microsoft.Extensions.Options.IOptions! options, Microsoft.AspNetCore.Identity.SignInManager! signInManager, Microsoft.Extensions.Logging.ILoggerFactory! logger) -> void Microsoft.AspNetCore.Routing.IdentityApiEndpointRouteBuilderExtensions -Microsoft.Extensions.DependencyInjection.IdentityApiEndpointsServiceCollectionExtensions -static Microsoft.AspNetCore.Identity.IdentityApiEndpointsIdentityBuilderExtensions.AddApiEndpoints(this Microsoft.AspNetCore.Identity.IdentityBuilder! builder) -> Microsoft.AspNetCore.Identity.IdentityBuilder! +static Microsoft.AspNetCore.Identity.IdentityAuthenticationBuilderExtensions.AddIdentityBearerToken(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! +static Microsoft.AspNetCore.Identity.IdentityAuthenticationBuilderExtensions.AddIdentityBearerToken(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, System.Action! configureOptions) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! +static Microsoft.AspNetCore.Identity.IdentityBuilderExtensions.AddApiEndpoints(this Microsoft.AspNetCore.Identity.IdentityBuilder! builder) -> Microsoft.AspNetCore.Identity.IdentityBuilder! static Microsoft.AspNetCore.Routing.IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder! -static Microsoft.Extensions.DependencyInjection.IdentityApiEndpointsServiceCollectionExtensions.AddIdentityApiEndpoints(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.AspNetCore.Identity.IdentityBuilder! -static Microsoft.Extensions.DependencyInjection.IdentityApiEndpointsServiceCollectionExtensions.AddIdentityApiEndpoints(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.AspNetCore.Identity.IdentityBuilder! +static Microsoft.Extensions.DependencyInjection.IdentityServiceCollectionExtensions.AddIdentityApiEndpoints(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.AspNetCore.Identity.IdentityBuilder! +static Microsoft.Extensions.DependencyInjection.IdentityServiceCollectionExtensions.AddIdentityApiEndpoints(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configure) -> Microsoft.AspNetCore.Identity.IdentityBuilder! static readonly Microsoft.AspNetCore.Identity.IdentityConstants.BearerScheme -> string! virtual Microsoft.AspNetCore.Identity.SignInManager.IsTwoFactorEnabledAsync(TUser! user) -> System.Threading.Tasks.Task! diff --git a/src/Identity/Identity.slnf b/src/Identity/Identity.slnf index bce278e7f590..0f3fe6d1d784 100644 --- a/src/Identity/Identity.slnf +++ b/src/Identity/Identity.slnf @@ -84,6 +84,7 @@ "src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj", "src\\Security\\Authentication\\OAuth\\src\\Microsoft.AspNetCore.Authentication.OAuth.csproj", "src\\Security\\Authentication\\Twitter\\src\\Microsoft.AspNetCore.Authentication.Twitter.csproj", + "src\\Security\\Authentication\\test\\Microsoft.AspNetCore.Authentication.Test.csproj", "src\\Security\\Authorization\\Core\\src\\Microsoft.AspNetCore.Authorization.csproj", "src\\Security\\Authorization\\Policy\\src\\Microsoft.AspNetCore.Authorization.Policy.csproj", "src\\Security\\CookiePolicy\\src\\Microsoft.AspNetCore.CookiePolicy.csproj", diff --git a/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs b/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs similarity index 54% rename from src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs rename to src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs index 42cad245feb0..7dc09295f896 100644 --- a/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs +++ b/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs @@ -4,27 +4,25 @@ #nullable enable using System.Net; +using System.Net.Http; using System.Net.Http.Json; using System.Security.Claims; using System.Text.Json; using Identity.DefaultUI.WebSite; using Identity.DefaultUI.WebSite.Data; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Identity.Test; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.TestHost; using Microsoft.AspNetCore.Testing; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.Identity.FunctionalTests; -public class MapIdentityTests : LoggedTest +public class MapIdentityApiTests : LoggedTest { private string Username { get; } = $"{Guid.NewGuid()}@example.com"; private string Password { get; } = $"[PLACEHOLDER]-1a"; @@ -76,7 +74,7 @@ public async Task CanCustomizeBearerTokenExpiration() await using var app = await CreateAppAsync(services => { services.AddIdentityCore().AddApiEndpoints().AddEntityFrameworkStores(); - services.AddAuthentication(IdentityConstants.BearerScheme).AddBearerToken(IdentityConstants.BearerScheme, options => + services.AddAuthentication(IdentityConstants.BearerScheme).AddIdentityBearerToken(options => { options.BearerTokenExpiration = expireTimeSpan; options.TimeProvider = clock; @@ -101,15 +99,13 @@ public async Task CanCustomizeBearerTokenExpiration() clock.Advance(TimeSpan.FromSeconds(expireTimeSpan.TotalSeconds - 1)); - // Still works without one second before expiration. + // Still works one second before expiration. Assert.Equal($"Hello, {Username}!", await client.GetStringAsync("/auth/hello")); clock.Advance(TimeSpan.FromSeconds(1)); - var unauthorizedResponse = await client.GetAsync("/auth/hello"); // Fails the second the BearerTokenExpiration elapses. - Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedResponse.StatusCode); - Assert.Equal(0, unauthorizedResponse.Content.Headers.ContentLength); + AssertUnauthorizedAndEmpty(await client.GetAsync("/auth/hello")); } [Fact] @@ -155,7 +151,7 @@ public async Task CanReadBearerTokenFromQueryString() await using var app = await CreateAppAsync(services => { services.AddIdentityCore().AddApiEndpoints().AddEntityFrameworkStores(); - services.AddAuthentication(IdentityConstants.BearerScheme).AddBearerToken(IdentityConstants.BearerScheme, options => + services.AddAuthentication(IdentityConstants.BearerScheme).AddIdentityBearerToken(options => { options.Events.OnMessageReceived = context => { @@ -187,8 +183,159 @@ public async Task Returns401UnauthorizedStatusGivenNoBearerTokenOrCookie(string await using var app = await CreateAppAsync(AddIdentityActions[addIdentityMode]); using var client = app.GetTestClient(); - var unauthorizedResponse = await client.GetAsync($"/auth/hello"); - Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedResponse.StatusCode); + AssertUnauthorizedAndEmpty(await client.GetAsync($"/auth/hello")); + + client.DefaultRequestHeaders.Authorization = new("Bearer"); + AssertUnauthorizedAndEmpty(await client.GetAsync($"/auth/hello")); + + client.DefaultRequestHeaders.Authorization = new("Bearer", ""); + AssertUnauthorizedAndEmpty(await client.GetAsync($"/auth/hello")); + } + + [Theory] + [MemberData(nameof(AddIdentityModes))] + public async Task CanUseRefreshToken(string addIdentityMode) + { + await using var app = await CreateAppAsync(AddIdentityActions[addIdentityMode]); + using var client = app.GetTestClient(); + + await client.PostAsJsonAsync("/identity/register", new { Username, Password }); + var loginResponse = await client.PostAsJsonAsync("/identity/login", new { Username, Password }); + var loginContent = await loginResponse.Content.ReadFromJsonAsync(); + var refreshToken = loginContent.GetProperty("refresh_token").GetString(); + + var refreshResponse = await client.PostAsJsonAsync("/identity/refresh", new { refreshToken }); + var refreshContent = await refreshResponse.Content.ReadFromJsonAsync(); + var accessToken = loginContent.GetProperty("access_token").GetString(); + + client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); + Assert.Equal($"Hello, {Username}!", await client.GetStringAsync("/auth/hello")); + } + + [Fact] + public async Task Returns401UnauthorizedStatusGivenNullOrEmptyRefreshToken() + { + await using var app = await CreateAppAsync(); + using var client = app.GetTestClient(); + + string? refreshToken = null; + AssertUnauthorizedAndEmpty(await client.PostAsJsonAsync("/identity/refresh", new { refreshToken })); + + refreshToken = ""; + AssertUnauthorizedAndEmpty(await client.PostAsJsonAsync("/identity/refresh", new { refreshToken })); + } + + [Fact] + public async Task CanCustomizeRefreshTokenExpiration() + { + var clock = new MockTimeProvider(); + var expireTimeSpan = TimeSpan.FromHours(42); + + await using var app = await CreateAppAsync(services => + { + services.AddSingleton(clock); + services.AddIdentityCore().AddApiEndpoints().AddEntityFrameworkStores(); + services.AddAuthentication(IdentityConstants.BearerScheme).AddIdentityBearerToken(options => + { + options.RefreshTokenExpiration = expireTimeSpan; + }); + }); + + using var client = app.GetTestClient(); + + await client.PostAsJsonAsync("/identity/register", new { Username, Password }); + var loginResponse = await client.PostAsJsonAsync("/identity/login", new { Username, Password }); + + var loginContent = await loginResponse.Content.ReadFromJsonAsync(); + var refreshToken = loginContent.GetProperty("refresh_token").GetString(); + var accessToken = loginContent.GetProperty("refresh_token").GetString(); + + // Works without time passing. + var refreshResponse = await client.PostAsJsonAsync("/identity/refresh", new { refreshToken }); + Assert.True(refreshResponse.IsSuccessStatusCode); + + clock.Advance(TimeSpan.FromSeconds(expireTimeSpan.TotalSeconds - 1)); + + // Still works one second before expiration. + refreshResponse = await client.PostAsJsonAsync("/identity/refresh", new { refreshToken }); + Assert.True(refreshResponse.IsSuccessStatusCode); + + // The bearer token stopped working 41 hours ago with the default 1 hour expiration. + client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); + AssertUnauthorizedAndEmpty(await client.GetAsync("/auth/hello")); + + clock.Advance(TimeSpan.FromSeconds(1)); + + // Fails the second the RefreshTokenExpiration elapses. + AssertUnauthorizedAndEmpty(await client.PostAsJsonAsync("/identity/refresh", new { refreshToken })); + + // But the last refresh_token from the successful /refresh only a second ago has not expired. + var refreshContent = await refreshResponse.Content.ReadFromJsonAsync(); + refreshToken = refreshContent.GetProperty("refresh_token").GetString(); + + refreshResponse = await client.PostAsJsonAsync("/identity/refresh", new { refreshToken }); + refreshContent = await refreshResponse.Content.ReadFromJsonAsync(); + accessToken = refreshContent.GetProperty("access_token").GetString(); + + client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); + Assert.Equal($"Hello, {Username}!", await client.GetStringAsync("/auth/hello")); + } + + [Theory] + [MemberData(nameof(AddIdentityModes))] + public async Task RefreshReturns401UnauthorizedIfSecurityStampChanges(string addIdentityMode) + { + await using var app = await CreateAppAsync(AddIdentityActions[addIdentityMode]); + using var client = app.GetTestClient(); + + await client.PostAsJsonAsync("/identity/register", new { Username, Password }); + var loginResponse = await client.PostAsJsonAsync("/identity/login", new { Username, Password }); + var loginContent = await loginResponse.Content.ReadFromJsonAsync(); + var refreshToken = loginContent.GetProperty("refresh_token").GetString(); + + var userManager = app.Services.GetRequiredService>(); + var user = await userManager.FindByNameAsync(Username); + + Assert.NotNull(user); + + await userManager.UpdateSecurityStampAsync(user); + + AssertUnauthorizedAndEmpty(await client.PostAsJsonAsync("/identity/refresh", new { refreshToken })); + } + + [Theory] + [MemberData(nameof(AddIdentityModes))] + public async Task RefreshUpdatesUserFromStore(string addIdentityMode) + { + await using var app = await CreateAppAsync(AddIdentityActions[addIdentityMode]); + using var client = app.GetTestClient(); + + await client.PostAsJsonAsync("/identity/register", new { Username, Password }); + var loginResponse = await client.PostAsJsonAsync("/identity/login", new { Username, Password }); + var loginContent = await loginResponse.Content.ReadFromJsonAsync(); + var refreshToken = loginContent.GetProperty("refresh_token").GetString(); + + var userManager = app.Services.GetRequiredService>(); + var user = await userManager.FindByNameAsync(Username); + + Assert.NotNull(user); + + var newUsername = $"{Guid.NewGuid()}@example.org"; + user.UserName = newUsername; + await userManager.UpdateAsync(user); + + var refreshResponse = await client.PostAsJsonAsync("/identity/refresh", new { refreshToken }); + var refreshContent = await refreshResponse.Content.ReadFromJsonAsync(); + var accessToken = refreshContent.GetProperty("access_token").GetString(); + + client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); + Assert.Equal($"Hello, {newUsername}!", await client.GetStringAsync("/auth/hello")); + } + + private static void AssertUnauthorizedAndEmpty(HttpResponseMessage response) + { + Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); + Assert.Equal(0, response.Content.Headers.ContentLength); } private async Task CreateAppAsync(Action? configureServices) @@ -231,8 +378,13 @@ private static void AddIdentityEndpoints(IServiceCollection services) private static void AddIdentityEndpointsBearerOnly(IServiceCollection services) { - services.AddIdentityCore().AddEntityFrameworkStores(); - services.AddAuthentication(IdentityConstants.BearerScheme).AddBearerToken(IdentityConstants.BearerScheme); + services + .AddIdentityCore() + .AddEntityFrameworkStores() + .AddApiEndpoints(); + services + .AddAuthentication(IdentityConstants.BearerScheme) + .AddIdentityBearerToken(); } private Task CreateAppAsync(Action? configureServices = null) diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenConfigureOptions.cs b/src/Security/Authentication/BearerToken/src/BearerTokenConfigureOptions.cs new file mode 100644 index 000000000000..e91dd9534fbe --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/BearerTokenConfigureOptions.cs @@ -0,0 +1,28 @@ +// 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.DataProtection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication.BearerToken; + +internal sealed class BearerTokenConfigureOptions(IDataProtectionProvider dp) : IConfigureNamedOptions +{ + private const string _primaryPurpose = "Microsoft.AspNetCore.Authentication.BearerToken"; + + public void Configure(string? schemeName, BearerTokenOptions options) + { + if (schemeName is null) + { + return; + } + + options.BearerTokenProtector = new TicketDataFormat(dp.CreateProtector(_primaryPurpose, schemeName, "BearerToken")); + options.RefreshTokenProtector = new TicketDataFormat(dp.CreateProtector(_primaryPurpose, schemeName, "RefreshToken")); + } + + public void Configure(BearerTokenOptions options) + { + throw new NotImplementedException(); + } +} diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs b/src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs index f2662caaff2d..5683cf5aa9cf 100644 --- a/src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs +++ b/src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs @@ -16,5 +16,6 @@ public class BearerTokenEvents /// /// Invoked when a protocol message is first received. /// - public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context); + /// The . + public virtual Task MessageReceivedAsync(MessageReceivedContext context) => OnMessageReceived(context); } diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs b/src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs index 0ac96986919d..e203d8a48025 100644 --- a/src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs +++ b/src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs @@ -3,6 +3,8 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.BearerToken; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; namespace Microsoft.Extensions.DependencyInjection; @@ -62,6 +64,7 @@ public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder bu ArgumentNullException.ThrowIfNull(authenticationScheme); ArgumentNullException.ThrowIfNull(configure); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, BearerTokenConfigureOptions>()); return builder.AddScheme(authenticationScheme, configure); } } diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs b/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs index 72c451681b7a..9aa55f15d32e 100644 --- a/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs +++ b/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs @@ -4,7 +4,6 @@ using System.Security.Claims; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Authentication.BearerToken.DTO; -using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -12,23 +11,14 @@ namespace Microsoft.AspNetCore.Authentication.BearerToken; -internal sealed class BearerTokenHandler( - IOptionsMonitor optionsMonitor, - ILoggerFactory loggerFactory, - UrlEncoder urlEncoder, -#pragma warning disable IDE0060 // Remove unused parameter. False positive fixed by https://github.com/dotnet/roslyn/pull/67167 - IDataProtectionProvider dataProtectionProvider) -#pragma warning restore IDE0060 // Remove unused parameter +internal sealed class BearerTokenHandler(IOptionsMonitor optionsMonitor, ILoggerFactory loggerFactory, UrlEncoder urlEncoder) : SignInAuthenticationHandler(optionsMonitor, loggerFactory, urlEncoder) { - private const string BearerTokenPurpose = $"Microsoft.AspNetCore.Authentication.BearerToken:v1:BearerToken"; + private static readonly long OneSecondTicks = TimeSpan.FromSeconds(1).Ticks; private static readonly AuthenticateResult FailedUnprotectingToken = AuthenticateResult.Fail("Unprotected token failed"); private static readonly AuthenticateResult TokenExpired = AuthenticateResult.Fail("Token expired"); - private ISecureDataFormat BearerTokenProtector - => Options.BearerTokenProtector ?? new TicketDataFormat(dataProtectionProvider.CreateProtector(BearerTokenPurpose)); - private new BearerTokenEvents Events => (BearerTokenEvents)base.Events!; protected override async Task HandleAuthenticateAsync() @@ -36,9 +26,9 @@ protected override async Task HandleAuthenticateAsync() // Give application opportunity to find from a different location, adjust, or reject token var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options); - await Events.MessageReceived(messageReceivedContext); + await Events.MessageReceivedAsync(messageReceivedContext); - if (messageReceivedContext.Result != null) + if (messageReceivedContext.Result is not null) { return messageReceivedContext.Result; } @@ -50,14 +40,14 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.NoResult(); } - var ticket = BearerTokenProtector.Unprotect(token); + var ticket = Options.BearerTokenProtector.Unprotect(token); - if (ticket?.Properties?.ExpiresUtc is null) + if (ticket?.Properties?.ExpiresUtc is not { } expiresUtc) { return FailedUnprotectingToken; } - if (TimeProvider.GetUtcNow() >= ticket.Properties.ExpiresUtc) + if (TimeProvider.GetUtcNow() >= expiresUtc) { return TokenExpired; } @@ -65,37 +55,29 @@ protected override async Task HandleAuthenticateAsync() return AuthenticateResult.Success(ticket); } - protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + protected override Task HandleChallengeAsync(AuthenticationProperties properties) { Response.Headers.Append(HeaderNames.WWWAuthenticate, "Bearer"); - await base.HandleChallengeAsync(properties); + return base.HandleChallengeAsync(properties); } - protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) + protected override async Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) { - long expiresInTotalSeconds; var utcNow = TimeProvider.GetUtcNow(); properties ??= new(); + properties.ExpiresUtc ??= utcNow + Options.BearerTokenExpiration; - if (properties.ExpiresUtc is null) - { - properties.ExpiresUtc ??= utcNow + Options.BearerTokenExpiration; - expiresInTotalSeconds = (long)Options.BearerTokenExpiration.TotalSeconds; - } - else + var response = new AccessTokenResponse { - expiresInTotalSeconds = (long)(properties.ExpiresUtc.Value - utcNow).TotalSeconds; - } - - var ticket = new AuthenticationTicket(user, properties, Scheme.Name); - var accessTokenResponse = new AccessTokenResponse - { - AccessToken = BearerTokenProtector.Protect(ticket), - ExpiresInTotalSeconds = expiresInTotalSeconds, + AccessToken = Options.BearerTokenProtector.Protect(CreateBearerTicket(user, properties)), + ExpiresInSeconds = CalculateExpiresInSeconds(utcNow, properties.ExpiresUtc), + RefreshToken = Options.RefreshTokenProtector.Protect(CreateRefreshTicket(user, utcNow)), }; - return Context.Response.WriteAsJsonAsync(accessTokenResponse, BearerTokenJsonSerializerContext.Default.AccessTokenResponse); + Logger.AuthenticationSchemeSignedIn(Scheme.Name); + + await Context.Response.WriteAsJsonAsync(response, BearerTokenJsonSerializerContext.Default.AccessTokenResponse); } // No-op to avoid interfering with any mass sign-out logic. @@ -109,4 +91,35 @@ protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationPr ? authorization["Bearer ".Length..] : null; } + + private long CalculateExpiresInSeconds(DateTimeOffset utcNow, DateTimeOffset? expiresUtc) + { + static DateTimeOffset FloorSeconds(DateTimeOffset dateTimeOffset) + => new(dateTimeOffset.Ticks / OneSecondTicks * OneSecondTicks, dateTimeOffset.Offset); + + // AuthenticationProperties floors ExpiresUtc. If this remains unchanged, we'll use BearerTokenExpiration directly + // to produce a consistent ExpiresInTotalSeconds values. If ExpiresUtc was overridden, we just calculate the + // the difference from utcNow and round even though this will likely result in unstable values. + var expiresTimeSpan = Options.BearerTokenExpiration; + var expectedExpiresUtc = FloorSeconds(utcNow + expiresTimeSpan); + return (long)(expiresUtc switch + { + DateTimeOffset d when d == expectedExpiresUtc => expiresTimeSpan.TotalSeconds, + DateTimeOffset d => (d - utcNow).TotalSeconds, + _ => expiresTimeSpan.TotalSeconds, + }); + } + + private AuthenticationTicket CreateBearerTicket(ClaimsPrincipal user, AuthenticationProperties properties) + => new(user, properties, $"{Scheme.Name}:AccessToken"); + + private AuthenticationTicket CreateRefreshTicket(ClaimsPrincipal user, DateTimeOffset utcNow) + { + var refreshProperties = new AuthenticationProperties + { + ExpiresUtc = utcNow + Options.RefreshTokenExpiration + }; + + return new AuthenticationTicket(user, refreshProperties, $"{Scheme.Name}:RefreshToken"); + } } diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs b/src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs index b62fe134bd41..03bb194e609b 100644 --- a/src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs +++ b/src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs @@ -10,6 +10,9 @@ namespace Microsoft.AspNetCore.Authentication.BearerToken; /// public sealed class BearerTokenOptions : AuthenticationSchemeOptions { + private ISecureDataFormat? _bearerTokenProtector; + private ISecureDataFormat? _refreshTokenProtector; + /// /// Constructs the options used to authenticate using opaque bearer tokens. /// @@ -23,14 +26,41 @@ public BearerTokenOptions() /// The expiration information is stored in the protected token. Because of that, an expired token will be rejected /// even if it is passed to the server after the client should have purged it. /// + /// + /// Defaults to 1 hour. + /// public TimeSpan BearerTokenExpiration { get; set; } = TimeSpan.FromHours(1); + /// + /// Controls how much time the refresh token will remain valid from the point it is created. + /// The expiration information is stored in the protected token. + /// + /// + /// Defaults to 14 days. + /// + public TimeSpan RefreshTokenExpiration { get; set; } = TimeSpan.FromDays(14); + /// /// If set, the is used to protect and unprotect the identity and other properties which are stored in the - /// bearer token value. If not provided, one will be created using and the + /// bearer token. If not provided, one will be created using and the + /// from the application . + /// + public ISecureDataFormat BearerTokenProtector + { + get => _bearerTokenProtector ?? throw new InvalidOperationException($"{nameof(BearerTokenProtector)} was not set."); + set => _bearerTokenProtector = value; + } + + /// + /// If set, the is used to protect and unprotect the identity and other properties which are stored in the + /// refresh token. If not provided, one will be created using and the /// from the application . /// - public ISecureDataFormat? BearerTokenProtector { get; set; } + public ISecureDataFormat RefreshTokenProtector + { + get => _refreshTokenProtector ?? throw new InvalidOperationException($"{nameof(RefreshTokenProtector)} was not set."); + set => _refreshTokenProtector = value; + } /// /// The object provided by the application to process events raised by the bearer token authentication handler. diff --git a/src/Security/Authentication/BearerToken/src/LoggingExtensions.cs b/src/Security/Authentication/BearerToken/src/LoggingExtensions.cs new file mode 100644 index 000000000000..5b88ce4744b5 --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/LoggingExtensions.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Logging; + +internal static partial class LoggingExtensions +{ + [LoggerMessage(1, LogLevel.Information, "AuthenticationScheme: {AuthenticationScheme} signed in.", EventName = "AuthenticationSchemeSignedIn")] + public static partial void AuthenticationSchemeSignedIn(this ILogger logger, string authenticationScheme); +} diff --git a/src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt index 1765bd328d8e..3de23c61dc8f 100644 --- a/src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt +++ b/src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt @@ -9,10 +9,14 @@ Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.BearerTokenExpiration.get -> System.TimeSpan Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.BearerTokenExpiration.set -> void Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.BearerTokenOptions() -> void -Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.BearerTokenProtector.get -> Microsoft.AspNetCore.Authentication.ISecureDataFormat? +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.BearerTokenProtector.get -> Microsoft.AspNetCore.Authentication.ISecureDataFormat! Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.BearerTokenProtector.set -> void Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.Events.get -> Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenEvents! Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.Events.set -> void +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.RefreshTokenExpiration.get -> System.TimeSpan +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.RefreshTokenExpiration.set -> void +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.RefreshTokenProtector.get -> Microsoft.AspNetCore.Authentication.ISecureDataFormat! +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions.RefreshTokenProtector.set -> void Microsoft.AspNetCore.Authentication.BearerToken.MessageReceivedContext Microsoft.AspNetCore.Authentication.BearerToken.MessageReceivedContext.MessageReceivedContext(Microsoft.AspNetCore.Http.HttpContext! context, Microsoft.AspNetCore.Authentication.AuthenticationScheme! scheme, Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions! options) -> void Microsoft.AspNetCore.Authentication.BearerToken.MessageReceivedContext.Token.get -> string? @@ -22,4 +26,4 @@ static Microsoft.Extensions.DependencyInjection.BearerTokenExtensions.AddBearerT static Microsoft.Extensions.DependencyInjection.BearerTokenExtensions.AddBearerToken(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, string! authenticationScheme) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! static Microsoft.Extensions.DependencyInjection.BearerTokenExtensions.AddBearerToken(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, string! authenticationScheme, System.Action! configure) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! static Microsoft.Extensions.DependencyInjection.BearerTokenExtensions.AddBearerToken(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, System.Action! configure) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! -virtual Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenEvents.MessageReceived(Microsoft.AspNetCore.Authentication.BearerToken.MessageReceivedContext! context) -> System.Threading.Tasks.Task! +virtual Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenEvents.MessageReceivedAsync(Microsoft.AspNetCore.Authentication.BearerToken.MessageReceivedContext! context) -> System.Threading.Tasks.Task! diff --git a/src/Shared/BearerToken/DTO/AccessTokenResponse.cs b/src/Shared/BearerToken/DTO/AccessTokenResponse.cs index 04a7fb75374e..18755578bd69 100644 --- a/src/Shared/BearerToken/DTO/AccessTokenResponse.cs +++ b/src/Shared/BearerToken/DTO/AccessTokenResponse.cs @@ -5,14 +5,45 @@ namespace Microsoft.AspNetCore.Authentication.BearerToken.DTO; +/// +/// The JSON data transfer object for the bearer token response. +/// internal sealed class AccessTokenResponse { + /// + /// The value is always "Bearer" which indicates this response provides a "Bearer" token + /// in the form of an opaque . + /// + /// + /// This is serialized as "token_type": "Bearer" using System.Text.Json. + /// [JsonPropertyName("token_type")] public string TokenType { get; } = "Bearer"; + /// + /// The opaque bearer token to send as part of the Authorization request header. + /// + /// + /// This is serialized as "access_token": "{AccessToken}" using System.Text.Json. + /// [JsonPropertyName("access_token")] public required string AccessToken { get; init; } + /// + /// The number of seconds before the expires. + /// + /// + /// This is serialized as "expires_in": "{ExpiresInSeconds}" using System.Text.Json. + /// [JsonPropertyName("expires_in")] - public required long ExpiresInTotalSeconds { get; init; } + public required long ExpiresInSeconds { get; init; } + + /// + /// If set, this provides the ability to get a new access_token after it expires using a refresh endpoint. + /// + /// + /// This is serialized as "refresh_token": "{RefreshToken}" using System.Text.Json. + /// + [JsonPropertyName("refresh_token")] + public required string RefreshToken { get; init; } }