This commit is contained in:
Stephen Halter 2023-04-27 12:06:51 -07:00 коммит произвёл GitHub
Родитель 0db29cc500
Коммит d4430f0d7d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
41 изменённых файлов: 1069 добавлений и 14 удалений

Просмотреть файл

@ -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",