Browse Source

Add MapIdentityApi<TUser>() (#47414)

Stephen Halter 2 years ago
parent
commit
d4430f0d7d
41 changed files with 1069 additions and 14 deletions
  1. 42 1
      AspNetCore.sln
  2. 1 0
      eng/ProjectReferences.props
  3. 1 0
      eng/SharedFramework.Local.props
  4. 1 1
      eng/TrimmableProjects.props
  5. 2 0
      src/Framework/test/TestData.cs
  6. 12 0
      src/Identity/Core/src/DTO/IdentityEndpointsJsonSerializerContext.cs
  7. 10 0
      src/Identity/Core/src/DTO/LoginRequest.cs
  8. 10 0
      src/Identity/Core/src/DTO/RegisterRequest.cs
  9. 107 0
      src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs
  10. 34 0
      src/Identity/Core/src/IdentityApiEndpointsIdentityBuilderExtensions.cs
  11. 78 0
      src/Identity/Core/src/IdentityApiEndpointsServiceCollectionExtensions.cs
  12. 15 5
      src/Identity/Core/src/IdentityConstants.cs
  13. 17 0
      src/Identity/Core/src/IdentityEndpointsJsonOptionsSetup.cs
  14. 13 1
      src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj
  15. 8 0
      src/Identity/Core/src/PublicAPI.Unshipped.txt
  16. 1 1
      src/Identity/Extensions.Core/src/SignInResult.cs
  17. 6 1
      src/Identity/Identity.slnf
  18. 1 0
      src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj
  19. 15 0
      src/Identity/samples/IdentitySample.ApiEndpoints/IdentitySample.ApiEndpoints.csproj
  20. 38 0
      src/Identity/samples/IdentitySample.ApiEndpoints/Program.cs
  21. 12 0
      src/Identity/samples/IdentitySample.ApiEndpoints/Properties/launchSettings.json
  22. 1 1
      src/Identity/samples/IdentitySample.DefaultUI/.config/dotnet-tools.json
  23. 2 1
      src/Identity/samples/IdentitySample.DefaultUI/Data/ApplicationDbContext.cs
  24. 248 0
      src/Identity/test/Identity.FunctionalTests/MapIdentityTests.cs
  25. 1 0
      src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj
  26. 15 0
      src/Security/Authentication/BearerToken/src/BearerTokenDefaults.cs
  27. 20 0
      src/Security/Authentication/BearerToken/src/BearerTokenEvents.cs
  28. 67 0
      src/Security/Authentication/BearerToken/src/BearerTokenExtensions.cs
  29. 112 0
      src/Security/Authentication/BearerToken/src/BearerTokenHandler.cs
  30. 11 0
      src/Security/Authentication/BearerToken/src/BearerTokenJsonSerializerContext.cs
  31. 46 0
      src/Security/Authentication/BearerToken/src/BearerTokenOptions.cs
  32. 27 0
      src/Security/Authentication/BearerToken/src/MessageReceivedContext.cs
  33. 20 0
      src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj
  34. 1 0
      src/Security/Authentication/BearerToken/src/PublicAPI.Shipped.txt
  35. 25 0
      src/Security/Authentication/BearerToken/src/PublicAPI.Unshipped.txt
  36. 2 2
      src/Security/Authentication/JwtBearer/src/JwtBearerDefaults.cs
  37. 26 0
      src/Security/Authentication/test/BearerTokenTests.cs
  38. 1 0
      src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj
  39. 1 0
      src/Security/Security.slnf
  40. 18 0
      src/Shared/BearerToken/DTO/AccessTokenResponse.cs
  41. 1 0
      src/Tools/Tools.slnf

+ 42 - 1
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}

+ 1 - 0
eng/ProjectReferences.props

@@ -53,6 +53,7 @@
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.NamedPipes\src\Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Quic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Sockets\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" />
+    <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.BearerToken" ProjectPath="$(RepoRoot)src\Security\Authentication\BearerToken\src\Microsoft.AspNetCore.Authentication.BearerToken.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Certificate" ProjectPath="$(RepoRoot)src\Security\Authentication\Certificate\src\Microsoft.AspNetCore.Authentication.Certificate.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Cookies" ProjectPath="$(RepoRoot)src\Security\Authentication\Cookies\src\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication" ProjectPath="$(RepoRoot)src\Security\Authentication\Core\src\Microsoft.AspNetCore.Authentication.csproj" />

