Skip to content

Commit 8bbab02

Browse files
committed
Add MapIdentityApi<TUser>()
1 parent e821f20 commit 8bbab02

40 files changed

+1062
-12
lines changed

AspNetCore.sln

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1762,7 +1762,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
17621762
EndProject
17631763
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}"
17641764
EndProject
1765-
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}"
1765+
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}"
17661766
EndProject
17671767
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "QuickGrid", "QuickGrid", "{C406D9E0-1585-43F9-AA8F-D468AF84A996}"
17681768
EndProject
@@ -1780,6 +1780,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon
17801780
EndProject
17811781
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorUnitedApp", "src\Components\Samples\BlazorUnitedApp\BlazorUnitedApp.csproj", "{F5AE525F-F435-40F9-A567-4D5EC3B50D6E}"
17821782
EndProject
1783+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BearerToken", "BearerToken", "{56291265-B7BF-4756-92AB-FC30F09381D1}"
1784+
EndProject
1785+
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}"
1786+
EndProject
1787+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IdentitySample.ApiEndpoints", "src\Identity\samples\IdentitySample.ApiEndpoints\IdentitySample.ApiEndpoints.csproj", "{37FC77EA-AC44-4D08-B002-8EFF415C424A}"
1788+
EndProject
17831789
Global
17841790
GlobalSection(SolutionConfigurationPlatforms) = preSolution
17851791
Debug|Any CPU = Debug|Any CPU
@@ -10701,6 +10707,38 @@ Global
1070110707
{F5AE525F-F435-40F9-A567-4D5EC3B50D6E}.Release|x64.Build.0 = Release|Any CPU
1070210708
{F5AE525F-F435-40F9-A567-4D5EC3B50D6E}.Release|x86.ActiveCfg = Release|Any CPU
1070310709
{F5AE525F-F435-40F9-A567-4D5EC3B50D6E}.Release|x86.Build.0 = Release|Any CPU
10710+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10711+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
10712+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|arm64.ActiveCfg = Debug|Any CPU
10713+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|arm64.Build.0 = Debug|Any CPU
10714+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|x64.ActiveCfg = Debug|Any CPU
10715+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|x64.Build.0 = Debug|Any CPU
10716+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|x86.ActiveCfg = Debug|Any CPU
10717+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Debug|x86.Build.0 = Debug|Any CPU
10718+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
10719+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|Any CPU.Build.0 = Release|Any CPU
10720+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|arm64.ActiveCfg = Release|Any CPU
10721+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|arm64.Build.0 = Release|Any CPU
10722+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|x64.ActiveCfg = Release|Any CPU
10723+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|x64.Build.0 = Release|Any CPU
10724+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|x86.ActiveCfg = Release|Any CPU
10725+
{66FA1041-5556-43A0-9CA3-F9937F085F6E}.Release|x86.Build.0 = Release|Any CPU
10726+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10727+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|Any CPU.Build.0 = Debug|Any CPU
10728+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|arm64.ActiveCfg = Debug|Any CPU
10729+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|arm64.Build.0 = Debug|Any CPU
10730+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|x64.ActiveCfg = Debug|Any CPU
10731+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|x64.Build.0 = Debug|Any CPU
10732+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|x86.ActiveCfg = Debug|Any CPU
10733+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Debug|x86.Build.0 = Debug|Any CPU
10734+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|Any CPU.ActiveCfg = Release|Any CPU
10735+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|Any CPU.Build.0 = Release|Any CPU
10736+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|arm64.ActiveCfg = Release|Any CPU
10737+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|arm64.Build.0 = Release|Any CPU
10738+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x64.ActiveCfg = Release|Any CPU
10739+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x64.Build.0 = Release|Any CPU
10740+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x86.ActiveCfg = Release|Any CPU
10741+
{37FC77EA-AC44-4D08-B002-8EFF415C424A}.Release|x86.Build.0 = Release|Any CPU
1070410742
EndGlobalSection
1070510743
GlobalSection(SolutionProperties) = preSolution
1070610744
HideSolutionNode = FALSE
@@ -11580,6 +11618,9 @@ Global
1158011618
{AE4D272D-6F13-42C8-9404-C149188AFA33} = {7BAEB9BF-28F4-4DFD-9A04-E5193683C261}
1158111619
{5D438258-CB19-4282-814F-974ABBC71411} = {7BAEB9BF-28F4-4DFD-9A04-E5193683C261}
1158211620
{F5AE525F-F435-40F9-A567-4D5EC3B50D6E} = {5FE1FBC1-8CE3-4355-9866-44FE1307C5F1}
11621+
{56291265-B7BF-4756-92AB-FC30F09381D1} = {822D1519-77F0-484A-B9AB-F694C2CC25F1}
11622+
{66FA1041-5556-43A0-9CA3-F9937F085F6E} = {56291265-B7BF-4756-92AB-FC30F09381D1}
11623+
{37FC77EA-AC44-4D08-B002-8EFF415C424A} = {64B2A28F-6D82-4F2B-B0BB-88DE5216DD2C}
1158311624
EndGlobalSection
1158411625
GlobalSection(ExtensibilityGlobals) = postSolution
1158511626
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

