diff --git a/AspNetCore.sln b/AspNetCore.sln
index 88ed078aec9e..02b6e7c51c77 100644
--- a/AspNetCore.sln
+++ b/AspNetCore.sln
@@ -1710,6 +1710,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Html.A
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RateLimiting", "RateLimiting", "{1D865E78-7A66-4CA9-92EE-2B350E45281F}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts", "src\Tools\dotnet-user-jwts\src\dotnet-user-jwts.csproj", "{B34CB502-0286-4939-B25F-45998528A802}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet-user-jwts", "dotnet-user-jwts", "{AB4B9E75-719C-4589-B852-20FBFD727730}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MinimalJwtBearerSample", "src\Security\Authentication\JwtBearer\samples\MinimalJwtBearerSample\MinimalJwtBearerSample.csproj", "{7F079E92-32D5-4257-B95B-CFFB0D49C160}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-user-jwts.Tests", "src\Tools\dotnet-user-jwts\test\dotnet-user-jwts.Tests.csproj", "{89896261-C5DD-4901-BCA7-7A5F718BC008}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -10247,6 +10255,54 @@ Global
{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x64.Build.0 = Release|Any CPU
{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.ActiveCfg = Release|Any CPU
{487EF7BE-5009-4C70-B79E-45519BDD9603}.Release|x86.Build.0 = Release|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Debug|arm64.ActiveCfg = Debug|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Debug|arm64.Build.0 = Debug|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Debug|x64.Build.0 = Debug|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Debug|x86.Build.0 = Debug|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Release|arm64.ActiveCfg = Release|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Release|arm64.Build.0 = Release|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Release|x64.ActiveCfg = Release|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Release|x64.Build.0 = Release|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Release|x86.ActiveCfg = Release|Any CPU
+ {B34CB502-0286-4939-B25F-45998528A802}.Release|x86.Build.0 = Release|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|arm64.ActiveCfg = Debug|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|arm64.Build.0 = Debug|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|x64.Build.0 = Debug|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Debug|x86.Build.0 = Debug|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|arm64.ActiveCfg = Release|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|arm64.Build.0 = Release|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x64.ActiveCfg = Release|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x64.Build.0 = Release|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x86.ActiveCfg = Release|Any CPU
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160}.Release|x86.Build.0 = Release|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|arm64.ActiveCfg = Debug|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|arm64.Build.0 = Debug|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|x64.Build.0 = Debug|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Debug|x86.Build.0 = Debug|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|Any CPU.Build.0 = Release|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|arm64.ActiveCfg = Release|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|arm64.Build.0 = Release|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x64.ActiveCfg = Release|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x64.Build.0 = Release|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x86.ActiveCfg = Release|Any CPU
+ {89896261-C5DD-4901-BCA7-7A5F718BC008}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -11094,6 +11150,10 @@ Global
{51D07AA9-6297-4F66-A7BD-71CE7E3F4A3F} = {0F84F170-57D0-496B-8E2C-7984178EF69F}
{487EF7BE-5009-4C70-B79E-45519BDD9603} = {412D4C15-F48F-4DB1-940A-131D1AA87088}
{1D865E78-7A66-4CA9-92EE-2B350E45281F} = {E5963C9F-20A6-4385-B364-814D2581FADF}
+ {B34CB502-0286-4939-B25F-45998528A802} = {AB4B9E75-719C-4589-B852-20FBFD727730}
+ {AB4B9E75-719C-4589-B852-20FBFD727730} = {0B200A66-B809-4ED3-A790-CB1C2E80975E}
+ {7F079E92-32D5-4257-B95B-CFFB0D49C160} = {7FD32066-C831-4E29-978C-9A2215E85C67}
+ {89896261-C5DD-4901-BCA7-7A5F718BC008} = {AB4B9E75-719C-4589-B852-20FBFD727730}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
diff --git a/eng/Signing.props b/eng/Signing.props
index 66f8dc2ce805..db20e2e9a45f 100644
--- a/eng/Signing.props
+++ b/eng/Signing.props
@@ -79,6 +79,7 @@
+
diff --git a/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs b/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs
index 8070f11abb66..616e24982958 100644
--- a/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs
+++ b/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs
@@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -21,6 +22,7 @@ public void AddAzureAD_AddsAllAuthenticationHandlers()
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -288,6 +290,7 @@ public void AddAzureADBearer_AddsAllAuthenticationHandlers()
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -305,6 +308,7 @@ public void AddAzureADBearer_ConfiguresAllOptions()
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -340,6 +344,7 @@ public void AddAzureADBearer_CanOverrideJwtBearerOptionsConfiguration()
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -373,6 +378,7 @@ public void AddAzureADBearer_RegisteringJwtBearerHasNoImpactOnAzureAAExtensions(
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -473,6 +479,7 @@ public void AddAzureADBearer_SkipsOptionsValidationForNonAzureCookies()
{
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
services.AddAuthentication()
.AddAzureADBearer(o => { })
diff --git a/src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs b/src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs
index 8c61fdb79844..36abc8d6d58d 100644
--- a/src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs
+++ b/src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs
@@ -6,6 +6,7 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -262,6 +263,7 @@ public void AddAzureADB2CBearer_AddsAllAuthenticationHandlers()
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -279,6 +281,7 @@ public void AddAzureADB2CBearer_ConfiguresAllOptions()
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -315,6 +318,7 @@ public void AddAzureADB2CBearer_CanOverrideJwtBearerOptionsConfiguration()
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -348,6 +352,7 @@ public void AddAzureADB2CBearer_RegisteringJwtBearerHasNoImpactOnAzureAAExtensio
// Arrange
var services = new ServiceCollection();
services.AddSingleton(new NullLoggerFactory());
+ services.AddSingleton(new ConfigurationManager());
// Act
services.AddAuthentication()
diff --git a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
index 2c66a8407db3..669ecb8037bb 100644
--- a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
+++ b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
@@ -11,6 +11,8 @@
+
+
@@ -32,4 +34,8 @@
+
+
+
+
diff --git a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt
index 88ff0b5ecc80..6eba7bee0ff8 100644
--- a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt
+++ b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt
@@ -1,3 +1,4 @@
#nullable enable
Microsoft.AspNetCore.Builder.WebApplication.Use(System.Func! middleware) -> Microsoft.AspNetCore.Builder.IApplicationBuilder!
+Microsoft.AspNetCore.Builder.WebApplicationBuilder.Authentication.get -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder!
static Microsoft.Extensions.Hosting.GenericHostBuilderExtensions.ConfigureWebHostDefaults(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action! configure, System.Action! configureOptions) -> Microsoft.Extensions.Hosting.IHostBuilder!
diff --git a/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs b/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs
new file mode 100644
index 000000000000..f95ca8a05896
--- /dev/null
+++ b/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs
@@ -0,0 +1,48 @@
+// 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 Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.AspNetCore.Authentication;
+
+internal class WebApplicationAuthenticationBuilder : AuthenticationBuilder
+{
+ public bool IsAuthenticationConfigured { get; private set; }
+
+ public WebApplicationAuthenticationBuilder(IServiceCollection services) : base(services) { }
+
+ public override AuthenticationBuilder AddPolicyScheme(string authenticationScheme, string? displayName, Action configureOptions)
+ {
+ RegisterServices(authenticationScheme);
+ return base.AddPolicyScheme(authenticationScheme, displayName, configureOptions);
+ }
+
+ public override AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string? displayName, Action? configureOptions)
+ {
+ RegisterServices(authenticationScheme);
+ return base.AddRemoteScheme(authenticationScheme, displayName, configureOptions);
+ }
+
+ public override AuthenticationBuilder AddScheme(string authenticationScheme, string? displayName, Action? configureOptions)
+ {
+ RegisterServices(authenticationScheme);
+ return base.AddScheme(authenticationScheme, displayName, configureOptions);
+ }
+
+ public override AuthenticationBuilder AddScheme(string authenticationScheme, Action? configureOptions)
+ {
+ RegisterServices(authenticationScheme);
+ return base.AddScheme(authenticationScheme, configureOptions);
+ }
+
+ private void RegisterServices(string authenticationScheme)
+ {
+ if (!IsAuthenticationConfigured)
+ {
+ IsAuthenticationConfigured = true;
+ Services.AddAuthentication(authenticationScheme);
+ Services.AddAuthorization();
+ }
+ }
+}
diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs
index c5eabba6816d..b96b15117432 100644
--- a/src/DefaultBuilder/src/WebApplicationBuilder.cs
+++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
+using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@@ -16,9 +17,11 @@ namespace Microsoft.AspNetCore.Builder;
public sealed class WebApplicationBuilder
{
private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
+ private const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet";
private readonly HostApplicationBuilder _hostApplicationBuilder;
private readonly ServiceDescriptor _genericWebHostServiceDescriptor;
+ private readonly WebApplicationAuthenticationBuilder _webAuthBuilder;
private WebApplication? _builtApplication;
@@ -79,6 +82,7 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action
@@ -113,6 +117,11 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action
public ConfigureHostBuilder Host { get; }
+ ///
+ /// An for configuration authentication-related properties.
+ ///
+ public AuthenticationBuilder Authentication => _webAuthBuilder;
+
///
/// Builds the .
///
@@ -166,6 +175,16 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
}
}
+ if (_webAuthBuilder.IsAuthenticationConfigured)
+ {
+ // Don't add more than one instance of the middleware
+ if (!_builtApplication.Properties.ContainsKey(AuthenticationMiddlewareSetKey))
+ {
+ _builtApplication.UseAuthentication();
+ _builtApplication.UseAuthorization();
+ }
+ }
+
// Wire the source pipeline to run in the destination pipeline
app.Use(next =>
{
diff --git a/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs b/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs
new file mode 100644
index 000000000000..0665de7acb0b
--- /dev/null
+++ b/src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Configuration;
+
+namespace Microsoft.AspNetCore.Authentication;
+
+///
+/// Provides an interface for implmenting a construct that provides
+/// access to specific configuration sections.
+///
+public interface IAuthenticationConfigurationProvider
+{
+ ///
+ /// Returns the specified object.
+ ///
+ /// The path to the section to be returned.
+ /// The specified object, or null if the requested section does not exist.
+ IConfiguration GetAuthenticationSchemeConfiguration(string authenticationScheme);
+}
diff --git a/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt b/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt
index 7dc5c58110bf..efe32d90a931 100644
--- a/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt
+++ b/src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt
@@ -1 +1,3 @@
#nullable enable
+Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider
+Microsoft.AspNetCore.Authentication.IAuthenticationConfigurationProvider.GetAuthenticationSchemeConfiguration(string! authenticationScheme) -> Microsoft.Extensions.Configuration.IConfiguration!
diff --git a/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs b/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs
index a729ef21e0e5..332fe321aaf2 100644
--- a/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs
+++ b/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Security.Cryptography.X509Certificates;
+using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
diff --git a/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs b/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs
index 8efc58c59305..5c210ef7e907 100644
--- a/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs
+++ b/src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs
@@ -10,6 +10,8 @@ namespace Microsoft.AspNetCore.Builder;
///
public static class AuthAppBuilderExtensions
{
+ internal const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet";
+
///
/// Adds the to the specified , which enables authentication capabilities.
///
@@ -22,6 +24,7 @@ public static IApplicationBuilder UseAuthentication(this IApplicationBuilder app
throw new ArgumentNullException(nameof(app));
}
+ app.Properties[AuthenticationMiddlewareSetKey] = true;
return app.UseMiddleware();
}
}
diff --git a/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs b/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs
index b29001aa37a7..2e34894d99b2 100644
--- a/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs
+++ b/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs
@@ -27,6 +27,7 @@ public static AuthenticationBuilder AddAuthentication(this IServiceCollection se
services.AddDataProtection();
services.AddWebEncoders();
services.TryAddSingleton();
+ services.TryAddSingleton();
return new AuthenticationBuilder(services);
}
diff --git a/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs b/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs
new file mode 100644
index 000000000000..057e1a5ad1d4
--- /dev/null
+++ b/src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs
@@ -0,0 +1,21 @@
+// 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.Configuration;
+
+namespace Microsoft.AspNetCore.Authentication;
+
+internal sealed class DefaultAuthenticationConfigurationProvider : IAuthenticationConfigurationProvider
+{
+ private readonly IConfiguration _configuration;
+
+ public DefaultAuthenticationConfigurationProvider(IConfiguration configuration)
+ {
+ _configuration = configuration;
+ }
+
+ public IConfiguration GetAuthenticationSchemeConfiguration(string authenticationScheme)
+ {
+ return _configuration.GetSection($"Authentication:Schemes:{authenticationScheme}");
+ }
+}
diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj
new file mode 100644
index 000000000000..e0ab758b6a83
--- /dev/null
+++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj
@@ -0,0 +1,17 @@
+
+
+
+ $(DefaultNetCoreTargetFramework)
+ MinimalJwtBearerSample-20151210102827
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Program.cs b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Program.cs
new file mode 100644
index 000000000000..ff0c3ecd22cb
--- /dev/null
+++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Program.cs
@@ -0,0 +1,32 @@
+// 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.Authentication.JwtBearer;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.AspNetCore.Builder;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Authentication.AddJwtBearer();
+builder.Authentication.AddJwtBearer("ClaimedDetails");
+
+builder.Services.AddAuthorization(options =>
+ options.AddPolicy("is_admin", policy =>
+ {
+ policy.RequireAuthenticatedUser();
+ policy.RequireClaim("is_admin", "true");
+ }));
+
+var app = builder.Build();
+
+app.MapGet("/protected", (ClaimsPrincipal user) => $"Hello {user.Identity?.Name}!")
+ .RequireAuthorization();
+
+app.MapGet("/protected-with-claims", (ClaimsPrincipal user) =>
+{
+ return $"Glory be to the admin {user.Identity?.Name}!";
+})
+.RequireAuthorization("is_admin");
+
+app.Run();
diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Properties/launchSettings.json b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Properties/launchSettings.json
new file mode 100644
index 000000000000..ea8f54aeb1a4
--- /dev/null
+++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:56852",
+ "sslPort": 44385
+ }
+ },
+ "profiles": {
+ "MinimalJwtBearerSample": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "protected",
+ "applicationUrl": "https://localhost:7259;http://localhost:5259",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "protected",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json
new file mode 100644
index 000000000000..9fe2b7a74f48
--- /dev/null
+++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json
@@ -0,0 +1,26 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Authentication": {
+ "Schemes": {
+ "Bearer": {
+ "Audiences": [
+ "https://localhost:7259",
+ "http://localhost:5259"
+ ],
+ "ClaimsIssuer": "dotnet-user-jwts"
+ },
+ "ClaimedDetails": {
+ "Audiences": [
+ "https://localhost:7259",
+ "http://localhost:5259"
+ ],
+ "ClaimsIssuer": "dotnet-user-jwts"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.json b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.json
new file mode 100644
index 000000000000..10f68b8c8b4f
--- /dev/null
+++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs
new file mode 100644
index 000000000000..7004f06a40be
--- /dev/null
+++ b/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs
@@ -0,0 +1,72 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Linq;
+using System.Security.Cryptography;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication;
+
+internal sealed class JwtBearerConfigureOptions : IConfigureNamedOptions
+{
+ private readonly IAuthenticationConfigurationProvider _authenticationConfigurationProvider;
+ private readonly IConfiguration _configuration;
+
+ ///
+ /// Initializes a new given the configuration
+ /// provided by the .
+ ///
+ /// An instance.
+ /// An instance for accessing configuration elements not in the schema.
+ public JwtBearerConfigureOptions(IAuthenticationConfigurationProvider configurationProvider, IConfiguration configuration)
+ {
+ _authenticationConfigurationProvider = configurationProvider;
+ _configuration = configuration;
+ }
+
+ ///
+ public void Configure(string? name, JwtBearerOptions options)
+ {
+ if (string.IsNullOrEmpty(name))
+ {
+ return;
+ }
+
+ var configSection = _authenticationConfigurationProvider.GetAuthenticationSchemeConfiguration(name);
+
+ if (configSection is null || !configSection.GetChildren().Any())
+ {
+ return;
+ }
+
+ var issuer = configSection["ClaimsIssuer"];
+ var audiences = configSection.GetSection("Audiences").GetChildren().Select(aud => aud.Value).ToArray();
+ options.TokenValidationParameters = new()
+ {
+ ValidateIssuer = issuer is not null,
+ ValidIssuers = new[] { issuer },
+ ValidateAudience = audiences.Length > 0,
+ ValidAudiences = audiences,
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = GetIssuerSigningKey(_configuration, issuer),
+ };
+ }
+
+ private static SecurityKey GetIssuerSigningKey(IConfiguration configuration, string? issuer)
+ {
+ var jwtKeyMaterialSecret = configuration[$"{issuer}:KeyMaterial"];
+ var jwtKeyMaterial = !string.IsNullOrEmpty(jwtKeyMaterialSecret)
+ ? Convert.FromBase64String(jwtKeyMaterialSecret)
+ : RandomNumberGenerator.GetBytes(32);
+ return new SymmetricSecurityKey(jwtKeyMaterial);
+ }
+
+ ///
+ public void Configure(JwtBearerOptions options)
+ {
+ Configure(Options.DefaultName, options);
+ }
+}
diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs
index 12022ed0780d..f4a7ec2e9ae3 100644
--- a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs
+++ b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs
@@ -24,6 +24,18 @@ public static class JwtBearerExtensions
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder)
=> builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { });
+ ///
+ /// Enables JWT-bearer authentication using a pre-defined scheme.
+ ///
+ /// JWT bearer authentication performs authentication by extracting and validating a JWT token from the Authorization request header.
+ ///
+ ///
+ /// The .
+ /// The authentication scheme.
+ /// A reference to after the operation has completed.
+ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme)
+ => builder.AddJwtBearer(authenticationScheme, _ => { });
+
///
/// Enables JWT-bearer authentication using the default scheme .
///
@@ -62,6 +74,7 @@ public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder buil
/// A reference to after the operation has completed.
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action configureOptions)
{
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerConfigureOptions>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, JwtBearerPostConfigureOptions>());
return builder.AddScheme(authenticationScheme, displayName, configureOptions);
}
diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt
index 2c729aeee8d2..e66f11d82ec1 100644
--- a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt
+++ b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt
@@ -1,3 +1,4 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.PostConfigure(string! name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerPostConfigureOptions.PostConfigure(string? name, Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerOptions! options) -> void
+static Microsoft.Extensions.DependencyInjection.JwtBearerExtensions.AddJwtBearer(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder! builder, string! authenticationScheme) -> Microsoft.AspNetCore.Authentication.AuthenticationBuilder!
diff --git a/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs b/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
index f6ac9acce57b..eb883f111d7a 100644
--- a/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
+++ b/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
@@ -151,6 +151,25 @@ public async Task IAuthenticateResultFeature_SettingResultSetsUser()
Assert.Same(context.User, newTicket.Principal);
}
+ [Fact]
+ public async Task WebApplicationBuilder_RegistersAuthenticationMiddlewares()
+ {
+ var builder = WebApplication.CreateBuilder();
+ builder.Authentication.AddJwtBearer();
+ await using var app = builder.Build();
+
+ var webAppAuthBuilder = Assert.IsType(builder.Authentication);
+ Assert.True(webAppAuthBuilder.IsAuthenticationConfigured);
+
+ // Authentication middleware isn't registered until application
+ // is built on startup
+ Assert.False(app.Properties.ContainsKey("__AuthenticationMiddlewareSet"));
+
+ await app.StartAsync();
+
+ Assert.True(app.Properties.ContainsKey("__AuthenticationMiddlewareSet"));
+ }
+
private HttpContext GetHttpContext(
Action registerServices = null,
IAuthenticationService authenticationService = null)
diff --git a/src/Security/Authentication/test/CertificateTests.cs b/src/Security/Authentication/test/CertificateTests.cs
index 03fd439a3660..018ccb139171 100644
--- a/src/Security/Authentication/test/CertificateTests.cs
+++ b/src/Security/Authentication/test/CertificateTests.cs
@@ -6,6 +6,7 @@
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.Xml.Linq;
+using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
diff --git a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj
index a65c8ad9271f..72dad5f713a3 100644
--- a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj
+++ b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj
@@ -36,6 +36,7 @@
+
diff --git a/src/Security/Authentication/test/SharedAuthenticationTests.cs b/src/Security/Authentication/test/SharedAuthenticationTests.cs
index 4ee984335d59..f1085b604169 100644
--- a/src/Security/Authentication/test/SharedAuthenticationTests.cs
+++ b/src/Security/Authentication/test/SharedAuthenticationTests.cs
@@ -4,6 +4,7 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication.Tests;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Authentication;
@@ -25,6 +26,7 @@ public abstract class SharedAuthenticationTests where TOptions : Authe
public async Task CanForwardDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
@@ -165,6 +167,7 @@ public async Task ForwardSignOutWinsOverDefault()
public async Task ForwardForbidWinsOverDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -214,6 +217,7 @@ public Task TransformAsync(ClaimsPrincipal principal)
public async Task ForwardAuthenticateOnlyRunsTransformOnceByDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton(new ConfigurationManager());
var transform = new RunOnce();
var builder = services.AddSingleton(transform).AddAuthentication(o =>
{
@@ -244,6 +248,7 @@ public async Task ForwardAuthenticateOnlyRunsTransformOnceByDefault()
public async Task ForwardAuthenticateWinsOverDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -283,6 +288,7 @@ public async Task ForwardAuthenticateWinsOverDefault()
public async Task ForwardChallengeWinsOverDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -322,6 +328,7 @@ public async Task ForwardChallengeWinsOverDefault()
public async Task ForwardSelectorWinsOverDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -391,6 +398,7 @@ public async Task ForwardSelectorWinsOverDefault()
public async Task NullForwardSelectorUsesDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -460,6 +468,7 @@ public async Task NullForwardSelectorUsesDefault()
public async Task SpecificForwardWinsOverSelectorAndDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
diff --git a/src/Security/Security.slnf b/src/Security/Security.slnf
index 0dd86cd84fd2..e8b8685e04b7 100644
--- a/src/Security/Security.slnf
+++ b/src/Security/Security.slnf
@@ -6,6 +6,7 @@
"src\\DataProtection\\Cryptography.Internal\\src\\Microsoft.AspNetCore.Cryptography.Internal.csproj",
"src\\DataProtection\\DataProtection\\src\\Microsoft.AspNetCore.DataProtection.csproj",
"src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj",
+ "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
"src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj",
"src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj",
"src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
@@ -15,7 +16,6 @@
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
- "src\\Extensions\\Features\\src\\Microsoft.Extensions.Features.csproj",
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
"src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
@@ -38,6 +38,7 @@
"src\\Security\\Authentication\\Facebook\\src\\Microsoft.AspNetCore.Authentication.Facebook.csproj",
"src\\Security\\Authentication\\Google\\src\\Microsoft.AspNetCore.Authentication.Google.csproj",
"src\\Security\\Authentication\\JwtBearer\\samples\\JwtBearerSample\\JwtBearerSample.csproj",
+ "src\\Security\\Authentication\\JwtBearer\\samples\\MinimalJwtBearerSample\\MinimalJwtBearerSample.csproj",
"src\\Security\\Authentication\\JwtBearer\\src\\Microsoft.AspNetCore.Authentication.JwtBearer.csproj",
"src\\Security\\Authentication\\MicrosoftAccount\\src\\Microsoft.AspNetCore.Authentication.MicrosoftAccount.csproj",
"src\\Security\\Authentication\\Negotiate\\samples\\NegotiateAuthSample\\NegotiateAuthSample.csproj",
@@ -69,4 +70,4 @@
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
]
}
-}
+}
\ No newline at end of file
diff --git a/src/Shared/test/Certificates/Certificates.cs b/src/Shared/test/Certificates/Certificates.cs
index 8124e9cdf0f1..d0c2a0c043b6 100644
--- a/src/Shared/test/Certificates/Certificates.cs
+++ b/src/Shared/test/Certificates/Certificates.cs
@@ -4,6 +4,8 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
+namespace Microsoft.AspNetCore.Authentication.Certificate;
+
public static class Certificates
{
private static string ServerEku = "1.3.6.1.5.5.7.3.1";
diff --git a/src/Tools/Tools.slnf b/src/Tools/Tools.slnf
index 8527dde99dcc..e277c5a31543 100644
--- a/src/Tools/Tools.slnf
+++ b/src/Tools/Tools.slnf
@@ -99,6 +99,8 @@
"src\\Tools\\dotnet-dev-certs\\src\\dotnet-dev-certs.csproj",
"src\\Tools\\dotnet-getdocument\\src\\dotnet-getdocument.csproj",
"src\\Tools\\dotnet-sql-cache\\src\\dotnet-sql-cache.csproj",
+ "src\\Tools\\dotnet-user-jwts\\src\\dotnet-user-jwts.csproj",
+ "src\\Tools\\dotnet-user-jwts\\test\\dotnet-user-jwts.Tests.csproj",
"src\\Tools\\dotnet-user-secrets\\src\\dotnet-user-secrets.csproj",
"src\\Tools\\dotnet-user-secrets\\test\\dotnet-user-secrets.Tests.csproj",
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs
new file mode 100644
index 000000000000..e0880812fc8e
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs
@@ -0,0 +1,69 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed class ClearCommand
+{
+ public static void Register(ProjectCommandLineApplication app)
+ {
+ app.Command("clear", cmd =>
+ {
+ cmd.Description = "Delete all issued JWTs for a project";
+
+ var forceOption = cmd.Option(
+ "--force",
+ "Don't prompt for confirmation before deleting JWTs",
+ CommandOptionType.NoValue);
+
+ cmd.HelpOption("-h|--help");
+
+ cmd.OnExecute(() =>
+ {
+ return Execute(cmd.Reporter, cmd.ProjectOption.Value(), forceOption.HasValue());
+ });
+ });
+ }
+
+ private static int Execute(IReporter reporter, string projectPath, bool force)
+ {
+ if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId))
+ {
+ return 1;
+ }
+ var jwtStore = new JwtStore(userSecretsId);
+ var count = jwtStore.Jwts.Count;
+
+ if (count == 0)
+ {
+ reporter.Output($"There are no JWTs to delete from {project}.");
+ return 0;
+ }
+
+ if (!force)
+ {
+ reporter.Output($"Are you sure you want to delete {count} JWT(s) for {project}?{Environment.NewLine} [Y]es / [N]o");
+ if (Console.ReadLine().Trim().ToUpperInvariant() != "Y")
+ {
+ reporter.Output("Canceled, no JWTs were deleted.");
+ return 0;
+ }
+ }
+
+ var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
+ foreach (var jwt in jwtStore.Jwts)
+ {
+ JwtAuthenticationSchemeSettings.RemoveScheme(appsettingsFilePath, jwt.Value.Scheme);
+ }
+
+ jwtStore.Jwts.Clear();
+ jwtStore.Save();
+
+ reporter.Output($"Deleted {count} token(s) from {project} successfully.");
+
+ return 0;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs
new file mode 100644
index 000000000000..17ac345c5945
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs
@@ -0,0 +1,204 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Globalization;
+using System.Linq;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed class CreateCommand
+{
+ private static readonly string[] _dateTimeFormats = new[] {
+ "yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy/MM/dd", "yyyy/MM/dd HH:mm" };
+ private static readonly string[] _timeSpanFormats = new[] {
+ @"d\dh\hm\ms\s", @"d\dh\hm\m", @"d\dh\h", @"d\d",
+ @"h\hm\ms\s", @"h\hm\m", @"h\h",
+ @"m\ms\s", @"m\m",
+ @"s\s"
+ };
+
+ public static void Register(ProjectCommandLineApplication app)
+ {
+ app.Command("create", cmd =>
+ {
+ cmd.Description = "Issue a new JSON Web Token";
+
+ var schemeNameOption = cmd.Option(
+ "--scheme",
+ "The scheme name to use for the generated token. Defaults to 'Bearer'",
+ CommandOptionType.SingleValue
+ );
+
+ var nameOption = cmd.Option(
+ "--name",
+ "The name of the user to create the JWT for. Defaults to the current environment user.",
+ CommandOptionType.SingleValue);
+
+ var audienceOption = cmd.Option(
+ "--audience",
+ "The audiences to create the JWT for. Defaults to the URLs configured in the project's launchSettings.json",
+ CommandOptionType.MultipleValue);
+
+ var issuerOption = cmd.Option(
+ "--issuer",
+ "The issuer of the JWT. Defaults to the dotnet-user-jwts",
+ CommandOptionType.SingleValue);
+
+ var scopesOption = cmd.Option(
+ "--scope",
+ "A scope claim to add to the JWT. Specify once for each scope.",
+ CommandOptionType.MultipleValue);
+
+ var rolesOption = cmd.Option(
+ "--role",
+ "A role claim to add to the JWT. Specify once for each role",
+ CommandOptionType.MultipleValue);
+
+ var claimsOption = cmd.Option(
+ "--claim",
+ "Claims to add to the JWT. Specify once for each claim in the format \"name=value\"",
+ CommandOptionType.MultipleValue);
+
+ var notBeforeOption = cmd.Option(
+ "--not-before",
+ @"The UTC date & time the JWT should not be valid before in the format 'yyyy-MM-dd [[HH:mm[[:ss]]]]'. Defaults to the date & time the JWT is created",
+ CommandOptionType.SingleValue);
+
+ var expiresOnOption = cmd.Option(
+ "--expires-on",
+ @"The UTC date & time the JWT should expire in the format 'yyyy-MM-dd [[[[HH:mm]]:ss]]'. Defaults to 6 months after the --not-before date. " +
+ "Do not use this option in conjunction with the --valid-for option.",
+ CommandOptionType.SingleValue);
+
+ var validForOption = cmd.Option(
+ "--valid-for",
+ "The period the JWT should expire after. Specify using a number followed by a period type like 'd' for days, 'h' for hours, " +
+ "'m' for minutes, and 's' for seconds, e.g. '365d'. Do not use this option in conjunction with the --expires-on option.",
+ CommandOptionType.SingleValue);
+
+ cmd.HelpOption("-h|--help");
+
+ cmd.OnExecute(() =>
+ {
+ var (options, isValid) = ValidateArguments(
+ cmd.Reporter, cmd.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption);
+
+ if (!isValid)
+ {
+ return 1;
+ }
+
+ return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options);
+ });
+ });
+ }
+
+ private static (JwtCreatorOptions, bool) ValidateArguments(
+ IReporter reporter,
+ CommandOption projectOption,
+ CommandOption schemeNameOption,
+ CommandOption nameOption,
+ CommandOption audienceOption,
+ CommandOption issuerOption,
+ CommandOption notBeforeOption,
+ CommandOption expiresOnOption,
+ CommandOption validForOption,
+ CommandOption rolesOption,
+ CommandOption scopesOption,
+ CommandOption claimsOption)
+ {
+ var isValid = true;
+ var project = DevJwtCliHelpers.GetProject(projectOption.Value());
+ var scheme = schemeNameOption.HasValue() ? schemeNameOption.Value() : "Bearer";
+ var name = nameOption.HasValue() ? nameOption.Value() : Environment.UserName;
+
+ var audience = audienceOption.HasValue() ? audienceOption.Values : DevJwtCliHelpers.GetAudienceCandidatesFromLaunchSettings(project).ToList();
+ if (audience is null)
+ {
+ reporter.Error("Could not determine the project's HTTPS URL. Please specify an audience for the JWT using the --audience option.");
+ isValid = false;
+ }
+ var issuer = issuerOption.HasValue() ? issuerOption.Value() : DevJwtsDefaults.Issuer;
+
+ var notBefore = DateTime.UtcNow;
+ if (notBeforeOption.HasValue())
+ {
+ if (!ParseDate(notBeforeOption.Value(), out notBefore))
+ {
+ reporter.Error(@"The date provided for --not-before could not be parsed. Dates must consist of a date and can include an optional timestamp.");
+ isValid = false;
+ }
+ }
+
+ var expiresOn = notBefore.AddMonths(3);
+ if (expiresOnOption.HasValue())
+ {
+ if (!ParseDate(expiresOnOption.Value(), out expiresOn))
+ {
+ reporter.Error(@"The date provided for --expires-on could not be parsed. Dates must consist of a date and can include an optional timestamp.");
+ isValid = false;
+ }
+ }
+
+ if (validForOption.HasValue())
+ {
+ if (!TimeSpan.TryParseExact(validForOption.Value(), _timeSpanFormats, CultureInfo.InvariantCulture, out var validForValue))
+ {
+ reporter.Error("The period provided for --valid-for could not be parsed. Ensure you use a format like '10d', '22h', '45s' etc.");
+ }
+ expiresOn = notBefore.Add(validForValue);
+ }
+
+ var roles = rolesOption.HasValue() ? rolesOption.Values : new List();
+ var scopes = scopesOption.HasValue() ? scopesOption.Values : new List();
+
+ var claims = new Dictionary();
+ if (claimsOption.HasValue())
+ {
+ if (!DevJwtCliHelpers.TryParseClaims(claimsOption.Values, out claims))
+ {
+ reporter.Error("Malformed claims supplied. Ensure each claim is in the format \"name=value\".");
+ isValid = false;
+ }
+ }
+
+ return (new JwtCreatorOptions(scheme, name, audience, issuer, notBefore, expiresOn, roles, scopes, claims), isValid);
+
+ static bool ParseDate(string datetime, out DateTime parsedDateTime) =>
+ DateTime.TryParseExact(datetime, _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsedDateTime);
+ }
+
+ private static int Execute(
+ IReporter reporter,
+ string projectPath,
+ JwtCreatorOptions options)
+ {
+ if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId))
+ {
+ return 1;
+ }
+ var keyMaterial = DevJwtCliHelpers.GetOrCreateSigningKeyMaterial(userSecretsId);
+
+ var jwtIssuer = new JwtIssuer(options.Issuer, keyMaterial);
+ var jwtToken = jwtIssuer.Create(options);
+
+ var jwtStore = new JwtStore(userSecretsId);
+ var jwt = Jwt.Create(options.Scheme, jwtToken, JwtIssuer.WriteToken(jwtToken), options.Scopes, options.Roles, options.Claims);
+ if (options.Claims is { } customClaims)
+ {
+ jwt.CustomClaims = customClaims;
+ }
+ jwtStore.Jwts.Add(jwtToken.Id, jwt);
+ jwtStore.Save();
+
+ var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
+ var settingsToWrite = new JwtAuthenticationSchemeSettings(options.Scheme, options.Audiences, options.Issuer);
+ settingsToWrite.Save(appsettingsFilePath);
+
+ reporter.Output($"New JWT saved with ID '{jwtToken.Id}'.");
+
+ return 0;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs
new file mode 100644
index 000000000000..83b287b81a2f
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs
@@ -0,0 +1,56 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed class DeleteCommand
+{
+ public static void Register(ProjectCommandLineApplication app)
+ {
+ app.Command("delete", cmd =>
+ {
+ cmd.Description = "Delete a given JWT";
+
+ var idArgument = cmd.Argument("[id]", "The ID of the JWT to delete");
+ cmd.HelpOption("-h|--help");
+
+ cmd.OnExecute(() =>
+ {
+ if (idArgument.Value is null)
+ {
+ cmd.ShowHelp();
+ return 0;
+ }
+ return Execute(cmd.Reporter, cmd.ProjectOption.Value(), idArgument.Value);
+ });
+ });
+ }
+
+ private static int Execute(IReporter reporter, string projectPath, string id)
+ {
+ if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId))
+ {
+ return 1;
+ }
+ var jwtStore = new JwtStore(userSecretsId);
+
+ if (!jwtStore.Jwts.ContainsKey(id))
+ {
+ reporter.Error($"[ERROR] No JWT with ID '{id}' found");
+ return 1;
+ }
+
+ var jwt = jwtStore.Jwts[id];
+ var appsettingsFilePath = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
+ JwtAuthenticationSchemeSettings.RemoveScheme(appsettingsFilePath, jwt.Scheme);
+ jwtStore.Jwts.Remove(id);
+ jwtStore.Save();
+
+ reporter.Output($"Deleted JWT with ID '{id}'");
+
+ return 0;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs
new file mode 100644
index 000000000000..1637d7d7f6ed
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs
@@ -0,0 +1,75 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed class KeyCommand
+{
+ public static void Register(ProjectCommandLineApplication app)
+ {
+ app.Command("key", cmd =>
+ {
+ cmd.Description = "Display or reset the signing key used to issue JWTs";
+
+ var resetOption = cmd.Option(
+ "--reset",
+ "Reset the signing key. This will invalidate all previously issued JWTs for this project.",
+ CommandOptionType.NoValue);
+
+ var forceOption = cmd.Option(
+ "--force",
+ "Don't prompt for confirmation before resetting the signing key.",
+ CommandOptionType.NoValue);
+
+ cmd.HelpOption("-h|--help");
+
+ cmd.OnExecute(() =>
+ {
+ return Execute(cmd.Reporter, cmd.ProjectOption.Value(), resetOption.HasValue(), forceOption.HasValue());
+ });
+ });
+ }
+
+ private static int Execute(IReporter reporter, string projectPath, bool reset, bool force)
+ {
+ if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var _, out var userSecretsId))
+ {
+ return 1;
+ }
+
+ if (reset == true)
+ {
+ if (!force)
+ {
+ reporter.Output("Are you sure you want to reset the JWT signing key? This will invalidate all JWTs previously issued for this project.\n [Y]es / [N]o");
+ if (Console.ReadLine().Trim().ToUpperInvariant() != "Y")
+ {
+ reporter.Output("Key reset canceled.");
+ return 0;
+ }
+ }
+
+ var key = DevJwtCliHelpers.CreateSigningKeyMaterial(userSecretsId, reset: true);
+ reporter.Output($"New signing key created: {Convert.ToBase64String(key)}");
+ return 0;
+ }
+
+ var projectConfiguration = new ConfigurationBuilder()
+ .AddUserSecrets(userSecretsId)
+ .Build();
+ var signingKeyMaterial = projectConfiguration[DevJwtsDefaults.SigningKeyConfigurationKey];
+
+ if (signingKeyMaterial is null)
+ {
+ reporter.Output("Signing key for JWTs was not found. One will be created automatically when the first JWT is created, or you can force creation of a key with the --reset option.");
+ return 0;
+ }
+
+ reporter.Output($"Signing Key: {signingKeyMaterial}");
+ return 0;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs
new file mode 100644
index 000000000000..8013e298997f
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs
@@ -0,0 +1,74 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed class ListCommand
+{
+ public static void Register(ProjectCommandLineApplication app)
+ {
+ app.Command("list", cmd =>
+ {
+ cmd.Description = "Lists the JWTs issued for the project";
+
+ var showTokensOption = cmd.Option(
+ "--show-tokens",
+ "Indicates whether JWT base64 strings should be shown",
+ CommandOptionType.NoValue);
+
+ cmd.HelpOption("-h|--help");
+
+ cmd.OnExecute(() =>
+ {
+ return Execute(cmd.Reporter, cmd.ProjectOption.Value(), showTokensOption.HasValue());
+ });
+ });
+ }
+
+ private static int Execute(IReporter reporter, string projectPath, bool showTokens)
+ {
+ if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId))
+ {
+ return 1;
+ }
+ var jwtStore = new JwtStore(userSecretsId);
+
+ reporter.Output($"Project: {project}");
+ reporter.Output($"User Secrets ID: {userSecretsId}");
+
+ if (jwtStore.Jwts is { Count: > 0 } jwts)
+ {
+ var table = new ConsoleTable(reporter);
+ table.AddColumns("Id", "Scheme Name", "Audience", "Issued", "Expires");
+
+ if (showTokens)
+ {
+ table.AddColumns("Encoded Token");
+ }
+
+ foreach (var jwtRow in jwts)
+ {
+ var jwt = jwtRow.Value;
+ if (showTokens)
+ {
+ table.AddRow(jwt.Id, jwt.Scheme, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O"), jwt.Token);
+ }
+ else
+ {
+ table.AddRow(jwt.Id, jwt.Scheme, jwt.Audience, jwt.Issued.ToString("O"), jwt.Expires.ToString("O"));
+ }
+ }
+
+ table.Write();
+ }
+ else
+ {
+ reporter.Output("No JWTs created yet!");
+ }
+
+ return 0;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs
new file mode 100644
index 000000000000..3d144b9c5001
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs
@@ -0,0 +1,64 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IdentityModel.Tokens.Jwt;
+using Microsoft.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+internal sealed class PrintCommand
+{
+ public static void Register(ProjectCommandLineApplication app)
+ {
+ app.Command("print", cmd =>
+ {
+ cmd.Description = "Print the details of a given JWT";
+
+ var idArgument = cmd.Argument("[id]", "The ID of the JWT to print");
+
+ var showFullOption = cmd.Option(
+ "--show-full",
+ "Whether to show the full JWT contents in addition to the compact serialized format",
+ CommandOptionType.NoValue);
+
+ cmd.HelpOption("-h|--help");
+
+ cmd.OnExecute(() =>
+ {
+ if (idArgument.Value is null)
+ {
+ cmd.ShowHelp();
+ return 0;
+ }
+ return Execute(cmd.Reporter, cmd.ProjectOption.Value(), idArgument.Value, showFullOption.HasValue());
+ });
+ });
+ }
+
+ private static int Execute(IReporter reporter, string projectPath, string id, bool showFull)
+ {
+ if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var _, out var userSecretsId))
+ {
+ return 1;
+ }
+ var jwtStore = new JwtStore(userSecretsId);
+
+ if (!jwtStore.Jwts.ContainsKey(id))
+ {
+ reporter.Output($"No token with ID '{id}' found");
+ return 1;
+ }
+
+ reporter.Output($"Found JWT with ID '{id}'");
+ var jwt = jwtStore.Jwts[id];
+ JwtSecurityToken fullToken;
+
+ if (showFull)
+ {
+ fullToken = JwtIssuer.Extract(jwt.Token);
+ DevJwtCliHelpers.PrintJwt(reporter, jwt, fullToken);
+ }
+
+ return 0;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs b/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs
new file mode 100644
index 000000000000..a15391cc99b6
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs
@@ -0,0 +1,32 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed class ProjectCommandLineApplication : CommandLineApplication
+{
+ public CommandOption ProjectOption { get; private set; }
+
+ public IReporter Reporter { get; private set; }
+
+ public ProjectCommandLineApplication(IReporter reporter, bool throwOnUnexpectedArg = true, bool continueAfterUnexpectedArg = false, bool treatUnmatchedOptionsAsArguments = false)
+ : base(throwOnUnexpectedArg, continueAfterUnexpectedArg, treatUnmatchedOptionsAsArguments)
+ {
+ ProjectOption = Option(
+ "-p|--project",
+ "The path of the project to operate on. Defaults to the project in the current directory",
+ CommandOptionType.SingleValue);
+ Reporter = reporter;
+ }
+
+ public ProjectCommandLineApplication Command(string name, Action configuration)
+ {
+ var command = new ProjectCommandLineApplication(Reporter) { Name = name, Parent = this };
+ Commands.Add(command);
+ configuration(command);
+ return command;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/ConsoleTable.cs b/src/Tools/dotnet-user-jwts/src/Helpers/ConsoleTable.cs
new file mode 100644
index 000000000000..b7773b719df4
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Helpers/ConsoleTable.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.Extensions.CommandLineUtils;
+
+internal sealed class ConsoleTable
+{
+ private readonly List _columns = new();
+ private readonly List