+ 1 - 0
eng/SharedFramework.Local.props

@@ -63,6 +63,7 @@
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" />
+    <AspNetCoreAppReference Include="Microsoft.AspNetCore.Authentication.BearerToken" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Authentication.Cookies" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Authentication" />
     <AspNetCoreAppReference Include="Microsoft.AspNetCore.Authentication.OAuth" />

+ 1 - 1
eng/TrimmableProjects.props

@@ -32,7 +32,6 @@
     <TrimmableProject Include="Microsoft.AspNetCore.Routing" />
     <TrimmableProject Include="Microsoft.AspNetCore.WebUtilities" />
     <TrimmableProject Include="Microsoft.AspNetCore.Html.Abstractions" />
-    <TrimmableProject Include="Microsoft.AspNetCore.Identity" />
     <TrimmableProject Include="Microsoft.Extensions.Identity.Core" />
     <TrimmableProject Include="Microsoft.Extensions.Identity.Stores" />
     <TrimmableProject Include="Microsoft.AspNetCore.Connections.Abstractions" />
@@ -44,6 +43,7 @@
     <TrimmableProject Include="Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes" />
     <TrimmableProject Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" />
     <TrimmableProject Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" />
+    <TrimmableProject Include="Microsoft.AspNetCore.Authentication.BearerToken" />
     <TrimmableProject Include="Microsoft.AspNetCore.Authentication.Certificate" />
     <TrimmableProject Include="Microsoft.AspNetCore.Authentication.Cookies" />
     <TrimmableProject Include="Microsoft.AspNetCore.Authentication" />

+ 2 - 0
src/Framework/test/TestData.cs

@@ -22,6 +22,7 @@ public static class 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 @@ public static class TestData
             {
                 { "Microsoft.AspNetCore.Antiforgery" },
                 { "Microsoft.AspNetCore.Authentication.Abstractions" },
+                { "Microsoft.AspNetCore.Authentication.BearerToken" },
                 { "Microsoft.AspNetCore.Authentication.Cookies" },
                 { "Microsoft.AspNetCore.Authentication.Core" },
                 { "Microsoft.AspNetCore.Authentication.OAuth" },

+ 12 - 0
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
+{
+}

+ 10 - 0
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; }
+}

+ 10 - 0
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; }
+}

+ 107 - 0
src/Identity/Core/src/IdentityApiEndpointRouteBuilderExtensions.cs

@@ -0,0 +1,107 @@
+// 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;
+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;
+
+/// <summary>
+/// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add identity endpoints.
+/// </summary>
+public static class IdentityApiEndpointRouteBuilderExtensions
+{
+    /// <summary>
+    /// Add endpoints for registering, logging in, and logging out using ASP.NET Core Identity.
+    /// </summary>
+    /// <typeparam name="TUser">The type describing the user. This should match the generic parameter in <see cref="UserManager{TUser}"/>.</typeparam>
+    /// <param name="endpoints">
+    /// The <see cref="IEndpointRouteBuilder"/> to add the identity endpoints to.
+    /// Call <see cref="EndpointRouteBuilderExtensions.MapGroup(IEndpointRouteBuilder, string)"/> to add a prefix to all the endpoints.
+    /// </param>
+    /// <returns>An <see cref="IEndpointConventionBuilder"/> to further customize the added endpoints.</returns>
+    // 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<TUser>(this IEndpointRouteBuilder endpoints) where TUser : class, new()
+    {
+        ArgumentNullException.ThrowIfNull(endpoints);
+
+        var routeGroup = endpoints.MapGroup("");
+
+        // NOTE: We cannot inject UserManager<TUser> directly because the TUser generic parameter is currently unsupported by RDG.
+        // https://github.com/dotnet/aspnetcore/issues/47338
+        routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
+            ([FromBody] RegisterRequest registration, [FromServices] IServiceProvider services) =>
+        {
+            var userManager = services.GetRequiredService<UserManager<TUser>>();
+
+            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<Results<UnauthorizedHttpResult, Ok<AccessTokenResponse>, SignInHttpResult>>
+            ([FromBody] LoginRequest login, [FromQuery] bool? cookieMode, [FromServices] IServiceProvider services) =>
+        {
+            var userManager = services.GetRequiredService<UserManager<TUser>>();
+            var user = await userManager.FindByNameAsync(login.Username);
+
+            if (user is null || !await userManager.CheckPasswordAsync(user, login.Password))
+            {
+                return TypedResults.Unauthorized();
+            }
+
+            var claimsFactory = services.GetRequiredService<IUserClaimsPrincipalFactory<TUser>>();
+            var 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<EndpointBuilder> convention) => InnerAsConventionBuilder.Add(convention);
+        public void Finally(Action<EndpointBuilder> 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;
+    }
+}

+ 34 - 0
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;
+
+/// <summary>
+/// <see cref="IdentityBuilder"/> extension methods to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>.
+/// </summary>
+public static class IdentityApiEndpointsIdentityBuilderExtensions
+{
+    /// <summary>
+    /// Adds configuration ans services needed to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
+    /// but does not configure authentication. Call <see cref="BearerTokenExtensions.AddBearerToken(AuthenticationBuilder, Action{BearerTokenOptions}?)"/> and/or
+    /// <see cref="IdentityCookieAuthenticationBuilderExtensions.AddIdentityCookies(AuthenticationBuilder)"/> to configure authentication separately.
+    /// </summary>
+    /// <param name="builder">The <see cref="IdentityBuilder"/>.</param>
+    /// <returns>The <see cref="IdentityBuilder"/>.</returns>
+    public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder)
+    {
+        ArgumentNullException.ThrowIfNull(builder);
+
+        builder.AddSignInManager();
+        builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JsonOptions>, IdentityEndpointsJsonOptionsSetup>());
+        return builder;
+    }
+}