eng/ProjectReferences.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.NamedPipes\src\Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes.csproj" />
5454
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Quic\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Quic.csproj" />
5555
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Sockets\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" />
56+
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.BearerToken" ProjectPath="$(RepoRoot)src\Security\Authentication\BearerToken\src\Microsoft.AspNetCore.Authentication.BearerToken.csproj" />
5657
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Certificate" ProjectPath="$(RepoRoot)src\Security\Authentication\Certificate\src\Microsoft.AspNetCore.Authentication.Certificate.csproj" />
5758
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Cookies" ProjectPath="$(RepoRoot)src\Security\Authentication\Cookies\src\Microsoft.AspNetCore.Authentication.Cookies.csproj" />
5859
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication" ProjectPath="$(RepoRoot)src\Security\Authentication\Core\src\Microsoft.AspNetCore.Authentication.csproj" />

eng/SharedFramework.Local.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<AspNetCoreAppReferenceAndPackage Include="Microsoft.Extensions.Identity.Core" />
1818
<AspNetCoreAppReferenceAndPackage Include="Microsoft.Extensions.Identity.Stores" />
1919
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Connections.Abstractions" />
20+
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Authentication.BearerToken" />
2021
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Authorization" />
2122
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.Http.Connections.Common" />
2223
<AspNetCoreAppReferenceAndPackage Include="Microsoft.AspNetCore.SignalR.Protocols.Json" />

