From 335b6782a249933d2d677e20bd34ac2c69bd6223 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 16 Mar 2023 19:06:16 -0700 Subject: [PATCH 1/3] Add MapIdentityApi() --- AspNetCore.sln | 43 ++- eng/ProjectReferences.props | 1 + eng/SharedFramework.Local.props | 1 + eng/TrimmableProjects.props | 1 + .../IdentityEndpointsJsonSerializerContext.cs | 12 + src/Identity/Core/src/DTO/LoginRequest.cs | 10 + src/Identity/Core/src/DTO/RegisterRequest.cs | 10 + ...entityApiEndpointRouteBuilderExtensions.cs | 104 ++++++++ ...tyApiEndpointsIdentityBuilderExtensions.cs | 34 +++ ...ApiEndpointsServiceCollectionExtensions.cs | 78 ++++++ src/Identity/Core/src/IdentityConstants.cs | 20 +- .../src/IdentityEndpointsJsonOptionsSetup.cs | 17 ++ .../src/Microsoft.AspNetCore.Identity.csproj | 11 + src/Identity/Core/src/PublicAPI.Unshipped.txt | 8 + .../Extensions.Core/src/SignInResult.cs | 2 +- src/Identity/Identity.slnf | 7 +- .../Microsoft.AspNetCore.Identity.UI.csproj | 1 + .../IdentitySample.ApiEndpoints.csproj | 15 ++ .../IdentitySample.ApiEndpoints/Program.cs | 38 +++ .../Properties/launchSettings.json | 12 + .../.config/dotnet-tools.json | 2 +- .../Data/ApplicationDbContext.cs | 3 +- .../MapIdentityTests.cs | 248 ++++++++++++++++++ ...AspNetCore.Identity.FunctionalTests.csproj | 1 + .../BearerToken/src/BearerTokenDefaults.cs | 15 ++ .../BearerToken/src/BearerTokenEvents.cs | 20 ++ .../BearerToken/src/BearerTokenExtensions.cs | 67 +++++ .../BearerToken/src/BearerTokenHandler.cs | 112 ++++++++ .../src/BearerTokenJsonSerializerContext.cs | 11 + .../BearerToken/src/BearerTokenOptions.cs | 46 ++++ .../BearerToken/src/MessageReceivedContext.cs | 27 ++ ...pNetCore.Authentication.BearerToken.csproj | 20 ++ .../BearerToken/src/PublicAPI.Shipped.txt | 1 + .../BearerToken/src/PublicAPI.Unshipped.txt | 25 ++ .../JwtBearer/src/JwtBearerDefaults.cs | 4 +- .../Authentication/test/BearerTokenTests.cs | 26 ++ ...soft.AspNetCore.Authentication.Test.csproj | 1 + src/Security/Security.slnf | 1 + .../BearerToken/DTO/AccessTokenResponse.cs | 18 ++ src/Tools/Tools.slnf | 1 + 40 files changed, 1062 insertions(+), 12 deletions(-) create mode 100644 src/Identity/Core/src/DTO/IdentityEndpointsJsonSerializerContext.cs create mode 100644 src/Identity/Core/src/DTO/LoginRequest.cs create mode 100644 src/Identity/Core/src/DTO/RegisterRequest.cs create mode 100644 src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs create mode 100644 src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs create mode 100644 src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs create mode 100644 src/Identity/Core/src/IdentityEndpointsJsonOptionsSetup.cs create mode 100644 src/Identity/samples/IdentitySample.ApiEndpoints/IdentitySample.ApiEndpoints.csproj create mode 100644 src/Identity/samples/IdentitySample.ApiEndpoints/Program.cs create mode 100644 src/Identity/samples/IdentitySample.ApiEndpoints/Properties/launchSettings.json create mode 100644 src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs create mode 100644 src/Security/Authentication/BearerToken/src/BearerTokenDefaults.cs create mode 100644 src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs create mode 100644 src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs create mode 100644 src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs create mode 100644 src/Security/Authentication/BearerToken/src/BearerTokenJsonSerializerContext.cs create mode 100644 src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs create mode 100644 src/Security/Authentication/BearerToken/src/MessageReceivedContext.cs create mode 100644 src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj create mode 100644 src/Security/Authentication/BearerToken/src/PublicAPI.Shipped.txt create mode 100644 src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt create mode 100644 src/Security/Authentication/test/BearerTokenTests.cs create mode 100644 src/Shared/BearerToken/DTO/AccessTokenResponse.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 78d5c6afef38..13e290d6ade3 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1762,7 +1762,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests", "src\Servers\Kestrel\Transport.NamedPipes\test\Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.Tests.csproj", "{97C7D2A4-87E5-4A4A-A170-D736427D5C21}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Http.RequestDelegateGenerator", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj", "{4730F56D-24EF-4BB2-AA75-862E31205F3A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Http.RequestDelegateGenerator", "src\Http\Http.Extensions\gen\Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj", "{4730F56D-24EF-4BB2-AA75-862E31205F3A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "QuickGrid", "QuickGrid", "{C406D9E0-1585-43F9-AA8F-D468AF84A996}" EndProject @@ -1780,6 +1780,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnitedApp", "src\Components\Samples\BlazorUnitedApp\BlazorUnitedApp.csproj", "{F5AE525F-F435-40F9-A567-4D5EC3B50D6E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BearerToken", "BearerToken", "{56291265-B7BF-4756-92AB-FC30F09381D1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.BearerToken", "src\Security\Authentication\BearerToken\src\Microsoft.AspNetCore.Authentication.BearerToken.csproj", "{66FA1041-5556-43A0-9CA3-F9937F085F6E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentitySample.ApiEndpoints", "src\Identity\samples\IdentitySample.ApiEndpoints\IdentitySample.ApiEndpoints.csproj", "{37FC77EA-AC44-4D08-B002-8EFF415C424A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -10701,6 +10707,38 @@ Global {F5AE525F-F435-40F9-A567-4D5EC3B50D6E}.Release|x64.Build.0 = Release|Any CPU {F5AE525F-F435-40F9-A567-4D5EC3B50D6E}.Release|x86.ActiveCfg = Release|Any CPU {F5AE525F-F435-40F9-A567-4D5EC3B50D6E}.Release|x86.Build.0 = Release|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|arm64.ActiveCfg = Debug|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|arm64.Build.0 = Debug|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|x64.ActiveCfg = Debug|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|x64.Build.0 = Debug|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|x86.ActiveCfg = Debug|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|x86.Build.0 = Debug|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|Any CPU.Build.0 = Release|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|arm64.ActiveCfg = Release|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|arm64.Build.0 = Release|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|x64.ActiveCfg = Release|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|x64.Build.0 = Release|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|x86.ActiveCfg = Release|Any CPU + {66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|x86.Build.0 = Release|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|arm64.ActiveCfg = Debug|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|arm64.Build.0 = Debug|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|x64.ActiveCfg = Debug|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|x64.Build.0 = Debug|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|x86.ActiveCfg = Debug|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|x86.Build.0 = Debug|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|Any CPU.Build.0 = Release|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|arm64.ActiveCfg = Release|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|arm64.Build.0 = Release|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x64.ActiveCfg = Release|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x64.Build.0 = Release|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x86.ActiveCfg = Release|Any CPU + {37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -11580,6 +11618,9 @@ Global {AE4D272D-6F13-42C8-9404-C149188AFA33} = {7BAEB9BF-28F4-4DFD-9A04-E5193683C261} {5D438258-CB19-4282-814F-974ABBC71411} = {7BAEB9BF-28F4-4DFD-9A04-E5193683C261} {F5AE525F-F435-40F9-A567-4D5EC3B50D6E} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1} + {56291265-B7BF-4756-92AB-FC30F09381D1} = {822D1519-77F0-484A-B9AB-F694C2CC25F1} + {66FA1041-5556-43A0-9CA3-F9937F085F6E} = {56291265-B7BF-4756-92AB-FC30F09381D1} + {37FC77EA-AC44-4D08-B002-8EFF415C424A} = {64B2A28F-6D82-4F2B-B0BB-88DE5216DD2C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F} diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index ad5f3588908a..133edc884611 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -53,6 +53,7 @@ + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index e9ccf92ecb66..007ad96a9f6a 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -17,6 +17,7 @@ + diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index a7a9c722f699..ef28ab2b815d 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -44,6 +44,7 @@ + diff --git a/src/Identity/Core/src/DTO/IdentityEndpointsJsonSerializerContext.cs b/src/Identity/Core/src/DTO/IdentityEndpointsJsonSerializerContext.cs new file mode 100644 index 000000000000..88db894aaed8 --- /dev/null +++ b/src/Identity/Core/src/DTO/IdentityEndpointsJsonSerializerContext.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.AspNetCore.Identity.DTO; + +[JsonSerializable(typeof(RegisterRequest))] +[JsonSerializable(typeof(LoginRequest))] +internal sealed partial class IdentityEndpointsJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/Identity/Core/src/DTO/LoginRequest.cs b/src/Identity/Core/src/DTO/LoginRequest.cs new file mode 100644 index 000000000000..fbe6b6900f0f --- /dev/null +++ b/src/Identity/Core/src/DTO/LoginRequest.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.AspNetCore.Identity.DTO; + +internal sealed class LoginRequest +{ + public required string Username { get; init; } + public required string Password { get; init; } +} diff --git a/src/Identity/Core/src/DTO/RegisterRequest.cs b/src/Identity/Core/src/DTO/RegisterRequest.cs new file mode 100644 index 000000000000..26b91eb512d4 --- /dev/null +++ b/src/Identity/Core/src/DTO/RegisterRequest.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.AspNetCore.Identity.DTO; + +internal sealed class RegisterRequest +{ + public required string Username { get; init; } + public required string Password { get; init; } +} diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000000..11a40a5e74be --- /dev/null +++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.AspNetCore.Authentication.BearerToken.DTO; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Http.Metadata; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.DTO; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Routing; + +/// +/// Provides extension methods for to add identity endpoints. +/// +public static class IdentityApiEndpointRouteBuilderExtensions +{ + /// + /// Add endpoints for registering, logging in, and logging out using ASP.NET Core Identity. + /// + /// The type describing the user. This should match the generic parameter in . + /// + /// The to add the identity endpoints to. + /// Call to add a prefix to all the endpoints. + /// + /// An to further customize the added endpoints. + public static IEndpointConventionBuilder MapIdentityApi(this IEndpointRouteBuilder endpoints) where TUser : class, new() + { + ArgumentNullException.ThrowIfNull(endpoints); + + var routeGroup = endpoints.MapGroup(""); + + // 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) => + { + var userManager = services.GetRequiredService>(); + + var user = new TUser(); + await userManager.SetUserNameAsync(user, registration.Username); + var result = await userManager.CreateAsync(user, registration.Password); + + if (result.Succeeded) + { + return TypedResults.Ok(); + } + + return TypedResults.ValidationProblem(result.Errors.ToDictionary(e => e.Code, e => new[] { e.Description })); + }); + + routeGroup.MapPost("/login", async Task, SignInHttpResult>> + ([FromBody] LoginRequest login, [FromQuery] bool? cookieMode, [FromServices] IServiceProvider services) => + { + var userManager = services.GetRequiredService>(); + var user = await userManager.FindByNameAsync(login.Username); + + if (user is null || !await userManager.CheckPasswordAsync(user, login.Password)) + { + return TypedResults.Unauthorized(); + } + + var claimsFactory = services.GetRequiredService>(); + var claimsPrincipal = await claimsFactory.CreateAsync(user); + + var useCookies = cookieMode ?? false; + var scheme = useCookies ? IdentityConstants.ApplicationScheme : IdentityConstants.BearerScheme; + + return TypedResults.SignIn(claimsPrincipal, authenticationScheme: scheme); + }); + + return new IdentityEndpointsConventionBuilder(routeGroup); + } + + // Wrap RouteGroupBuilder with a non-public type to avoid a potential future behavioral breaking change. + private sealed class IdentityEndpointsConventionBuilder(RouteGroupBuilder inner) : IEndpointConventionBuilder + { +#pragma warning disable CA1822 // Mark members as static False positive reported by https://github.com/dotnet/roslyn-analyzers/issues/6573 + private IEndpointConventionBuilder InnerAsConventionBuilder => inner; +#pragma warning restore CA1822 // Mark members as static + + public void Add(Action convention) => InnerAsConventionBuilder.Add(convention); + public void Finally(Action finallyConvention) => InnerAsConventionBuilder.Finally(finallyConvention); + } + + [AttributeUsage(AttributeTargets.Parameter)] + private sealed class FromBodyAttribute : Attribute, IFromBodyMetadata + { + } + + [AttributeUsage(AttributeTargets.Parameter)] + private sealed class FromServicesAttribute : Attribute, IFromServiceMetadata + { + } + + [AttributeUsage(AttributeTargets.Parameter)] + private sealed class FromQueryAttribute : Attribute, IFromQueryMetadata + { + public string? Name => null; + } +} diff --git a/src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs new file mode 100644 index 000000000000..c8dc72997a5f --- /dev/null +++ b/src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs @@ -0,0 +1,34 @@ +// 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 new file mode 100644 index 000000000000..e24ba288c345 --- /dev/null +++ b/src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs @@ -0,0 +1,78 @@ +// 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/IdentityConstants.cs b/src/Identity/Core/src/IdentityConstants.cs index 233dcecbd3e0..734b365d32ad 100644 --- a/src/Identity/Core/src/IdentityConstants.cs +++ b/src/Identity/Core/src/IdentityConstants.cs @@ -8,25 +8,35 @@ namespace Microsoft.AspNetCore.Identity; /// public class IdentityConstants { - private const string CookiePrefix = "Identity"; + private const string IdentityPrefix = "Identity"; /// /// The scheme used to identify application authentication cookies. /// - public static readonly string ApplicationScheme = CookiePrefix + ".Application"; + public static readonly string ApplicationScheme = IdentityPrefix + ".Application"; + + /// + /// The scheme used to identify bearer authentication tokens. + /// + public static readonly string BearerScheme = IdentityPrefix + ".Bearer"; + + /// + /// The scheme used to identify combination of and . + /// + internal const string BearerAndApplicationScheme = IdentityPrefix + ".BearerAndApplication"; /// /// The scheme used to identify external authentication cookies. /// - public static readonly string ExternalScheme = CookiePrefix + ".External"; + public static readonly string ExternalScheme = IdentityPrefix + ".External"; /// /// The scheme used to identify Two Factor authentication cookies for saving the Remember Me state. /// - public static readonly string TwoFactorRememberMeScheme = CookiePrefix + ".TwoFactorRememberMe"; + public static readonly string TwoFactorRememberMeScheme = IdentityPrefix + ".TwoFactorRememberMe"; /// /// The scheme used to identify Two Factor authentication cookies for round tripping user identities. /// - public static readonly string TwoFactorUserIdScheme = CookiePrefix + ".TwoFactorUserId"; + public static readonly string TwoFactorUserIdScheme = IdentityPrefix + ".TwoFactorUserId"; } diff --git a/src/Identity/Core/src/IdentityEndpointsJsonOptionsSetup.cs b/src/Identity/Core/src/IdentityEndpointsJsonOptionsSetup.cs new file mode 100644 index 000000000000..6ac875a8cc43 --- /dev/null +++ b/src/Identity/Core/src/IdentityEndpointsJsonOptionsSetup.cs @@ -0,0 +1,17 @@ +// 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.Http.Json; +using Microsoft.AspNetCore.Identity.DTO; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Identity; + +internal sealed class IdentityEndpointsJsonOptionsSetup : IConfigureOptions +{ + public void Configure(JsonOptions options) + { + // Put our resolver in front of the reflection-based one. See ProblemDetailsOptionsSetup for a detailed explanation. + options.SerializerOptions.TypeInfoResolverChain.Insert(0, IdentityEndpointsJsonSerializerContext.Default); + } +} diff --git a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj index 5dea4d49f49c..1b7c9cd62a97 100644 --- a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj +++ b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj @@ -10,8 +10,14 @@ true + + + + + + @@ -22,4 +28,9 @@ + + + + + diff --git a/src/Identity/Core/src/PublicAPI.Unshipped.txt b/src/Identity/Core/src/PublicAPI.Unshipped.txt index d1b2bbc0f0b4..8808ee11f130 100644 --- a/src/Identity/Core/src/PublicAPI.Unshipped.txt +++ b/src/Identity/Core/src/PublicAPI.Unshipped.txt @@ -1,2 +1,10 @@ #nullable enable +Microsoft.AspNetCore.Identity.IdentityApiEndpointsIdentityBuilderExtensions +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.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 readonly Microsoft.AspNetCore.Identity.IdentityConstants.BearerScheme -> string! virtual Microsoft.AspNetCore.Identity.SignInManager.IsTwoFactorEnabledAsync(TUser! user) -> System.Threading.Tasks.Task! diff --git a/src/Identity/Extensions.Core/src/SignInResult.cs b/src/Identity/Extensions.Core/src/SignInResult.cs index 68c546c483d8..01ae9864e2a8 100644 --- a/src/Identity/Extensions.Core/src/SignInResult.cs +++ b/src/Identity/Extensions.Core/src/SignInResult.cs @@ -81,7 +81,7 @@ public class SignInResult public override string ToString() { return IsLockedOut ? "Lockedout" : - IsNotAllowed ? "NotAllowed" : + IsNotAllowed ? "NotAllowed" : RequiresTwoFactor ? "RequiresTwoFactor" : Succeeded ? "Succeeded" : "Failed"; } diff --git a/src/Identity/Identity.slnf b/src/Identity/Identity.slnf index c2d577d70a4f..97b9364ac9e4 100644 --- a/src/Identity/Identity.slnf +++ b/src/Identity/Identity.slnf @@ -2,6 +2,8 @@ "solution": { "path": "..\\..\\AspNetCore.sln", "projects" : [ + "src\\Http\\Http.Extensions\\gen\\Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj", + "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj", "src\\Identity\\ApiAuthorization.IdentityServer\\samples\\ApiAuthSample\\ApiAuthSample.csproj", "src\\Identity\\ApiAuthorization.IdentityServer\\src\\Microsoft.AspNetCore.ApiAuthorization.IdentityServer.csproj", "src\\Identity\\ApiAuthorization.IdentityServer\\test\\Microsoft.AspNetCore.ApiAuthorization.IdentityServer.Tests.csproj", @@ -11,6 +13,7 @@ "src\\Identity\\EntityFrameworkCore\\test\\EF.Test\\Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test.csproj", "src\\Identity\\Extensions.Core\\src\\Microsoft.Extensions.Identity.Core.csproj", "src\\Identity\\Extensions.Stores\\src\\Microsoft.Extensions.Identity.Stores.csproj", + "src\\Identity\\samples\\IdentitySample.ApiEndpoints\\IdentitySample.ApiEndpoints.csproj", "src\\Identity\\samples\\IdentitySample.DefaultUI\\IdentitySample.DefaultUI.csproj", "src\\Identity\\samples\\IdentitySample.Mvc\\IdentitySample.Mvc.csproj", "src\\Identity\\Specification.Tests\\src\\Microsoft.AspNetCore.Identity.Specification.Tests.csproj", @@ -68,6 +71,7 @@ "src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj", "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj", "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj", + "src\\Security\\Authentication\\BearerToken\\src\\Microsoft.AspNetCore.Authentication.BearerToken.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", @@ -90,7 +94,8 @@ "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj", "src\\Localization\\Abstractions\\src\\Microsoft.Extensions.Localization.Abstractions.csproj", "src\\Components\\Web\\src\\Microsoft.AspNetCore.Components.Web.csproj", - "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj" + "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj", + "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj" ] } } diff --git a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj index 847aa53906d5..986c84729a81 100644 --- a/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj +++ b/src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj @@ -21,6 +21,7 @@ + diff --git a/src/Identity/samples/IdentitySample.ApiEndpoints/IdentitySample.ApiEndpoints.csproj b/src/Identity/samples/IdentitySample.ApiEndpoints/IdentitySample.ApiEndpoints.csproj new file mode 100644 index 000000000000..940addf2a293 --- /dev/null +++ b/src/Identity/samples/IdentitySample.ApiEndpoints/IdentitySample.ApiEndpoints.csproj @@ -0,0 +1,15 @@ + + + + Identity sample application on ASP.NET Core using endpoint routing + $(DefaultNetCoreTargetFramework) + + + + + + + + + + diff --git a/src/Identity/samples/IdentitySample.ApiEndpoints/Program.cs b/src/Identity/samples/IdentitySample.ApiEndpoints/Program.cs new file mode 100644 index 000000000000..b02e276b1775 --- /dev/null +++ b/src/Identity/samples/IdentitySample.ApiEndpoints/Program.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 System.Security.Claims; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; + +using var connection = new SqliteConnection("DataSource=:memory:"); +connection.Open(); + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddAuthorization(); + +builder.Services.AddDbContext( + options => options.UseSqlite(connection)); +builder.Services.AddIdentityApiEndpoints() + .AddEntityFrameworkStores(); + +var app = builder.Build(); + +app.MapGet("/", () => "Hello, World!"); +app.MapGet("/requires-auth", (ClaimsPrincipal user) => $"Hello, {user.Identity?.Name}!").RequireAuthorization(); + +app.MapGroup("/identity").MapIdentityApi(); + +app.Run(); +connection.Close(); + +public class ApplicationDbContext : IdentityDbContext +{ + public ApplicationDbContext(DbContextOptions options) : base(options) + { + Database.EnsureCreated(); + } +} diff --git a/src/Identity/samples/IdentitySample.ApiEndpoints/Properties/launchSettings.json b/src/Identity/samples/IdentitySample.ApiEndpoints/Properties/launchSettings.json new file mode 100644 index 000000000000..eb3fc1bc7cee --- /dev/null +++ b/src/Identity/samples/IdentitySample.ApiEndpoints/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "IdentitySample.ApiEndpoints": { + "commandName": "Project", + "launchBrowser": false, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:62313;http://localhost:62314" + } + } +} diff --git a/src/Identity/samples/IdentitySample.DefaultUI/.config/dotnet-tools.json b/src/Identity/samples/IdentitySample.DefaultUI/.config/dotnet-tools.json index f6a8270a15d5..3aca4744eb47 100644 --- a/src/Identity/samples/IdentitySample.DefaultUI/.config/dotnet-tools.json +++ b/src/Identity/samples/IdentitySample.DefaultUI/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "3.0.0-preview3.19153.1", + "version": "7.0.4", "commands": [ "dotnet-ef" ] diff --git a/src/Identity/samples/IdentitySample.DefaultUI/Data/ApplicationDbContext.cs b/src/Identity/samples/IdentitySample.DefaultUI/Data/ApplicationDbContext.cs index ae1cf417d0ad..4366e15ecef0 100644 --- a/src/Identity/samples/IdentitySample.DefaultUI/Data/ApplicationDbContext.cs +++ b/src/Identity/samples/IdentitySample.DefaultUI/Data/ApplicationDbContext.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// 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.Identity.EntityFrameworkCore; @@ -10,5 +10,6 @@ public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions options) : base(options) { + Database.EnsureCreated(); } } diff --git a/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs b/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs new file mode 100644 index 000000000000..4c58725dfd6a --- /dev/null +++ b/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs @@ -0,0 +1,248 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System.Net; +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 +{ + private string Username { get; } = $"{Guid.NewGuid()}@example.com"; + private string Password { get; } = $"[PLACEHOLDER]-1a"; + + [Theory] + [MemberData(nameof(AddIdentityModes))] + public async Task CanRegisterUser(string addIdentityMode) + { + await using var app = await CreateAppAsync(AddIdentityActions[addIdentityMode]); + using var client = app.GetTestClient(); + + var response = await client.PostAsJsonAsync("/identity/register", new { Username, Password }); + + response.EnsureSuccessStatusCode(); + Assert.Equal(0, response.Content.Headers.ContentLength); + } + + [Theory] + [MemberData(nameof(AddIdentityModes))] + public async Task CanLoginWithBearerToken(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 }); + + loginResponse.EnsureSuccessStatusCode(); + Assert.False(loginResponse.Headers.Contains(HeaderNames.SetCookie)); + + var loginContent = await loginResponse.Content.ReadFromJsonAsync(); + var tokenType = loginContent.GetProperty("token_type").GetString(); + var accessToken = loginContent.GetProperty("access_token").GetString(); + var expiresIn = loginContent.GetProperty("expires_in").GetDouble(); + + Assert.Equal("Bearer", tokenType); + Assert.Equal(3600, expiresIn); + + client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); + Assert.Equal($"Hello, {Username}!", await client.GetStringAsync("/auth/hello")); + } + + [Fact] + public async Task CanCustomizeBearerTokenExpiration() + { + var clock = new TestTimeProvider(); + var expireTimeSpan = TimeSpan.FromSeconds(42); + + await using var app = await CreateAppAsync(services => + { + services.AddIdentityCore().AddApiEndpoints().AddEntityFrameworkStores(); + services.AddAuthentication(IdentityConstants.BearerScheme).AddBearerToken(IdentityConstants.BearerScheme, options => + { + options.BearerTokenExpiration = expireTimeSpan; + options.TimeProvider = clock; + }); + }); + + 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 accessToken = loginContent.GetProperty("access_token").GetString(); + var expiresIn = loginContent.GetProperty("expires_in").GetDouble(); + + Assert.Equal(expireTimeSpan.TotalSeconds, expiresIn); + + client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); + + // Works without time passing. + Assert.Equal($"Hello, {Username}!", await client.GetStringAsync("/auth/hello")); + + clock.Advance(TimeSpan.FromSeconds(expireTimeSpan.TotalSeconds - 1)); + + // Still works without 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); + } + + [Fact] + public async Task CanLoginWithCookies() + { + await using var app = await CreateAppAsync(); + using var client = app.GetTestClient(); + + await client.PostAsJsonAsync("/identity/register", new { Username, Password }); + var loginResponse = await client.PostAsJsonAsync("/identity/login?cookieMode=true", new { Username, Password }); + + loginResponse.EnsureSuccessStatusCode(); + Assert.Equal(0, loginResponse.Content.Headers.ContentLength); + + Assert.True(loginResponse.Headers.TryGetValues(HeaderNames.SetCookie, out var setCookieHeaders)); + var setCookieHeader = Assert.Single(setCookieHeaders); + + // The compiler does not see Assert.True's DoesNotReturnIfAttribute :( + if (setCookieHeader.Split(';', 2) is not [var cookieHeader, _]) + { + throw new Exception("Invalid Set-Cookie header!"); + } + + client.DefaultRequestHeaders.Add(HeaderNames.Cookie, cookieHeader); + Assert.Equal($"Hello, {Username}!", await client.GetStringAsync("/auth/hello")); + } + + [Fact] + public async Task CannotLoginWithCookiesWithOnlyCoreServices() + { + await using var app = await CreateAppAsync(AddIdentityEndpointsBearerOnly); + using var client = app.GetTestClient(); + + await client.PostAsJsonAsync("/identity/register", new { Username, Password }); + + await Assert.ThrowsAsync(() + => client.PostAsJsonAsync("/identity/login?cookieMode=true", new { Username, Password })); + } + + [Fact] + public async Task CanReadBearerTokenFromQueryString() + { + await using var app = await CreateAppAsync(services => + { + services.AddIdentityCore().AddApiEndpoints().AddEntityFrameworkStores(); + services.AddAuthentication(IdentityConstants.BearerScheme).AddBearerToken(IdentityConstants.BearerScheme, options => + { + options.Events.OnMessageReceived = context => + { + context.Token = (string?)context.Request.Query["access_token"]; + return Task.CompletedTask; + }; + }); + }); + + 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 accessToken = loginContent.GetProperty("access_token").GetString(); + + Assert.Equal($"Hello, {Username}!", await client.GetStringAsync($"/auth/hello?access_token={accessToken}")); + + // The normal header still works + client.DefaultRequestHeaders.Authorization = new("Bearer", accessToken); + Assert.Equal($"Hello, {Username}!", await client.GetStringAsync($"/auth/hello")); + } + + [Theory] + [MemberData(nameof(AddIdentityModes))] + public async Task Returns401UnauthorizedStatusGivenNoBearerTokenOrCookie(string addIdentityMode) + { + 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); + } + + private async Task CreateAppAsync(Action? configureServices) + where TUser : class, new() + where TContext : DbContext + { + var builder = WebApplication.CreateSlimBuilder(); + builder.WebHost.UseTestServer(); + builder.Services.AddSingleton(LoggerFactory); + builder.Services.AddAuthorization(); + + var dbConnection = new SqliteConnection($"DataSource=:memory:"); + builder.Services.AddDbContext(options => options.UseSqlite(dbConnection)); + // Dispose SqliteConnection with host by registering as a singleton factory. + builder.Services.AddSingleton(() => dbConnection); + + configureServices ??= AddIdentityEndpoints; + configureServices(builder.Services); + + var app = builder.Build(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapGroup("/identity").MapIdentityApi(); + + var authGroup = app.MapGroup("/auth").RequireAuthorization(); + authGroup.MapGet("/hello", + (ClaimsPrincipal user) => $"Hello, {user.Identity?.Name}!"); + + await dbConnection.OpenAsync(); + await app.Services.GetRequiredService().Database.EnsureCreatedAsync(); + await app.StartAsync(); + + return app; + } + + private static void AddIdentityEndpoints(IServiceCollection services) + => services.AddIdentityApiEndpoints().AddEntityFrameworkStores(); + + private static void AddIdentityEndpointsBearerOnly(IServiceCollection services) + { + services.AddIdentityCore().AddEntityFrameworkStores(); + services.AddAuthentication(IdentityConstants.BearerScheme).AddBearerToken(IdentityConstants.BearerScheme); + } + + private Task CreateAppAsync(Action? configureServices = null) + => CreateAppAsync(configureServices); + + private static Dictionary> AddIdentityActions { get; } = new() + { + [nameof(AddIdentityEndpoints)] = AddIdentityEndpoints, + [nameof(AddIdentityEndpointsBearerOnly)] = AddIdentityEndpointsBearerOnly, + }; + + public static object[][] AddIdentityModes => AddIdentityActions.Keys.Select(key => new object[] { key }).ToArray(); +} diff --git a/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj b/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj index 6f9ae54776ad..0b19674121ee 100644 --- a/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj +++ b/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenDefaults.cs b/src/Security/Authentication/BearerToken/src/BearerTokenDefaults.cs new file mode 100644 index 000000000000..67ee642396c7 --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/BearerTokenDefaults.cs @@ -0,0 +1,15 @@ +// 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.Authentication.BearerToken; + +/// +/// Default values used by bearer token authentication. +/// +public static class BearerTokenDefaults +{ + /// + /// Default value for AuthenticationScheme property in the . + /// + public const string AuthenticationScheme = "BearerToken"; +} diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs b/src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs new file mode 100644 index 000000000000..f2662caaff2d --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs @@ -0,0 +1,20 @@ +// 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.Authentication.BearerToken; + +/// +/// Specifies events which the bearer token handler invokes to enable developer control over the authentication process. +/// +public class BearerTokenEvents +{ + /// + /// Invoked when a protocol message is first received. + /// + public Func OnMessageReceived { get; set; } = context => Task.CompletedTask; + + /// + /// Invoked when a protocol message is first received. + /// + public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context); +} diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs b/src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs new file mode 100644 index 000000000000..0ac96986919d --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs @@ -0,0 +1,67 @@ +// 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; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods to configure the bearer token authentication. +/// +public static class BearerTokenExtensions +{ + /// + /// Adds bearer token authentication. The default scheme is specified by . + /// + /// Bearer tokens can be obtained by calling . + /// + /// + /// The . + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder) + => builder.AddBearerToken(BearerTokenDefaults.AuthenticationScheme); + + /// + /// Adds bearer token authentication. + /// + /// Bearer tokens can be obtained by calling . + /// + /// + /// The . + /// The authentication scheme. + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, string authenticationScheme) + => builder.AddBearerToken(authenticationScheme, _ => { }); + + /// + /// Adds bearer token authentication. The default scheme is specified by . + /// + /// Bearer tokens can be obtained by calling . + /// + /// + /// The . + /// Action used to configure the bearer token authentication options. + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, Action configure) + => builder.AddBearerToken(BearerTokenDefaults.AuthenticationScheme, configure); + + /// + /// Adds bearer token authentication. + /// + /// Bearer tokens can be obtained by calling . + /// + /// + /// The . + /// The authentication scheme. + /// Action used to configure the bearer token authentication options. + /// A reference to after the operation has completed. + public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, string authenticationScheme, Action configure) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(authenticationScheme); + ArgumentNullException.ThrowIfNull(configure); + + return builder.AddScheme(authenticationScheme, configure); + } +} diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs b/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs new file mode 100644 index 000000000000..72c451681b7a --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +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; +using Microsoft.Net.Http.Headers; + +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 + : SignInAuthenticationHandler(optionsMonitor, loggerFactory, urlEncoder) +{ + private const string BearerTokenPurpose = $"Microsoft.AspNetCore.Authentication.BearerToken:v1:BearerToken"; + + 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() + { + // Give application opportunity to find from a different location, adjust, or reject token + var messageReceivedContext = new MessageReceivedContext(Context, Scheme, Options); + + await Events.MessageReceived(messageReceivedContext); + + if (messageReceivedContext.Result != null) + { + return messageReceivedContext.Result; + } + + var token = messageReceivedContext.Token ?? GetBearerTokenOrNull(); + + if (token is null) + { + return AuthenticateResult.NoResult(); + } + + var ticket = BearerTokenProtector.Unprotect(token); + + if (ticket?.Properties?.ExpiresUtc is null) + { + return FailedUnprotectingToken; + } + + if (TimeProvider.GetUtcNow() >= ticket.Properties.ExpiresUtc) + { + return TokenExpired; + } + + return AuthenticateResult.Success(ticket); + } + + protected override async Task HandleChallengeAsync(AuthenticationProperties properties) + { + Response.Headers.Append(HeaderNames.WWWAuthenticate, "Bearer"); + await base.HandleChallengeAsync(properties); + } + + protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) + { + long expiresInTotalSeconds; + var utcNow = TimeProvider.GetUtcNow(); + + properties ??= new(); + + if (properties.ExpiresUtc is null) + { + properties.ExpiresUtc ??= utcNow + Options.BearerTokenExpiration; + expiresInTotalSeconds = (long)Options.BearerTokenExpiration.TotalSeconds; + } + else + { + 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, + }; + + return Context.Response.WriteAsJsonAsync(accessTokenResponse, BearerTokenJsonSerializerContext.Default.AccessTokenResponse); + } + + // No-op to avoid interfering with any mass sign-out logic. + protected override Task HandleSignOutAsync(AuthenticationProperties? properties) => Task.CompletedTask; + + private string? GetBearerTokenOrNull() + { + var authorization = Request.Headers.Authorization.ToString(); + + return authorization.StartsWith("Bearer ", StringComparison.Ordinal) + ? authorization["Bearer ".Length..] + : null; + } +} diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenJsonSerializerContext.cs b/src/Security/Authentication/BearerToken/src/BearerTokenJsonSerializerContext.cs new file mode 100644 index 000000000000..b1ceb316d69e --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/BearerTokenJsonSerializerContext.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.AspNetCore.Authentication.BearerToken.DTO; + +[JsonSerializable(typeof(AccessTokenResponse))] +internal sealed partial class BearerTokenJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs b/src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs new file mode 100644 index 000000000000..b62fe134bd41 --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs @@ -0,0 +1,46 @@ +// 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; + +namespace Microsoft.AspNetCore.Authentication.BearerToken; + +/// +/// Contains the options used to authenticate using opaque bearer tokens. +/// +public sealed class BearerTokenOptions : AuthenticationSchemeOptions +{ + /// + /// Constructs the options used to authenticate using opaque bearer tokens. + /// + public BearerTokenOptions() + { + Events = new(); + } + + /// + /// Controls how much time the bearer token will remain valid from the point it is created. + /// 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. + /// + public TimeSpan BearerTokenExpiration { get; set; } = TimeSpan.FromHours(1); + + /// + /// 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 + /// from the application . + /// + public ISecureDataFormat? BearerTokenProtector { get; set; } + + /// + /// The object provided by the application to process events raised by the bearer token authentication handler. + /// The application may implement the interface fully, or it may create an instance of + /// and assign delegates only to the events it wants to process. + /// + public new BearerTokenEvents Events + { + get { return (BearerTokenEvents)base.Events!; } + set { base.Events = value; } + } +} + diff --git a/src/Security/Authentication/BearerToken/src/MessageReceivedContext.cs b/src/Security/Authentication/BearerToken/src/MessageReceivedContext.cs new file mode 100644 index 000000000000..27777dbe0195 --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/MessageReceivedContext.cs @@ -0,0 +1,27 @@ +// 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.Http; + +namespace Microsoft.AspNetCore.Authentication.BearerToken; + +/// +/// A context for . +/// +public class MessageReceivedContext : ResultContext +{ + /// + /// Initializes a new instance of . + /// + /// + public MessageReceivedContext( + HttpContext context, + AuthenticationScheme scheme, + BearerTokenOptions options) + : base(context, scheme, options) { } + + /// + /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location. + /// + public string? Token { get; set; } +} diff --git a/src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj b/src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj new file mode 100644 index 000000000000..0b34a4b224dd --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj @@ -0,0 +1,20 @@ + + + + ASP.NET Core Identity Endpoints defines endpoints for HTTP clients to login and interact with the ASP.NET Core Identity framework. + $(DefaultNetCoreTargetFramework) + true + true + aspnetcore;authentication;security + true + true + + + + + + + + + + diff --git a/src/Security/Authentication/BearerToken/src/PublicAPI.Shipped.txt b/src/Security/Authentication/BearerToken/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..7dc5c58110bf --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..1765bd328d8e --- /dev/null +++ b/src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt @@ -0,0 +1,25 @@ +#nullable enable +const Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenDefaults.AuthenticationScheme = "BearerToken" -> string! +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenDefaults +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenEvents +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenEvents.BearerTokenEvents() -> void +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenEvents.OnMessageReceived.get -> System.Func! +Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenEvents.OnMessageReceived.set -> void +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.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.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? +Microsoft.AspNetCore.Authentication.BearerToken.MessageReceivedContext.Token.set -> void +Microsoft.Extensions.DependencyInjection.BearerTokenExtensions +static Microsoft.Extensions.DependencyInjection.BearerTokenExtensions.AddBearerToken(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder! +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! diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerDefaults.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerDefaults.cs index 92349779fde8..73dcc697bf45 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerDefaults.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerDefaults.cs @@ -4,12 +4,12 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer; /// -/// Default values used by bearer authentication. +/// Default values used by for JWT bearer authentication. /// public static class JwtBearerDefaults { /// - /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions + /// Default value for AuthenticationScheme property in the . /// public const string AuthenticationScheme = "Bearer"; } diff --git a/src/Security/Authentication/test/BearerTokenTests.cs b/src/Security/Authentication/test/BearerTokenTests.cs new file mode 100644 index 000000000000..08ee3bb8511e --- /dev/null +++ b/src/Security/Authentication/test/BearerTokenTests.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authentication.BearerToken; + +public class BearerTokenTests : SharedAuthenticationTests +{ + protected override string DefaultScheme => BearerTokenDefaults.AuthenticationScheme; + + protected override Type HandlerType + { + get + { + var services = new ServiceCollection(); + services.AddAuthentication().AddBearerToken(); + return services.Select(d => d.ServiceType).Single(typeof(AuthenticationHandler).IsAssignableFrom); + } + } + + protected override void RegisterAuth(AuthenticationBuilder services, Action configure) + { + services.AddBearerToken(configure); + } +} diff --git a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj index 72dad5f713a3..2d5f2deb7350 100644 --- a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj +++ b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Security/Security.slnf b/src/Security/Security.slnf index 09f0ab95125b..257bb33f6c23 100644 --- a/src/Security/Security.slnf +++ b/src/Security/Security.slnf @@ -28,6 +28,7 @@ "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj", + "src\\Security\\Authentication\\BearerToken\\src\\Microsoft.AspNetCore.Authentication.BearerToken.csproj", "src\\Security\\Authentication\\Certificate\\samples\\Certificate.Optional.Sample\\Certificate.Optional.Sample.csproj", "src\\Security\\Authentication\\Certificate\\samples\\Certificate.Sample\\Certificate.Sample.csproj", "src\\Security\\Authentication\\Certificate\\src\\Microsoft.AspNetCore.Authentication.Certificate.csproj", diff --git a/src/Shared/BearerToken/DTO/AccessTokenResponse.cs b/src/Shared/BearerToken/DTO/AccessTokenResponse.cs new file mode 100644 index 000000000000..04a7fb75374e --- /dev/null +++ b/src/Shared/BearerToken/DTO/AccessTokenResponse.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.AspNetCore.Authentication.BearerToken.DTO; + +internal sealed class AccessTokenResponse +{ + [JsonPropertyName("token_type")] + public string TokenType { get; } = "Bearer"; + + [JsonPropertyName("access_token")] + public required string AccessToken { get; init; } + + [JsonPropertyName("expires_in")] + public required long ExpiresInTotalSeconds { get; init; } +} diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf index 63b7b26c1370..7a09783caf7f 100644 --- a/src/Tools/Tools.slnf +++ b/src/Tools/Tools.slnf @@ -75,6 +75,7 @@ "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", "src\\Middleware\\WebSockets\\src\\Microsoft.AspNetCore.WebSockets.csproj", "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj", + "src\\Security\\Authentication\\BearerToken\\src\\Microsoft.AspNetCore.Authentication.BearerToken.csproj", "src\\Security\\Authentication\\Certificate\\src\\Microsoft.AspNetCore.Authentication.Certificate.csproj", "src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj", "src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj", From a7edf84c55de961fb88ddaf2141f28f3e2867a96 Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Wed, 26 Apr 2023 22:25:41 -0700 Subject: [PATCH 2/3] Fix CI --- eng/SharedFramework.Local.props | 2 +- eng/TrimmableProjects.props | 1 - src/Framework/test/TestData.cs | 2 ++ .../Core/src/IdentityApiEndpointRouteBuilderExtensions.cs | 3 +++ src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj | 7 ++++--- .../Microsoft.AspNetCore.Authentication.BearerToken.csproj | 4 ++-- 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 007ad96a9f6a..8208597edbc4 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -17,7 +17,6 @@ - @@ -64,6 +63,7 @@ + diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index ef28ab2b815d..4646bb59dc70 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -32,7 +32,6 @@ - diff --git a/src/Framework/test/TestData.cs b/src/Framework/test/TestData.cs index b657f5d16b61..98de16bb3587 100644 --- a/src/Framework/test/TestData.cs +++ b/src/Framework/test/TestData.cs @@ -22,6 +22,7 @@ static TestData() "Microsoft.AspNetCore.Antiforgery", "Microsoft.AspNetCore.Authentication", "Microsoft.AspNetCore.Authentication.Abstractions", + "Microsoft.AspNetCore.Authentication.BearerToken", "Microsoft.AspNetCore.Authentication.Cookies", "Microsoft.AspNetCore.Authentication.Core", "Microsoft.AspNetCore.Authentication.OAuth", @@ -168,6 +169,7 @@ static TestData() { { "Microsoft.AspNetCore.Antiforgery" }, { "Microsoft.AspNetCore.Authentication.Abstractions" }, + { "Microsoft.AspNetCore.Authentication.BearerToken" }, { "Microsoft.AspNetCore.Authentication.Cookies" }, { "Microsoft.AspNetCore.Authentication.Core" }, { "Microsoft.AspNetCore.Authentication.OAuth" }, diff --git a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs index 11a40a5e74be..45f0724dfcf0 100644 --- a/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs +++ b/src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs @@ -1,6 +1,7 @@ // 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.Linq; using Microsoft.AspNetCore.Authentication.BearerToken.DTO; using Microsoft.AspNetCore.Builder; @@ -27,6 +28,8 @@ public static class IdentityApiEndpointRouteBuilderExtensions /// Call to add a prefix to all the endpoints. /// /// An to further customize the added endpoints. + // TODO: Remove RequiresDynamicCode when https://github.com/dotnet/aspnetcore/issues/47918 is fixed and RDG is enabled. + [RequiresDynamicCode("This API requires generated code that is not compatible with native AOT applications.")] public static IEndpointConventionBuilder MapIdentityApi(this IEndpointRouteBuilder endpoints) where TUser : class, new() { ArgumentNullException.ThrowIfNull(endpoints); diff --git a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj index 1b7c9cd62a97..c86f186db971 100644 --- a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj +++ b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj @@ -7,7 +7,8 @@ true aspnetcore;identity;membership false - true + + @@ -30,7 +31,7 @@ - - + + diff --git a/src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj b/src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj index 0b34a4b224dd..885c874bf950 100644 --- a/src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj +++ b/src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj @@ -1,13 +1,13 @@ - ASP.NET Core Identity Endpoints defines endpoints for HTTP clients to login and interact with the ASP.NET Core Identity framework. + ASP.NET Core authentication handler that enables an application to receive an opaque bearer token. $(DefaultNetCoreTargetFramework) true true aspnetcore;authentication;security + false true - true From 5c5a9f2a3c2483109ea85b5c3a46faa3cc25f8de Mon Sep 17 00:00:00 2001 From: Stephen Halter Date: Thu, 27 Apr 2023 11:00:57 -0700 Subject: [PATCH 3/3] Revert TimeProvider reaction --- .../IdentityApiEndpointsServiceCollectionExtensions.cs | 4 ++-- .../test/Identity.FunctionalTests/MapIdentityTests.cs | 8 ++++---- .../Microsoft.AspNetCore.Identity.FunctionalTests.csproj | 2 +- .../Authentication/BearerToken/src/BearerTokenHandler.cs | 7 ++++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs b/src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs index e24ba288c345..d97f2adadc54 100644 --- a/src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs +++ b/src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs @@ -58,8 +58,8 @@ public static IdentityBuilder AddIdentityApiEndpoints(this IServiceCollec .AddApiEndpoints(); } - private sealed class CompositeIdentityHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) - : PolicySchemeHandler(options, logger, encoder) + private sealed class CompositeIdentityHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) + : PolicySchemeHandler(options, logger, encoder, clock) { protected override async Task HandleAuthenticateAsync() { diff --git a/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs b/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs index 4c58725dfd6a..b9c3abd69a88 100644 --- a/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs +++ b/src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs @@ -70,7 +70,7 @@ public async Task CanLoginWithBearerToken(string addIdentityMode) [Fact] public async Task CanCustomizeBearerTokenExpiration() { - var clock = new TestTimeProvider(); + var clock = new TestClock(); var expireTimeSpan = TimeSpan.FromSeconds(42); await using var app = await CreateAppAsync(services => @@ -79,8 +79,8 @@ public async Task CanCustomizeBearerTokenExpiration() services.AddAuthentication(IdentityConstants.BearerScheme).AddBearerToken(IdentityConstants.BearerScheme, options => { options.BearerTokenExpiration = expireTimeSpan; - options.TimeProvider = clock; }); + services.AddSingleton(clock); }); using var client = app.GetTestClient(); @@ -99,12 +99,12 @@ public async Task CanCustomizeBearerTokenExpiration() // Works without time passing. Assert.Equal($"Hello, {Username}!", await client.GetStringAsync("/auth/hello")); - clock.Advance(TimeSpan.FromSeconds(expireTimeSpan.TotalSeconds - 1)); + clock.Add(TimeSpan.FromSeconds(expireTimeSpan.TotalSeconds - 1)); // Still works without one second before expiration. Assert.Equal($"Hello, {Username}!", await client.GetStringAsync("/auth/hello")); - clock.Advance(TimeSpan.FromSeconds(1)); + clock.Add(TimeSpan.FromSeconds(1)); var unauthorizedResponse = await client.GetAsync("/auth/hello"); // Fails the second the BearerTokenExpiration elapses. diff --git a/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj b/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj index 0b19674121ee..7f62606cea10 100644 --- a/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj +++ b/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs b/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs index 72c451681b7a..fce7cc6d48bc 100644 --- a/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs +++ b/src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs @@ -16,10 +16,11 @@ internal sealed class BearerTokenHandler( IOptionsMonitor optionsMonitor, ILoggerFactory loggerFactory, UrlEncoder urlEncoder, + ISystemClock clock, #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 - : SignInAuthenticationHandler(optionsMonitor, loggerFactory, urlEncoder) + : SignInAuthenticationHandler(optionsMonitor, loggerFactory, urlEncoder, clock) { private const string BearerTokenPurpose = $"Microsoft.AspNetCore.Authentication.BearerToken:v1:BearerToken"; @@ -57,7 +58,7 @@ protected override async Task HandleAuthenticateAsync() return FailedUnprotectingToken; } - if (TimeProvider.GetUtcNow() >= ticket.Properties.ExpiresUtc) + if (Clock.UtcNow >= ticket.Properties.ExpiresUtc) { return TokenExpired; } @@ -74,7 +75,7 @@ protected override async Task HandleChallengeAsync(AuthenticationProperties prop protected override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) { long expiresInTotalSeconds; - var utcNow = TimeProvider.GetUtcNow(); + var utcNow = Clock.UtcNow; properties ??= new();