+ 78 - 0
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;
+
+/// <summary>
+/// Default extensions to <see cref="IServiceCollection"/> for <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>.
+/// </summary>
+public static class IdentityApiEndpointsServiceCollectionExtensions
+{
+    /// <summary>
+    /// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
+    /// and configures authentication to support identity bearer tokens and cookies.
+    /// </summary>
+    /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+    /// <returns>The <see cref="IdentityBuilder"/>.</returns>
+    [RequiresUnreferencedCode("Authentication middleware does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")]
+    public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
+        where TUser : class, new()
+        => services.AddIdentityApiEndpoints<TUser>(_ => { });
+
+    /// <summary>
+    /// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
+    /// and configures authentication to support identity bearer tokens and cookies.
+    /// </summary>
+    /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+    /// <param name="configure">Configures the <see cref="IdentityOptions"/>.</param>
+    /// <returns>The <see cref="IdentityBuilder"/>.</returns>
+    [RequiresUnreferencedCode("Authentication middleware does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")]
+    public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
+        where TUser : class, new()
+    {
+        ArgumentNullException.ThrowIfNull(services);
+        ArgumentNullException.ThrowIfNull(configure);
+
+        services.AddAuthentication(IdentityConstants.BearerAndApplicationScheme)
+            .AddScheme<PolicySchemeOptions, CompositeIdentityHandler>(IdentityConstants.BearerAndApplicationScheme, null, o =>
+            {
+                o.ForwardDefault = IdentityConstants.BearerScheme;
+                o.ForwardAuthenticate = IdentityConstants.BearerAndApplicationScheme;
+            })
+            .AddBearerToken(IdentityConstants.BearerScheme)
+            .AddIdentityCookies();
+
+        return services.AddIdentityCore<TUser>(o =>
+            {
+                o.Stores.MaxLengthForKeys = 128;
+                configure(o);
+            })
+            .AddApiEndpoints();
+    }
+
+    private sealed class CompositeIdentityHandler(IOptionsMonitor<PolicySchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder)
+        : PolicySchemeHandler(options, logger, encoder)
+    {
+        protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
+        {
+            var bearerResult = await Context.AuthenticateAsync(IdentityConstants.BearerScheme);
+
+            // Only try to authenticate with the application cookie if there is no bearer token.
+            if (!bearerResult.None)
+            {
+                return bearerResult;
+            }
+
+            // Cookie auth will return AuthenticateResult.NoResult() like bearer auth just did if there is no cookie.
+            return await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme);
+        }
+    }
+}

+ 15 - 5
src/Identity/Core/src/IdentityConstants.cs