eng/TrimmableProjects.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<TrimmableProject Include="Microsoft.AspNetCore.Server.Kestrel.Transport.NamedPipes" />
4545
<TrimmableProject Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Quic" />
4646
<TrimmableProject Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" />
47+
<TrimmableProject Include="Microsoft.AspNetCore.Authentication.BearerToken" />
4748
<TrimmableProject Include="Microsoft.AspNetCore.Authentication.Certificate" />
4849
<TrimmableProject Include="Microsoft.AspNetCore.Authentication.Cookies" />
4950
<TrimmableProject Include="Microsoft.AspNetCore.Authentication" />
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json.Serialization;
5+
6+
namespace Microsoft.AspNetCore.Identity.DTO;
7+
8+
[JsonSerializable(typeof(RegisterRequest))]
9+
[JsonSerializable(typeof(LoginRequest))]
10+
internal sealed partial class IdentityEndpointsJsonSerializerContext : JsonSerializerContext
11+
{
12+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Identity.DTO;
5+
6+
internal sealed class LoginRequest
7+
{
8+
public required string Username { get; init; }
9+
public required string Password { get; init; }
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Identity.DTO;
5+
6+
internal sealed class RegisterRequest
7+
{
8+
public required string Username { get; init; }
9+
public required string Password { get; init; }
10+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using Microsoft.AspNetCore.Authentication.BearerToken.DTO;
6+
using Microsoft.AspNetCore.Builder;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Http.HttpResults;
9+
using Microsoft.AspNetCore.Http.Metadata;
10+
using Microsoft.AspNetCore.Identity;
11+
using Microsoft.AspNetCore.Identity.DTO;
12+
using Microsoft.Extensions.DependencyInjection;
13+
14+
namespace Microsoft.AspNetCore.Routing;
15+
16+
/// <summary>
17+
/// Provides extension methods for <see cref="IEndpointRouteBuilder"/> to add identity endpoints.
18+
/// </summary>
19+
public static class IdentityApiEndpointRouteBuilderExtensions
20+
{
21+
/// <summary>
22+
/// Add endpoints for registering, logging in, and logging out using ASP.NET Core Identity.
23+
/// </summary>
24+
/// <typeparam name="TUser">The type describing the user. This should match the generic parameter in <see cref="UserManager{TUser}"/>.</typeparam>
25+
/// <param name="endpoints">
26+
/// The <see cref="IEndpointRouteBuilder"/> to add the identity endpoints to.
27+
/// Call <see cref="EndpointRouteBuilderExtensions.MapGroup(IEndpointRouteBuilder, string)"/> to add a prefix to all the endpoints.
28+
/// </param>
29+
/// <returns>An <see cref="IEndpointConventionBuilder"/> to further customize the added endpoints.</returns>
30+
public static IEndpointConventionBuilder MapIdentityApi<TUser>(this IEndpointRouteBuilder endpoints) where TUser : class, new()
31+
{
32+
ArgumentNullException.ThrowIfNull(endpoints);
33+
34+
var routeGroup = endpoints.MapGroup("");
35+
36+
// NOTE: We cannot inject UserManager<TUser> directly because the TUser generic parameter is currently unsupported by RDG.
37+
// https://github.com/dotnet/aspnetcore/issues/47338
38+
routeGroup.MapPost("/register", async Task<Results<Ok, ValidationProblem>>
39+
([FromBody] RegisterRequest registration, [FromServices] IServiceProvider services) =>
40+
{
41+
var userManager = services.GetRequiredService<UserManager<TUser>>();
42+
43+
var user = new TUser();
44+
await userManager.SetUserNameAsync(user, registration.Username);
45+
var result = await userManager.CreateAsync(user, registration.Password);
46+
47+
if (result.Succeeded)
48+
{
49+
return TypedResults.Ok();
50+
}
51+
52+
return TypedResults.ValidationProblem(result.Errors.ToDictionary(e => e.Code, e => new[] { e.Description }));
53+
});
54+
55+
routeGroup.MapPost("/login", async Task<Results<UnauthorizedHttpResult, Ok<AccessTokenResponse>, SignInHttpResult>>
56+
([FromBody] LoginRequest login, [FromQuery] bool? cookieMode, [FromServices] IServiceProvider services) =>
57+
{
58+
var userManager = services.GetRequiredService<UserManager<TUser>>();
59+
var user = await userManager.FindByNameAsync(login.Username);
60+
61+
if (user is null || !await userManager.CheckPasswordAsync(user, login.Password))
62+
{
63+
return TypedResults.Unauthorized();
64+
}
65+
66+
var claimsFactory = services.GetRequiredService<IUserClaimsPrincipalFactory<TUser>>();
67+
var claimsPrincipal = await claimsFactory.CreateAsync(user);
68+
69+
var useCookies = cookieMode ?? false;
70+
var scheme = useCookies ? IdentityConstants.ApplicationScheme : IdentityConstants.BearerScheme;
71+
72+
return TypedResults.SignIn(claimsPrincipal, authenticationScheme: scheme);
73+
});
74+
75+
return new IdentityEndpointsConventionBuilder(routeGroup);
76+
}
77+
78+
// Wrap RouteGroupBuilder with a non-public type to avoid a potential future behavioral breaking change.
79+
private sealed class IdentityEndpointsConventionBuilder(RouteGroupBuilder inner) : IEndpointConventionBuilder
80+
{
81+
#pragma warning disable CA1822 // Mark members as static False positive reported by https://github.com/dotnet/roslyn-analyzers/issues/6573
82+
private IEndpointConventionBuilder InnerAsConventionBuilder => inner;
83+
#pragma warning restore CA1822 // Mark members as static
84+
85+
public void Add(Action<EndpointBuilder> convention) => InnerAsConventionBuilder.Add(convention);
86+
public void Finally(Action<EndpointBuilder> finallyConvention) => InnerAsConventionBuilder.Finally(finallyConvention);
87+
}
88+
89+
[AttributeUsage(AttributeTargets.Parameter)]
90+
private sealed class FromBodyAttribute : Attribute, IFromBodyMetadata
91+
{
92+
}
93+
94+
[AttributeUsage(AttributeTargets.Parameter)]
95+
private sealed class FromServicesAttribute : Attribute, IFromServiceMetadata
96+
{
97+
}
98+
99+
[AttributeUsage(AttributeTargets.Parameter)]
100+
private sealed class FromQueryAttribute : Attribute, IFromQueryMetadata
101+
{
102+
public string? Name => null;
103+
}
104+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Authentication;
5+
using Microsoft.AspNetCore.Authentication.BearerToken;
6+
using Microsoft.AspNetCore.Http.Json;
7+
using Microsoft.AspNetCore.Routing;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Extensions.DependencyInjection.Extensions;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Microsoft.AspNetCore.Identity;
13+
14+
/// <summary>
15+
/// <see cref="IdentityBuilder"/> extension methods to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>.
16+
/// </summary>
17+
public static class IdentityApiEndpointsIdentityBuilderExtensions
18+
{
19+
/// <summary>
20+
/// Adds configuration ans services needed to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
21+
/// but does not configure authentication. Call <see cref="BearerTokenExtensions.AddBearerToken(AuthenticationBuilder, Action{BearerTokenOptions}?)"/> and/or
22+
/// <see cref="IdentityCookieAuthenticationBuilderExtensions.AddIdentityCookies(AuthenticationBuilder)"/> to configure authentication separately.
23+
/// </summary>
24+
/// <param name="builder">The <see cref="IdentityBuilder"/>.</param>
25+
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
26+
public static IdentityBuilder AddApiEndpoints(this IdentityBuilder builder)
27+
{
28+
ArgumentNullException.ThrowIfNull(builder);
29+
30+
builder.AddSignInManager();
31+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JsonOptions>, IdentityEndpointsJsonOptionsSetup>());
32+
return builder;
33+
}
34+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Diagnostics.CodeAnalysis;
5+
using System.Text.Encodings.Web;
6+
using Microsoft.AspNetCore.Authentication;
7+
using Microsoft.AspNetCore.Identity;
8+
using Microsoft.AspNetCore.Routing;
9+
using Microsoft.Extensions.Logging;
10+
using Microsoft.Extensions.Options;
11+
12+
namespace Microsoft.Extensions.DependencyInjection;
13+
14+
/// <summary>
15+
/// Default extensions to <see cref="IServiceCollection"/> for <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>.
16+
/// </summary>
17+
public static class IdentityApiEndpointsServiceCollectionExtensions
18+
{
19+
/// <summary>
20+
/// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
21+
/// and configures authentication to support identity bearer tokens and cookies.
22+
/// </summary>
23+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
24+
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
25+
[RequiresUnreferencedCode("Authentication middleware does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")]
26+
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services)
27+
where TUser : class, new()
28+
=> services.AddIdentityApiEndpoints<TUser>(_ => { });
29+
30+
/// <summary>
31+
/// Adds a set of common identity services to the application to support <see cref="IdentityApiEndpointRouteBuilderExtensions.MapIdentityApi{TUser}(IEndpointRouteBuilder)"/>
32+
/// and configures authentication to support identity bearer tokens and cookies.
33+
/// </summary>
34+
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
35+
/// <param name="configure">Configures the <see cref="IdentityOptions"/>.</param>
36+
/// <returns>The <see cref="IdentityBuilder"/>.</returns>
37+
[RequiresUnreferencedCode("Authentication middleware does not currently support native AOT.", Url = "https://aka.ms/aspnet/nativeaot")]
38+
public static IdentityBuilder AddIdentityApiEndpoints<TUser>(this IServiceCollection services, Action<IdentityOptions> configure)
39+
where TUser : class, new()
40+
{
41+
ArgumentNullException.ThrowIfNull(services);
42+
ArgumentNullException.ThrowIfNull(configure);
43+
44+
services.AddAuthentication(IdentityConstants.BearerAndApplicationScheme)
45+
.AddScheme<PolicySchemeOptions, CompositeIdentityHandler>(IdentityConstants.BearerAndApplicationScheme, null, o =>
46+
{
47+
o.ForwardDefault = IdentityConstants.BearerScheme;
48+
o.ForwardAuthenticate = IdentityConstants.BearerAndApplicationScheme;
49+
})
50+
.AddBearerToken(IdentityConstants.BearerScheme)
51+
.AddIdentityCookies();
52+
53+
return services.AddIdentityCore<TUser>(o =>
54+
{
55+
o.Stores.MaxLengthForKeys = 128;
56+
configure(o);
57+
})
58+
.AddApiEndpoints();
59+
}
60+
61+
private sealed class CompositeIdentityHandler(IOptionsMonitor<PolicySchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder)
62+
: PolicySchemeHandler(options, logger, encoder)
63+
{
64+
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
65+
{
66+
var bearerResult = await Context.AuthenticateAsync(IdentityConstants.BearerScheme);
67+
68+
// Only try to authenticate with the application cookie if there is no bearer token.
69+
if (!bearerResult.None)
70+
{
71+
return bearerResult;
72+
}
73+
74+
// Cookie auth will return AuthenticateResult.NoResult() like bearer auth just did if there is no cookie.
75+
return await Context.AuthenticateAsync(IdentityConstants.ApplicationScheme);
76+
}
77+
}
78+
}

0 commit comments

Comments
 (0)