зеркало из https://github.com/dotnet/aspnetcore.git
Add MapIdentityApi<TUser>() (#47414)
This commit is contained in:
Родитель
0db29cc500
Коммит
d4430f0d7d
|
@ -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}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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" },
|
||||
|
|
|
@ -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
|
||||
{
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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; }
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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>!
|
||||
|
|
|
@ -81,7 +81,7 @@ public class SignInResult
|
|||
public override string ToString()
|
||||
{
|
||||
return IsLockedOut ? "Lockedout" :
|
||||
IsNotAllowed ? "NotAllowed" :
|
||||
IsNotAllowed ? "NotAllowed" :
|
||||
RequiresTwoFactor ? "RequiresTwoFactor" :
|
||||
Succeeded ? "Succeeded" : "Failed";
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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\" />
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"profiles": {
|
||||
"IdentitySample.ApiEndpoints": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": false,
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"applicationUrl": "https://localhost:62313;http://localhost:62314"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "3.0.0-preview3.19153.1",
|
||||
"version": "7.0.4",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
#nullable enable
|
|
@ -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!
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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" />
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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; }
|
||||
}
|
|
@ -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",
|
||||
|
|
Загрузка…
Ссылка в новой задаче