@@ -8,25 +8,35 @@ namespace Microsoft.AspNetCore.Identity;
 /// </summary>
 public class IdentityConstants
 {
-    private const string CookiePrefix = "Identity";
+    private const string IdentityPrefix = "Identity";
 
     /// <summary>
     /// The scheme used to identify application authentication cookies.
     /// </summary>
-    public static readonly string ApplicationScheme = CookiePrefix + ".Application";
+    public static readonly string ApplicationScheme = IdentityPrefix + ".Application";
+
+    /// <summary>
+    /// The scheme used to identify bearer authentication tokens.
+    /// </summary>
+    public static readonly string BearerScheme = IdentityPrefix + ".Bearer";
+
+    /// <summary>
+    /// The scheme used to identify combination of <see cref="BearerScheme"/> and <see cref="ApplicationScheme"/>.
+    /// </summary>
+    internal const string BearerAndApplicationScheme = IdentityPrefix + ".BearerAndApplication";
 
     /// <summary>
     /// The scheme used to identify external authentication cookies.
     /// </summary>
-    public static readonly string ExternalScheme = CookiePrefix + ".External";
+    public static readonly string ExternalScheme = IdentityPrefix + ".External";
 
     /// <summary>
     /// The scheme used to identify Two Factor authentication cookies for saving the Remember Me state.
     /// </summary>
-    public static readonly string TwoFactorRememberMeScheme = CookiePrefix + ".TwoFactorRememberMe";
+    public static readonly string TwoFactorRememberMeScheme = IdentityPrefix + ".TwoFactorRememberMe";
 
     /// <summary>
     /// The scheme used to identify Two Factor authentication cookies for round tripping user identities.
     /// </summary>
-    public static readonly string TwoFactorUserIdScheme = CookiePrefix + ".TwoFactorUserId";
+    public static readonly string TwoFactorUserIdScheme = IdentityPrefix + ".TwoFactorUserId";
 }

+ 17 - 0
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<JsonOptions>
+{
+    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);
+    }
+}

+ 13 - 1
src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj

@@ -7,11 +7,18 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <PackageTags>aspnetcore;identity;membership</PackageTags>
     <IsPackable>false</IsPackable>
-    <IsTrimmable>true</IsTrimmable>
+    <!-- TODO: Re-enable trimming once https://github.com/dotnet/aspnetcore/issues/47918 is fixed and RDG is enabled. -->
+    <!--<IsTrimmable>true</IsTrimmable>-->
   </PropertyGroup>
 
+  <ItemGroup>
+    <Compile Include="$(SharedSourceRoot)BearerToken\DTO\*.cs" LinkBase="DTO" />
+  </ItemGroup>
+
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
+    <Reference Include="Microsoft.AspNetCore.Authentication.BearerToken" />
+    <Reference Include="Microsoft.AspNetCore.Http.Results" />
     <Reference Include="Microsoft.Extensions.Identity.Core" />
   </ItemGroup>
 
@@ -22,4 +29,9 @@
     <InternalsVisibleTo Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore.InMemory.Test" />
     <InternalsVisibleTo Include="Microsoft.AspNetCore.Identity.InMemory.Test" />
   </ItemGroup>
+
+  <ItemGroup>
+    <!-- TODO: Re-enable RDG once https://github.com/dotnet/aspnetcore/issues/47918 is fixed. -->
+    <!--<ProjectReference Include="$(RepoRoot)/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />-->
+  </ItemGroup>
 </Project>

+ 8 - 0
src/Identity/Core/src/PublicAPI.Unshipped.txt

@@ -1,7 +1,15 @@
 #nullable enable
+Microsoft.AspNetCore.Identity.IdentityApiEndpointsIdentityBuilderExtensions
 Microsoft.AspNetCore.Identity.SecurityStampValidator<TUser>.SecurityStampValidator(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions!>! options, Microsoft.AspNetCore.Identity.SignInManager<TUser!>! signInManager, Microsoft.Extensions.Logging.ILoggerFactory! logger) -> void
 Microsoft.AspNetCore.Identity.SecurityStampValidator<TUser>.TimeProvider.get -> System.TimeProvider!
 Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions.TimeProvider.get -> System.TimeProvider?
 Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions.TimeProvider.set -> void
 Microsoft.AspNetCore.Identity.TwoFactorSecurityStampValidator<TUser>.TwoFactorSecurityStampValidator(Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.Identity.SecurityStampValidatorOptions!>! options, Microsoft.AspNetCore.Identity.SignInManager<TUser!>! 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.Routing.IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi<TUser>(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Builder.IEndpointConventionBuilder!
+static Microsoft.Extensions.DependencyInjection.IdentityApiEndpointsServiceCollectionExtensions.AddIdentityApiEndpoints<TUser>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.AspNetCore.Identity.IdentityBuilder!
+static Microsoft.Extensions.DependencyInjection.IdentityApiEndpointsServiceCollectionExtensions.AddIdentityApiEndpoints<TUser>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action<Microsoft.AspNetCore.Identity.IdentityOptions!>! configure) -> Microsoft.AspNetCore.Identity.IdentityBuilder!
+static readonly Microsoft.AspNetCore.Identity.IdentityConstants.BearerScheme -> string!
 virtual Microsoft.AspNetCore.Identity.SignInManager<TUser>.IsTwoFactorEnabledAsync(TUser! user) -> System.Threading.Tasks.Task<bool>!

+ 1 - 1
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";
     }

+ 6 - 1
src/Identity/Identity.slnf

@@ -21,8 +21,10 @@
       "src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
       "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
       "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
+      "src\\Http\\Http.Extensions\\gen\\Microsoft.AspNetCore.Http.RequestDelegateGenerator.csproj",
       "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
       "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
+      "src\\Http\\Http.Results\\src\\Microsoft.AspNetCore.Http.Results.csproj",
       "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
       "src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
       "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj",
@@ -39,6 +41,7 @@
       "src\\Identity\\Extensions.Stores\\src\\Microsoft.Extensions.Identity.Stores.csproj",
       "src\\Identity\\Specification.Tests\\src\\Microsoft.AspNetCore.Identity.Specification.Tests.csproj",
       "src\\Identity\\UI\\src\\Microsoft.AspNetCore.Identity.UI.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\\test\\Identity.FunctionalTests\\Microsoft.AspNetCore.Identity.FunctionalTests.csproj",
@@ -73,6 +76,7 @@
       "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj",
       "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj",
       "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj",
+      "src\\Security\\Authentication\\BearerToken\\src\\Microsoft.AspNetCore.Authentication.BearerToken.csproj",
       "src\\Security\\Authentication\\Cookies\\src\\Microsoft.AspNetCore.Authentication.Cookies.csproj",
       "src\\Security\\Authentication\\Core\\src\\Microsoft.AspNetCore.Authentication.csproj",
       "src\\Security\\Authentication\\Facebook\\src\\Microsoft.AspNetCore.Authentication.Facebook.csproj",
@@ -90,7 +94,8 @@
       "src\\Servers\\Kestrel\\Transport.NamedPipes\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj",
       "src\\Servers\\Kestrel\\Transport.Quic\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj",
       "src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
-      "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
+      "src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj",
+      "src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
     ]
   }
 }

+ 1 - 0
src/Identity/UI/src/Microsoft.AspNetCore.Identity.UI.csproj

@@ -21,6 +21,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <None Include="@(Content)" />
     <Content Remove="@(Content)" />
     <None Include="build\*" Pack="true" PackagePath="build\" />
     <None Include="buildMultiTargeting\*" Pack="true" PackagePath="buildMultiTargeting\" />

+ 15 - 0
src/Identity/samples/IdentitySample.ApiEndpoints/IdentitySample.ApiEndpoints.csproj

@@ -0,0 +1,15 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <Description>Identity sample application on ASP.NET Core using endpoint routing</Description>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore" />
+    <Reference Include="Microsoft.AspNetCore.Identity" />
+    <Reference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" />
+    <Reference Include="Microsoft.EntityFrameworkCore.Sqlite" />
+  </ItemGroup>
+
+</Project>

+ 38 - 0
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<ApplicationDbContext>(
+    options => options.UseSqlite(connection));
+builder.Services.AddIdentityApiEndpoints<IdentityUser>()
+    .AddEntityFrameworkStores<ApplicationDbContext>();
+
+var app = builder.Build();
+
+app.MapGet("/", () => "Hello, World!");
+app.MapGet("/requires-auth", (ClaimsPrincipal user) => $"Hello, {user.Identity?.Name}!").RequireAuthorization();
+
+app.MapGroup("/identity").MapIdentityApi<IdentityUser>();
+
+app.Run();
+connection.Close();
+
+public class ApplicationDbContext : IdentityDbContext<IdentityUser>
+{
+    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
+    {
+        Database.EnsureCreated();
+    }
+}

+ 12 - 0
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"
+    }
+  }
+}

+ 1 - 1
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"
       ]

+ 2 - 1
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<ApplicationUser>
 {
     public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
     {
+        Database.EnsureCreated();
     }
 }

+ 248 - 0
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<JsonElement>();
+        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<ApplicationUser>().AddApiEndpoints().AddEntityFrameworkStores<ApplicationDbContext>();
+            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<JsonElement>();
+        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<InvalidOperationException>(()
+            => client.PostAsJsonAsync("/identity/login?cookieMode=true", new { Username, Password }));
+    }
+
+    [Fact]
+    public async Task CanReadBearerTokenFromQueryString()
+    {
+        await using var app = await CreateAppAsync(services =>
+        {
+            services.AddIdentityCore<ApplicationUser>().AddApiEndpoints().AddEntityFrameworkStores<ApplicationDbContext>();
+            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<JsonElement>();
+        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<WebApplication> CreateAppAsync<TUser, TContext>(Action<IServiceCollection>? 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<TContext>(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<TUser>();
+
+        var authGroup = app.MapGroup("/auth").RequireAuthorization();
+        authGroup.MapGet("/hello",
+            (ClaimsPrincipal user) => $"Hello, {user.Identity?.Name}!");
+
+        await dbConnection.OpenAsync();
+        await app.Services.GetRequiredService<TContext>().Database.EnsureCreatedAsync();
+        await app.StartAsync();
+
+        return app;
+    }
+
+    private static void AddIdentityEndpoints(IServiceCollection services)
+        => services.AddIdentityApiEndpoints<ApplicationUser>().AddEntityFrameworkStores<ApplicationDbContext>();
+
+    private static void AddIdentityEndpointsBearerOnly(IServiceCollection services)
+    {
+        services.AddIdentityCore<ApplicationUser>().AddEntityFrameworkStores<ApplicationDbContext>();
+        services.AddAuthentication(IdentityConstants.BearerScheme).AddBearerToken(IdentityConstants.BearerScheme);
+    }
+
+    private Task<WebApplication> CreateAppAsync(Action<IServiceCollection>? configureServices = null)
+        => CreateAppAsync<ApplicationUser, ApplicationDbContext>(configureServices);
+
+    private static Dictionary<string, Action<IServiceCollection>> AddIdentityActions { get; } = new()
+    {
+        [nameof(AddIdentityEndpoints)] = AddIdentityEndpoints,
+        [nameof(AddIdentityEndpointsBearerOnly)] = AddIdentityEndpointsBearerOnly,
+    };
+
+    public static object[][] AddIdentityModes => AddIdentityActions.Keys.Select(key => new object[] { key }).ToArray();
+}

+ 1 - 0
src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj

@@ -10,6 +10,7 @@
     <Compile Include="..\..\UI\src\UIFramework.cs" Link="Infrastructure\UIFramework.cs" />
     <Compile Include="$(SharedSourceRoot)ThrowHelpers\ArgumentNullThrowHelper.cs" LinkBase="Shared" />
     <Compile Include="$(SharedSourceRoot)CallerArgument\CallerArgumentExpressionAttribute.cs" LinkBase="Shared" />
+    <Compile Include="$(IdentityTestSharedSourceRoot)\TestTimeProvider.cs" LinkBase="Shared" />
 
     <None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>

+ 15 - 0
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;
+
+/// <summary>
+/// Default values used by bearer token authentication.
+/// </summary>
+public static class BearerTokenDefaults
+{
+    /// <summary>
+    /// Default value for AuthenticationScheme property in the <see cref="BearerTokenOptions"/>.
+    /// </summary>
+    public const string AuthenticationScheme = "BearerToken";
+}

+ 20 - 0
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;
+
+/// <summary>
+/// Specifies events which the bearer token handler invokes to enable developer control over the authentication process.
+/// </summary>
+public class BearerTokenEvents
+{
+    /// <summary>
+    /// Invoked when a protocol message is first received.
+    /// </summary>
+    public Func<MessageReceivedContext, Task> OnMessageReceived { get; set; } = context => Task.CompletedTask;
+
+    /// <summary>
+    /// Invoked when a protocol message is first received.
+    /// </summary>
+    public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context);
+}

+ 67 - 0
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;
+
+/// <summary>
+/// Extension methods to configure the bearer token authentication.
+/// </summary>
+public static class BearerTokenExtensions
+{
+    /// <summary>
+    /// Adds bearer token authentication. The default scheme is specified by <see cref="BearerTokenDefaults.AuthenticationScheme"/>.
+    /// <para>
+    /// Bearer tokens can be obtained by calling <see cref="AuthenticationHttpContextExtensions.SignInAsync(AspNetCore.Http.HttpContext, string?, System.Security.Claims.ClaimsPrincipal)" />.
+    /// </para>
+    /// </summary>
+    /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
+    /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns>
+    public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder)
+        => builder.AddBearerToken(BearerTokenDefaults.AuthenticationScheme);
+
+    /// <summary>
+    /// Adds bearer token authentication.
+    /// <para>
+    /// Bearer tokens can be obtained by calling <see cref="AuthenticationHttpContextExtensions.SignInAsync(AspNetCore.Http.HttpContext, string?, System.Security.Claims.ClaimsPrincipal)" />.
+    /// </para>
+    /// </summary>
+    /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
+    /// <param name="authenticationScheme">The authentication scheme.</param>
+    /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns>
+    public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, string authenticationScheme)
+        => builder.AddBearerToken(authenticationScheme, _ => { });
+
+    /// <summary>
+    /// Adds bearer token authentication. The default scheme is specified by <see cref="BearerTokenDefaults.AuthenticationScheme"/>.
+    /// <para>
+    /// Bearer tokens can be obtained by calling <see cref="AuthenticationHttpContextExtensions.SignInAsync(AspNetCore.Http.HttpContext, string?, System.Security.Claims.ClaimsPrincipal)" />.
+    /// </para>
+    /// </summary>
+    /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
+    /// <param name="configure">Action used to configure the bearer token authentication options.</param>
+    /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns>
+    public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, Action<BearerTokenOptions> configure)
+        => builder.AddBearerToken(BearerTokenDefaults.AuthenticationScheme, configure);
+
+    /// <summary>
+    /// Adds bearer token authentication.
+    /// <para>
+    /// Bearer tokens can be obtained by calling <see cref="AuthenticationHttpContextExtensions.SignInAsync(AspNetCore.Http.HttpContext, string?, System.Security.Claims.ClaimsPrincipal)" />.
+    /// </para>
+    /// </summary>
+    /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param>
+    /// <param name="authenticationScheme">The authentication scheme.</param>
+    /// <param name="configure">Action used to configure the bearer token authentication options.</param>
+    /// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns>
+    public static AuthenticationBuilder AddBearerToken(this AuthenticationBuilder builder, string authenticationScheme, Action<BearerTokenOptions> configure)
+    {
+        ArgumentNullException.ThrowIfNull(builder);
+        ArgumentNullException.ThrowIfNull(authenticationScheme);
+        ArgumentNullException.ThrowIfNull(configure);
+
+        return builder.AddScheme<BearerTokenOptions, BearerTokenHandler>(authenticationScheme, configure);
+    }
+}

+ 112 - 0
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<BearerTokenOptions> 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<BearerTokenOptions>(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<AuthenticationTicket> BearerTokenProtector
+        => Options.BearerTokenProtector ?? new TicketDataFormat(dataProtectionProvider.CreateProtector(BearerTokenPurpose));
+
+    private new BearerTokenEvents Events => (BearerTokenEvents)base.Events!;
+
+    protected override async Task<AuthenticateResult> 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;
+    }
+}

+ 11 - 0
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
+{
+}

+ 46 - 0
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;
+
+/// <summary>
+/// Contains the options used to authenticate using opaque bearer tokens.
+/// </summary>
+public sealed class BearerTokenOptions : AuthenticationSchemeOptions
+{
+    /// <summary>
+    /// Constructs the options used to authenticate using opaque bearer tokens.
+    /// </summary>
+    public BearerTokenOptions()
+    {
+        Events = new();
+    }
+
+    /// <summary>
+    /// 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.
+    /// </summary>
+    public TimeSpan BearerTokenExpiration { get; set; } = TimeSpan.FromHours(1);
+
+    /// <summary>
+    /// If set, the <see cref="BearerTokenProtector"/> 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 <see cref="TicketDataFormat"/> and the <see cref="IDataProtectionProvider"/>
+    /// from the application <see cref="IServiceProvider"/>.
+    /// </summary>
+    public ISecureDataFormat<AuthenticationTicket>? BearerTokenProtector { get; set; }
+
+    /// <summary>
+    /// 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 <see cref="BearerTokenEvents"/>
+    /// and assign delegates only to the events it wants to process.
+    /// </summary>
+    public new BearerTokenEvents Events
+    {
+        get { return (BearerTokenEvents)base.Events!; }
+        set { base.Events = value; }
+    }
+}
+

+ 27 - 0
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;
+
+/// <summary>
+/// A context for <see cref="BearerTokenEvents.OnMessageReceived"/>.
+/// </summary>
+public class MessageReceivedContext : ResultContext<BearerTokenOptions>
+{
+    /// <summary>
+    /// Initializes a new instance of <see cref="MessageReceivedContext"/>.
+    /// </summary>
+    /// <inheritdoc />
+    public MessageReceivedContext(
+        HttpContext context,
+        AuthenticationScheme scheme,
+        BearerTokenOptions options)
+        : base(context, scheme, options) { }
+
+    /// <summary>
+    /// Bearer Token. This will give the application an opportunity to retrieve a token from an alternative location.
+    /// </summary>
+    public string? Token { get; set; }
+}

+ 20 - 0
src/Security/Authentication/BearerToken/src/Microsoft.AspNetCore.Authentication.BearerToken.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <Description>ASP.NET Core authentication handler that enables an application to receive an opaque bearer token.</Description>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <IsAspNetCoreApp>true</IsAspNetCoreApp>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <PackageTags>aspnetcore;authentication;security</PackageTags>
+    <IsPackable>false</IsPackable>
+    <IsTrimmable>true</IsTrimmable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Compile Include="$(SharedSourceRoot)BearerToken\**\*.cs" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Authentication" />
+  </ItemGroup>
+</Project>

+ 1 - 0
src/Security/Authentication/BearerToken/src/PublicAPI.Shipped.txt

@@ -0,0 +1 @@
+#nullable enable

+ 25 - 0
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.MessageReceivedContext!, System.Threading.Tasks.Task!>!
+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.AuthenticationTicket!>?
+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<Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions!>! configure) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder!
+static Microsoft.Extensions.DependencyInjection.BearerTokenExtensions.AddBearerToken(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, System.Action<Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenOptions!>! configure) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder!
+virtual Microsoft.AspNetCore.Authentication.BearerToken.BearerTokenEvents.MessageReceived(Microsoft.AspNetCore.Authentication.BearerToken.MessageReceivedContext! context) -> System.Threading.Tasks.Task!

+ 2 - 2
src/Security/Authentication/JwtBearer/src/JwtBearerDefaults.cs

@@ -4,12 +4,12 @@
 namespace Microsoft.AspNetCore.Authentication.JwtBearer;
 
 /// <summary>
-/// Default values used by bearer authentication.
+/// Default values used by <see cref="JwtBearerHandler"/> for JWT bearer authentication.
 /// </summary>
 public static class JwtBearerDefaults
 {
     /// <summary>
-    /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions
+    /// Default value for AuthenticationScheme property in the <see cref="JwtBearerOptions"/>.
     /// </summary>
     public const string AuthenticationScheme = "Bearer";
 }

+ 26 - 0
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<BearerTokenOptions>
+{
+    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<BearerTokenOptions>).IsAssignableFrom);
+        }
+    }
+
+    protected override void RegisterAuth(AuthenticationBuilder services, Action<BearerTokenOptions> configure)
+    {
+        services.AddBearerToken(configure);
+    }
+}

+ 1 - 0
src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj

@@ -37,6 +37,7 @@
 
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore" />
+    <Reference Include="Microsoft.AspNetCore.Authentication.BearerToken" />
     <Reference Include="Microsoft.AspNetCore.Authentication.Certificate" />
     <Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
     <Reference Include="Microsoft.AspNetCore.Authentication.Facebook" />

+ 1 - 0
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",

+ 18 - 0
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; }
+}

+ 1 - 0
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",