Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/aspnetcore.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSafia Abdalla <safia@microsoft.com>2022-06-02 01:07:40 +0300
committerGitHub <noreply@github.com>2022-06-02 01:07:40 +0300
commitd7d7deb232b9fb7ba05d9d08d024c0b7aee57982 (patch)
tree1ef14b66ca5657ca74a9beeb6ee003eed1d65096
parent2d2295183b26b98c386d31c29403328e9c1f53ef (diff)
Add dotnet user-jwts tool and runtime support (#41520) (#41956)
* Add dotnet dev-jwts tool * Add dotnet dev-jwts tool * Address feedback from review * Rename project file * Write auth config to app settings * Address more feedback * :seal: * Apply suggestions from code review Co-authored-by: Brennan <brecon@microsoft.com> * Address more feedback * Add framework support for authentication changes * Add tests for user-jwts CLI and react to feedback * Move ConsoleTable implementation to avoid conflicts in ProjectTemplates * Update existing auth tests and fix middleware registration * Update AzureAdB2C tests and auth app builder * Fix build and move registration check * Fix up resolution for Certificate test sources * Fix write stream configuration for writing key material * Fix handling missing config section when processing options Co-authored-by: Brennan <brecon@microsoft.com> Co-authored-by: Brennan <brecon@microsoft.com>
-rw-r--r--AspNetCore.sln60
-rw-r--r--eng/Signing.props1
-rw-r--r--src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs7
-rw-r--r--src/Azure/AzureAD/Authentication.AzureADB2C.UI/test/AzureAdB2CAuthenticationBuilderExtensionsTests.cs5
-rw-r--r--src/DefaultBuilder/src/Microsoft.AspNetCore.csproj6
-rw-r--r--src/DefaultBuilder/src/PublicAPI.Unshipped.txt1
-rw-r--r--src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs48
-rw-r--r--src/DefaultBuilder/src/WebApplicationBuilder.cs19
-rw-r--r--src/Http/Authentication.Abstractions/src/IAuthenticationConfigurationProvider.cs20
-rw-r--r--src/Http/Authentication.Abstractions/src/PublicAPI.Unshipped.txt2
-rw-r--r--src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs1
-rw-r--r--src/Security/Authentication/Core/src/AuthAppBuilderExtensions.cs3
-rw-r--r--src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs1
-rw-r--r--src/Security/Authentication/Core/src/DefaultAuthenticationConfigurationProvider.cs21
-rw-r--r--src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj17
-rw-r--r--src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Program.cs32
-rw-r--r--src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/Properties/launchSettings.json31
-rw-r--r--src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.Development.json26
-rw-r--r--src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/appsettings.json9
-rw-r--r--src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs72
-rw-r--r--src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs13
-rw-r--r--src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt1
-rw-r--r--src/Security/Authentication/test/AuthenticationMiddlewareTests.cs19
-rw-r--r--src/Security/Authentication/test/CertificateTests.cs1
-rw-r--r--src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj1
-rw-r--r--src/Security/Authentication/test/SharedAuthenticationTests.cs9
-rw-r--r--src/Security/Security.slnf5
-rw-r--r--src/Shared/test/Certificates/Certificates.cs2
-rw-r--r--src/Tools/Tools.slnf2
-rw-r--r--src/Tools/dotnet-user-jwts/src/Commands/ClearCommand.cs69
-rw-r--r--src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs204
-rw-r--r--src/Tools/dotnet-user-jwts/src/Commands/DeleteCommand.cs56
-rw-r--r--src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs75
-rw-r--r--src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs74
-rw-r--r--src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs64
-rw-r--r--src/Tools/dotnet-user-jwts/src/Commands/ProjectCommandLineApplication.cs32
-rw-r--r--src/Tools/dotnet-user-jwts/src/Helpers/ConsoleTable.cs83
-rw-r--r--src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs181
-rw-r--r--src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs13
-rw-r--r--src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs34
-rw-r--r--src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs78
-rw-r--r--src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs15
-rw-r--r--src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs88
-rw-r--r--src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs44
-rw-r--r--src/Tools/dotnet-user-jwts/src/Program.cs52
-rw-r--r--src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj25
-rw-r--r--src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs102
-rw-r--r--src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs146
-rw-r--r--src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj16
49 files changed, 1884 insertions, 2 deletions
diff --git a/AspNetCore.sln b/AspNetCore.sln
index 88ed078aec..02b6e7c51c 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 66f8dc2ce8..db20e2e9a4 100644
--- a/eng/Signing.props
+++ b/eng/Signing.props
@@ -79,6 +79,7 @@
<FileSignInfo Include="dotnet-user-secrets.exe" CertificateName="MicrosoftDotNet500" />
<FileSignInfo Include="dotnet-watch.exe" CertificateName="MicrosoftDotNet500" />
<FileSignInfo Include="dotnet-openapi.exe" CertificateName="MicrosoftDotNet500" />
+ <FileSignInfo Include="dotnet-user-jwts.exe" CertificateName="MicrosoftDotNet500" />
<FileSignInfo Include="Microsoft.AspNetCore.Blazor.Build.exe" CertificateName="MicrosoftDotNet500" />
<FileSignInfo Include="sni.dll" CertificateName="MicrosoftDotNet500" />
diff --git a/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs b/src/Azure/AzureAD/Authentication.AzureAD.UI/test/AzureADAuthenticationBuilderExtensionsTests.cs
index 8070f11abb..616e249829 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.Cookies;
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 class AzureADAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -288,6 +290,7 @@ public class AzureADAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -305,6 +308,7 @@ public class AzureADAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -340,6 +344,7 @@ public class AzureADAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -373,6 +378,7 @@ public class AzureADAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -473,6 +479,7 @@ public class AzureADAuthenticationBuilderExtensionsTests
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(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 8c61fdb798..36abc8d6d5 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.Cookies;
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 class AzureADB2CAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -279,6 +281,7 @@ public class AzureADB2CAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -315,6 +318,7 @@ public class AzureADB2CAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
@@ -348,6 +352,7 @@ public class AzureADB2CAuthenticationBuilderExtensionsTests
// Arrange
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(new NullLoggerFactory());
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
// Act
services.AddAuthentication()
diff --git a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
index 2c66a8407d..669ecb8037 100644
--- a/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
+++ b/src/DefaultBuilder/src/Microsoft.AspNetCore.csproj
@@ -11,6 +11,8 @@
</PropertyGroup>
<ItemGroup>
+ <Reference Include="Microsoft.AspNetCore.Authentication" />
+ <Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
<Reference Include="Microsoft.AspNetCore.Diagnostics" />
<Reference Include="Microsoft.AspNetCore.HostFiltering" />
<Reference Include="Microsoft.AspNetCore.Hosting" />
@@ -32,4 +34,8 @@
<Reference Include="Microsoft.Extensions.Logging.EventSource" />
</ItemGroup>
+ <ItemGroup>
+ <InternalsVisibleTo Include="Microsoft.AspNetCore.Authentication.Test" />
+ </ItemGroup>
+
</Project>
diff --git a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt
index 88ff0b5ecc..6eba7bee0f 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<Microsoft.AspNetCore.Http.RequestDelegate!, Microsoft.AspNetCore.Http.RequestDelegate!>! 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<Microsoft.AspNetCore.Hosting.IWebHostBuilder!>! configure, System.Action<Microsoft.Extensions.Hosting.WebHostBuilderOptions!>! configureOptions) -> Microsoft.Extensions.Hosting.IHostBuilder!
diff --git a/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs b/src/DefaultBuilder/src/WebApplicationAuthenticationBuilder.cs
new file mode 100644
index 0000000000..f95ca8a058
--- /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<PolicySchemeOptions> configureOptions)
+ {
+ RegisterServices(authenticationScheme);
+ return base.AddPolicyScheme(authenticationScheme, displayName, configureOptions);
+ }
+
+ public override AuthenticationBuilder AddRemoteScheme<TOptions, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(string authenticationScheme, string? displayName, Action<TOptions>? configureOptions)
+ {
+ RegisterServices(authenticationScheme);
+ return base.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
+ }
+
+ public override AuthenticationBuilder AddScheme<TOptions, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(string authenticationScheme, string? displayName, Action<TOptions>? configureOptions)
+ {
+ RegisterServices(authenticationScheme);
+ return base.AddScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
+ }
+
+ public override AuthenticationBuilder AddScheme<TOptions, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(string authenticationScheme, Action<TOptions>? configureOptions)
+ {
+ RegisterServices(authenticationScheme);
+ return base.AddScheme<TOptions, THandler>(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 c5eabba681..b96b151174 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 @@ public sealed class WebApplicationBuilder
Host = new ConfigureHostBuilder(bootstrapHostBuilder.Context, Configuration, Services);
WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
+ _webAuthBuilder = new WebApplicationAuthenticationBuilder(Services);
}
/// <summary>
@@ -114,6 +118,11 @@ public sealed class WebApplicationBuilder
public ConfigureHostBuilder Host { get; }
/// <summary>
+ /// An <see cref="AuthenticationBuilder"/> for configuration authentication-related properties.
+ /// </summary>
+ public AuthenticationBuilder Authentication => _webAuthBuilder;
+
+ /// <summary>
/// Builds the <see cref="WebApplication"/>.
/// </summary>
/// <returns>A configured <see cref="WebApplication"/>.</returns>
@@ -166,6 +175,16 @@ public sealed class WebApplicationBuilder
}
}
+ 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 0000000000..0665de7acb
--- /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;
+
+/// <summary>
+/// Provides an interface for implmenting a construct that provides
+/// access to specific configuration sections.
+/// </summary>
+public interface IAuthenticationConfigurationProvider
+{
+ /// <summary>
+ /// Returns the specified <see cref="ConfigurationSection"/> object.
+ /// </summary>
+ /// <param name="authenticationScheme">The path to the section to be returned.</param>
+ /// <returns>The specified <see cref="ConfigurationSection"/> object, or null if the requested section does not exist.</returns>
+ 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 7dc5c58110..efe32d90a9 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 a729ef21e0..332fe321aa 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 8efc58c593..5c210ef7e9 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;
/// </summary>
public static class AuthAppBuilderExtensions
{
+ internal const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet";
+
/// <summary>
/// Adds the <see cref="AuthenticationMiddleware"/> to the specified <see cref="IApplicationBuilder"/>, which enables authentication capabilities.
/// </summary>
@@ -22,6 +24,7 @@ public static class AuthAppBuilderExtensions
throw new ArgumentNullException(nameof(app));
}
+ app.Properties[AuthenticationMiddlewareSetKey] = true;
return app.UseMiddleware<AuthenticationMiddleware>();
}
}
diff --git a/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs b/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs
index b29001aa37..2e34894d99 100644
--- a/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs
+++ b/src/Security/Authentication/Core/src/AuthenticationServiceCollectionExtensions.cs
@@ -27,6 +27,7 @@ public static class AuthenticationServiceCollectionExtensions
services.AddDataProtection();
services.AddWebEncoders();
services.TryAddSingleton<ISystemClock, SystemClock>();
+ services.TryAddSingleton<IAuthenticationConfigurationProvider, DefaultAuthenticationConfigurationProvider>();
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 0000000000..057e1a5ad1
--- /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 0000000000..e0ab758b6a
--- /dev/null
+++ b/src/Security/Authentication/JwtBearer/samples/MinimalJwtBearerSample/MinimalJwtBearerSample.csproj
@@ -0,0 +1,17 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+ <UserSecretsId>MinimalJwtBearerSample-20151210102827</UserSecretsId>
+ <Nullable>enable</Nullable>
+ <ImplicitUsings>enable</ImplicitUsings>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="Microsoft.AspNetCore" />
+ <Reference Include="Microsoft.AspNetCore.Authentication" />
+ <Reference Include="Microsoft.AspNetCore.Authentication.JwtBearer" />
+ <Reference Include="Microsoft.AspNetCore.Authorization.Policy" />
+ </ItemGroup>
+
+</Project>
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 0000000000..ff0c3ecd22
--- /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 0000000000..ea8f54aeb1
--- /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 0000000000..9fe2b7a74f
--- /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 0000000000..10f68b8c8b
--- /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 0000000000..7004f06a40
--- /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<JwtBearerOptions>
+{
+ private readonly IAuthenticationConfigurationProvider _authenticationConfigurationProvider;
+ private readonly IConfiguration _configuration;
+
+ /// <summary>
+ /// Initializes a new <see cref="JwtBearerConfigureOptions"/> given the configuration
+ /// provided by the <paramref name="configurationProvider"/>.
+ /// </summary>
+ /// <param name="configurationProvider">An <see cref="IAuthenticationConfigurationProvider"/> instance.</param>
+ /// <param name="configuration">An <see cref="IConfiguration"/> instance for accessing configuration elements not in the schema.</param>
+ public JwtBearerConfigureOptions(IAuthenticationConfigurationProvider configurationProvider, IConfiguration configuration)
+ {
+ _authenticationConfigurationProvider = configurationProvider;
+ _configuration = configuration;
+ }
+
+ /// <inheritdoc />
+ 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);
+ }
+
+ /// <inheritdoc />
+ 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 12022ed078..f4a7ec2e9a 100644
--- a/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs
+++ b/src/Security/Authentication/JwtBearer/src/JwtBearerExtensions.cs
@@ -25,6 +25,18 @@ public static class JwtBearerExtensions
=> builder.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, _ => { });
/// <summary>
+ /// Enables JWT-bearer authentication using a pre-defined scheme.
+ /// <para>
+ /// JWT bearer authentication performs authentication by extracting and validating a JWT token from the <c>Authorization</c> request header.
+ /// </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 AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme)
+ => builder.AddJwtBearer(authenticationScheme, _ => { });
+
+ /// <summary>
/// Enables JWT-bearer authentication using the default scheme <see cref="JwtBearerDefaults.AuthenticationScheme"/>.
/// <para>
/// JWT bearer authentication performs authentication by extracting and validating a JWT token from the <c>Authorization</c> request header.
@@ -62,6 +74,7 @@ public static class JwtBearerExtensions
/// <returns>A reference to <paramref name="builder"/> after the operation has completed.</returns>
public static AuthenticationBuilder AddJwtBearer(this AuthenticationBuilder builder, string authenticationScheme, string? displayName, Action<JwtBearerOptions> configureOptions)
{
+ builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<JwtBearerOptions>, JwtBearerConfigureOptions>());
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerPostConfigureOptions>());
return builder.AddScheme<JwtBearerOptions, JwtBearerHandler>(authenticationScheme, displayName, configureOptions);
}
diff --git a/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt b/src/Security/Authentication/JwtBearer/src/PublicAPI.Unshipped.txt
index 2c729aeee8..e66f11d82e 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 f6ac9acce5..eb883f111d 100644
--- a/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
+++ b/src/Security/Authentication/test/AuthenticationMiddlewareTests.cs
@@ -151,6 +151,25 @@ public class AuthenticationMiddlewareTests
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<WebApplicationAuthenticationBuilder>(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<IServiceCollection> registerServices = null,
IAuthenticationService authenticationService = null)
diff --git a/src/Security/Authentication/test/CertificateTests.cs b/src/Security/Authentication/test/CertificateTests.cs
index 03fd439a36..018ccb1391 100644
--- a/src/Security/Authentication/test/CertificateTests.cs
+++ b/src/Security/Authentication/test/CertificateTests.cs
@@ -6,6 +6,7 @@ using System.Net;
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 a65c8ad927..72dad5f713 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 @@
</ItemGroup>
<ItemGroup>
+ <Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Authentication.Certificate" />
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
<Reference Include="Microsoft.AspNetCore.Authentication.Facebook" />
diff --git a/src/Security/Authentication/test/SharedAuthenticationTests.cs b/src/Security/Authentication/test/SharedAuthenticationTests.cs
index 4ee984335d..f1085b6041 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<TOptions> where TOptions : Authe
public async Task CanForwardDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
@@ -165,6 +167,7 @@ public abstract class SharedAuthenticationTests<TOptions> where TOptions : Authe
public async Task ForwardForbidWinsOverDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -214,6 +217,7 @@ public abstract class SharedAuthenticationTests<TOptions> where TOptions : Authe
public async Task ForwardAuthenticateOnlyRunsTransformOnceByDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
var transform = new RunOnce();
var builder = services.AddSingleton<IClaimsTransformation>(transform).AddAuthentication(o =>
{
@@ -244,6 +248,7 @@ public abstract class SharedAuthenticationTests<TOptions> where TOptions : Authe
public async Task ForwardAuthenticateWinsOverDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -283,6 +288,7 @@ public abstract class SharedAuthenticationTests<TOptions> where TOptions : Authe
public async Task ForwardChallengeWinsOverDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -322,6 +328,7 @@ public abstract class SharedAuthenticationTests<TOptions> where TOptions : Authe
public async Task ForwardSelectorWinsOverDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -391,6 +398,7 @@ public abstract class SharedAuthenticationTests<TOptions> where TOptions : Authe
public async Task NullForwardSelectorUsesDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
@@ -460,6 +468,7 @@ public abstract class SharedAuthenticationTests<TOptions> where TOptions : Authe
public async Task SpecificForwardWinsOverSelectorAndDefault()
{
var services = new ServiceCollection().AddLogging();
+ services.AddSingleton<IConfiguration>(new ConfigurationManager());
var builder = services.AddAuthentication(o =>
{
o.DefaultScheme = DefaultScheme;
diff --git a/src/Security/Security.slnf b/src/Security/Security.slnf
index 0dd86cd84f..e8b8685e04 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 8124e9cdf0..d0c2a0c043 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 8527dde99d..e277c5a315 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 0000000000..e0880812fc
--- /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 0000000000..17ac345c59
--- /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<string>();
+ var scopes = scopesOption.HasValue() ? scopesOption.Values : new List<string>();
+
+ var claims = new Dictionary<string, string>();
+ 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 0000000000..83b287b81a
--- /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 0000000000..1637d7d7f6
--- /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 0000000000..8013e29899
--- /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 0000000000..3d144b9c50
--- /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 0000000000..a15391cc99
--- /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<ProjectCommandLineApplication> 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 0000000000..b7773b719d
--- /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<string> _columns = new();
+ private readonly List<object[]> _rows = new();
+ private readonly IReporter _reporter;
+
+ public ConsoleTable(IReporter reporter)
+ {
+ _reporter = reporter;
+ }
+
+ public void AddColumns(params string[] names)
+ {
+ _columns.AddRange(names);
+ }
+
+ public void AddRow(params object[] values)
+ {
+ if (values == null)
+ {
+ throw new ArgumentNullException(nameof(values));
+ }
+
+ if (!_columns.Any())
+ {
+ throw new Exception("Columns must be set before rows can be added.");
+ }
+
+ if (_columns.Count != values.Length)
+ {
+ throw new Exception(
+ $"The number of columns in the table '{_columns.Count}' does not match the number of columns in the row '{values.Length}'.");
+ }
+
+ _rows.Add(values);
+ }
+
+ public void Write()
+ {
+ var builder = new StringBuilder();
+
+ var maxColumnLengths = _columns
+ .Select((t, i) => _rows.Select(x => x[i])
+ .Concat(new[] { _columns[i] })
+ .Where(x => x != null)
+ .Select(x => x!.ToString()!.Length).Max())
+ .ToList();
+
+ var formatRow = Enumerable.Range(0, _columns.Count)
+ .Select(i => " | {" + i + ", " + maxColumnLengths[i] + "}")
+ .Aggregate((previousRowColumn, nextRowColumn) => previousRowColumn + nextRowColumn) + " |";
+
+ var formattedRows = _rows.Select(row => string.Format(CultureInfo.InvariantCulture, formatRow, row)).ToList();
+ var columnHeaders = string.Format(CultureInfo.InvariantCulture, formatRow, _columns.ToArray());
+ var rowDivider = $" {new string('-', columnHeaders.Length - 1)} ";
+
+ builder.AppendLine(rowDivider);
+ builder.AppendLine(columnHeaders);
+
+ foreach (var formattedRow in formattedRows)
+ {
+ builder.AppendLine(rowDivider);
+ builder.AppendLine(formattedRow);
+ }
+
+ builder.AppendLine(rowDivider);
+
+ _reporter.Output(builder.ToString());
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs
new file mode 100644
index 0000000000..40e35506b2
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs
@@ -0,0 +1,181 @@
+// 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 System.Linq;
+using System.Text.Json;
+using System.Xml.Linq;
+using System.Xml.XPath;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.UserSecrets;
+using Microsoft.Extensions.Tools.Internal;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal static class DevJwtCliHelpers
+{
+ public static string GetUserSecretsId(string projectFilePath)
+ {
+ var projectDocument = XDocument.Load(projectFilePath, LoadOptions.PreserveWhitespace);
+ var existingUserSecretsId = projectDocument.XPathSelectElements("//UserSecretsId").FirstOrDefault();
+
+ if (existingUserSecretsId == null)
+ {
+ return null;
+ }
+
+ return existingUserSecretsId.Value;
+ }
+
+ public static string GetProject(string projectPath = null)
+ {
+ if (projectPath is not null)
+ {
+ return projectPath;
+ }
+
+ var csprojFiles = Directory.EnumerateFileSystemEntries(Directory.GetCurrentDirectory(), "*.*proj", SearchOption.TopDirectoryOnly)
+ .Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase))
+ .ToList();
+ if (csprojFiles is [var path])
+ {
+ return path;
+ }
+ return null;
+ }
+
+ public static bool GetProjectAndSecretsId(string projectPath, IReporter reporter, out string project, out string userSecretsId)
+ {
+ project = GetProject(projectPath);
+ userSecretsId = null;
+ if (project == null)
+ {
+ reporter.Error($"No project found at `-p|--project` path or current directory.");
+ return false;
+ }
+
+ userSecretsId = GetUserSecretsId(project);
+ if (userSecretsId == null)
+ {
+ reporter.Error($"Project does not contain a user secrets ID.");
+ return false;
+ }
+ return true;
+ }
+
+ public static byte[] GetOrCreateSigningKeyMaterial(string userSecretsId)
+ {
+ var projectConfiguration = new ConfigurationBuilder()
+ .AddUserSecrets(userSecretsId)
+ .Build();
+
+ var signingKeyMaterial = projectConfiguration[DevJwtsDefaults.SigningKeyConfigurationKey];
+
+ var keyMaterial = new byte[DevJwtsDefaults.SigningKeyLength];
+ if (signingKeyMaterial is not null && Convert.TryFromBase64String(signingKeyMaterial, keyMaterial, out var bytesWritten) && bytesWritten == DevJwtsDefaults.SigningKeyLength)
+ {
+ return keyMaterial;
+ }
+
+ return CreateSigningKeyMaterial(userSecretsId);
+ }
+
+ public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = false)
+ {
+ // Create signing material and save to user secrets
+ var newKeyMaterial = System.Security.Cryptography.RandomNumberGenerator.GetBytes(DevJwtsDefaults.SigningKeyLength);
+ var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
+
+ IDictionary<string, string> secrets = null;
+ if (File.Exists(secretsFilePath))
+ {
+ using var secretsFileStream = new FileStream(secretsFilePath, FileMode.Open, FileAccess.Read);
+ if (secretsFileStream.Length > 0)
+ {
+ secrets = JsonSerializer.Deserialize<IDictionary<string, string>>(secretsFileStream) ?? new Dictionary<string, string>();
+ }
+ }
+
+ secrets ??= new Dictionary<string, string>();
+
+ if (reset && secrets.ContainsKey(DevJwtsDefaults.SigningKeyConfigurationKey))
+ {
+ secrets.Remove(DevJwtsDefaults.SigningKeyConfigurationKey);
+ }
+ secrets.Add(DevJwtsDefaults.SigningKeyConfigurationKey, Convert.ToBase64String(newKeyMaterial));
+
+ using var secretsWriteStream = new FileStream(secretsFilePath, FileMode.Create, FileAccess.Write);
+ JsonSerializer.Serialize(secretsWriteStream, secrets);
+
+ return newKeyMaterial;
+ }
+
+ public static string[] GetAudienceCandidatesFromLaunchSettings(string project)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(nameof(project));
+
+ var launchSettingsFilePath = Path.Combine(Path.GetDirectoryName(project)!, "Properties", "launchSettings.json");
+ if (File.Exists(launchSettingsFilePath))
+ {
+ using var launchSettingsFileStream = new FileStream(launchSettingsFilePath, FileMode.Open, FileAccess.Read);
+ if (launchSettingsFileStream.Length > 0)
+ {
+ var launchSettingsJson = JsonDocument.Parse(launchSettingsFileStream);
+ if (launchSettingsJson.RootElement.TryGetProperty("profiles", out var profiles))
+ {
+ var profilesEnumerator = profiles.EnumerateObject();
+ foreach (var profile in profilesEnumerator)
+ {
+ if (profile.Value.TryGetProperty("commandName", out var commandName))
+ {
+ if (commandName.ValueEquals("Project"))
+ {
+ if (profile.Value.TryGetProperty("applicationUrl", out var applicationUrl))
+ {
+ var value = applicationUrl.GetString();
+ if (value is { } applicationUrls)
+ {
+ return applicationUrls.Split(";");
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static void PrintJwt(IReporter reporter, Jwt jwt, JwtSecurityToken fullToken = null)
+ {
+ reporter.Output(JsonSerializer.Serialize(jwt, new JsonSerializerOptions { WriteIndented = true }));
+
+ if (fullToken is not null)
+ {
+ reporter.Output($"Token Header: {fullToken.Header.SerializeToJson()}");
+ reporter.Output($"Token Payload: {fullToken.Payload.SerializeToJson()}");
+ }
+ reporter.Output($"Compact Token: {jwt.Token}");
+ }
+
+ public static bool TryParseClaims(List<string> input, out Dictionary<string, string> claims)
+ {
+ claims = new Dictionary<string, string>();
+ foreach (var claim in input)
+ {
+ var parts = claim.Split('=');
+ if (parts.Length != 2)
+ {
+ return false;
+ }
+
+ var key = parts[0];
+ var value = parts[1];
+
+ claims.Add(key, value);
+ }
+ return true;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs
new file mode 100644
index 0000000000..595d7c510b
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs
@@ -0,0 +1,13 @@
+// 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.JwtBearer.Tools;
+
+internal static class DevJwtsDefaults
+{
+ public static string Issuer => "dotnet-user-jwts";
+
+ public static string SigningKeyConfigurationKey => $"{Issuer}:KeyMaterial";
+
+ public static int SigningKeyLength => 32;
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs b/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs
new file mode 100644
index 0000000000..e78112fa6c
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+public record Jwt(string Id, string Scheme, string Name, string Audience, DateTimeOffset NotBefore, DateTimeOffset Expires, DateTimeOffset Issued, string Token)
+{
+ public IEnumerable<string> Scopes { get; set; } = new List<string>();
+
+ public IEnumerable<string> Roles { get; set; } = new List<string>();
+
+ public IDictionary<string, string> CustomClaims { get; set; } = new Dictionary<string, string>();
+
+ public override string ToString() => Token;
+
+ public static Jwt Create(
+ string scheme,
+ JwtSecurityToken token,
+ string encodedToken,
+ IEnumerable<string> scopes = null,
+ IEnumerable<string> roles = null,
+ IDictionary<string, string> customClaims = null)
+ {
+ return new Jwt(token.Id, scheme, token.Subject, token.Audiences.FirstOrDefault(), token.ValidFrom, token.ValidTo, token.IssuedAt, encodedToken)
+ {
+ Scopes = scopes,
+ Roles = roles,
+ CustomClaims = customClaims
+ };
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs
new file mode 100644
index 0000000000..b8108f5294
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtAuthenticationSchemeSettings.cs
@@ -0,0 +1,78 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed record JwtAuthenticationSchemeSettings(string SchemeName, List<string> Audiences, string ClaimsIssuer)
+{
+ private const string AuthenticationKey = "Authentication";
+ private const string SchemesKey = "Schemes";
+
+ private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
+ {
+ WriteIndented = true,
+ };
+
+ public void Save(string filePath)
+ {
+ using var reader = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+ var config = JsonSerializer.Deserialize<JsonObject>(reader, _jsonSerializerOptions);
+ reader.Close();
+
+ var settingsObject = new JsonObject
+ {
+ [nameof(Audiences)] = new JsonArray(Audiences.Select(aud => JsonValue.Create(aud)).ToArray()),
+ [nameof(ClaimsIssuer)] = ClaimsIssuer
+ };
+
+ if (config[AuthenticationKey] is JsonObject authentication)
+ {
+ if (authentication[SchemesKey] is JsonObject schemes)
+ {
+ // If a scheme with the same name has already been registered, we
+ // override with the latest token's options
+ schemes[SchemeName] = settingsObject;
+ }
+ else
+ {
+ authentication.Add(SchemesKey, new JsonObject
+ {
+ [SchemeName] = settingsObject
+ });
+ }
+ }
+ else
+ {
+ config[AuthenticationKey] = new JsonObject
+ {
+ [SchemesKey] = new JsonObject
+ {
+ [SchemeName] = settingsObject
+ }
+ };
+ }
+
+ using var writer = new FileStream(filePath, FileMode.Open, FileAccess.Write);
+ JsonSerializer.Serialize(writer, config, _jsonSerializerOptions);
+ }
+
+ public static void RemoveScheme(string filePath, string name)
+ {
+ using var reader = new FileStream(filePath, FileMode.Open, FileAccess.Read);
+ var config = JsonSerializer.Deserialize<JsonObject>(reader);
+ reader.Close();
+
+ if (config[AuthenticationKey] is JsonObject authentication &&
+ authentication[SchemesKey] is JsonObject schemes)
+ {
+ schemes.Remove(name);
+ }
+
+ using var writer = new FileStream(filePath, FileMode.Create, FileAccess.Write);
+ JsonSerializer.Serialize(writer, config, _jsonSerializerOptions);
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs
new file mode 100644
index 0000000000..589f3d5d07
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtCreatorOptions.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed record JwtCreatorOptions(
+ string Scheme,
+ string Name,
+ List<string> Audiences,
+ string Issuer,
+ DateTime NotBefore,
+ DateTime ExpiresOn,
+ List<string> Roles,
+ List<string> Scopes,
+ Dictionary<string, string> Claims);
diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs
new file mode 100644
index 0000000000..cf086d1d76
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtIssuer.cs
@@ -0,0 +1,88 @@
+// 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.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Claims;
+using System.Security.Principal;
+using Microsoft.IdentityModel.Tokens;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+internal sealed class JwtIssuer
+{
+ private readonly SymmetricSecurityKey _signingKey;
+
+ public JwtIssuer(string issuer, byte[] signingKeyMaterial)
+ {
+ Issuer = issuer;
+ _signingKey = new SymmetricSecurityKey(signingKeyMaterial);
+ }
+
+ public string Issuer { get; }
+
+ public JwtSecurityToken Create(JwtCreatorOptions options)
+ {
+ var identity = new GenericIdentity(options.Name);
+
+ identity.AddClaim(new Claim(JwtRegisteredClaimNames.Sub, options.Name));
+
+ var id = Guid.NewGuid().ToString().GetHashCode().ToString("x", CultureInfo.InvariantCulture);
+ identity.AddClaim(new Claim(JwtRegisteredClaimNames.Jti, id));
+
+ if (options.Scopes is { } scopesToAdd)
+ {
+ identity.AddClaims(scopesToAdd.Select(s => new Claim("scope", s)));
+ }
+
+ if (options.Roles is { } rolesToAdd)
+ {
+ identity.AddClaims(rolesToAdd.Select(r => new Claim(ClaimTypes.Role, r)));
+ }
+
+ if (options.Claims is { Count: > 0 } claimsToAdd)
+ {
+ identity.AddClaims(claimsToAdd.Select(kvp => new Claim(kvp.Key, kvp.Value)));
+ }
+
+ // Although the JwtPayload supports having multiple audiences registered, the
+ // creator methods and constructors don't provide a way of setting multiple
+ // audiences. Instead, we have to register an `aud` claim for each audience
+ // we want to add so that the multiple audiences are populated correctly.
+ if (options.Audiences is { Count: > 0} audiences)
+ {
+ identity.AddClaims(audiences.Select(aud => new Claim(JwtRegisteredClaimNames.Aud, aud)));
+ }
+
+ var handler = new JwtSecurityTokenHandler();
+ var jwtSigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256Signature);
+ var jwtToken = handler.CreateJwtSecurityToken(Issuer, audience: null, identity, options.NotBefore, options.ExpiresOn, issuedAt: DateTime.UtcNow, jwtSigningCredentials);
+ return jwtToken;
+ }
+
+ public static string WriteToken(JwtSecurityToken token)
+ {
+ var handler = new JwtSecurityTokenHandler();
+ return handler.WriteToken(token);
+ }
+
+ public static JwtSecurityToken Extract(string token) => new JwtSecurityToken(token);
+
+ public bool IsValid(string encodedToken)
+ {
+ var handler = new JwtSecurityTokenHandler();
+ var tokenValidationParameters = new TokenValidationParameters
+ {
+ IssuerSigningKey = _signingKey,
+ ValidateAudience = false,
+ ValidateIssuer = false,
+ ValidateIssuerSigningKey = true
+ };
+ if (handler.ValidateToken(encodedToken, tokenValidationParameters, out _).Identity?.IsAuthenticated == true)
+ {
+ return true;
+ }
+ return false;
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs
new file mode 100644
index 0000000000..8bffc9d9c2
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Helpers/JwtStore.cs
@@ -0,0 +1,44 @@
+// 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;
+using Microsoft.Extensions.Configuration.UserSecrets;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+
+public class JwtStore
+{
+ private const string FileName = "user-jwts.json";
+ private readonly string _userSecretsId;
+ private readonly string _filePath;
+
+ public JwtStore(string userSecretsId)
+ {
+ _userSecretsId = userSecretsId;
+ _filePath = Path.Combine(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(userSecretsId)), FileName);
+ Load();
+ }
+
+ public IDictionary<string, Jwt> Jwts { get; private set; } = new Dictionary<string, Jwt>();
+
+ public void Load()
+ {
+ if (File.Exists(_filePath))
+ {
+ using var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
+ if (fileStream.Length > 0)
+ {
+ Jwts = JsonSerializer.Deserialize<IDictionary<string, Jwt>>(fileStream) ?? new Dictionary<string, Jwt>();
+ }
+ }
+ }
+
+ public void Save()
+ {
+ if (Jwts is not null)
+ {
+ using var fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write);
+ JsonSerializer.Serialize(fileStream, Jwts);
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/Program.cs b/src/Tools/dotnet-user-jwts/src/Program.cs
new file mode 100644
index 0000000000..24d2ea9daf
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/Program.cs
@@ -0,0 +1,52 @@
+// 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;
+
+public class Program
+{
+ private readonly IConsole _console;
+ private readonly IReporter _reporter;
+
+ public Program(IConsole console)
+ {
+ _console = console;
+ _reporter = new ConsoleReporter(console);
+ }
+
+ public static void Main(string[] args)
+ {
+ new Program(PhysicalConsole.Singleton).Run(args);
+ }
+
+ public void Run(string[] args)
+ {
+ ProjectCommandLineApplication userJwts = new(_reporter)
+ {
+ Name = "dotnet user-jwts"
+ };
+
+ userJwts.HelpOption("-h|--help");
+
+ // dotnet user-jwts list
+ ListCommand.Register(userJwts);
+ // dotnet user-jwts create
+ CreateCommand.Register(userJwts);
+ // dotnet user-jwts print ecd045
+ PrintCommand.Register(userJwts);
+ // dotnet user-jwts delete ecd045
+ DeleteCommand.Register(userJwts);
+ // dotnet user-jwts clear
+ ClearCommand.Register(userJwts);
+ // dotnet user-jwts key
+ KeyCommand.Register(userJwts);
+
+ // Show help information if no subcommand/option was specified.
+ userJwts.OnExecute(() => userJwts.ShowHelp());
+
+ userJwts.Execute(args);
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj b/src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj
new file mode 100644
index 0000000000..93b34c52c6
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj
@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+ <OutputType>exe</OutputType>
+ <Description>Command line tool to manage JSON Web Tokens in a user application.</Description>
+ <GenerateUserSecretsAttribute>false</GenerateUserSecretsAttribute>
+ <PackageTags>configuration;authentication;authorization;jwt</PackageTags>
+ <RootNamespace>Microsoft.AspNetCore.Authentication.JwtBearer.Tools</RootNamespace>
+ <PackAsTool>true</PackAsTool>
+ <!-- This package is for internal use only. It contains a CLI which is bundled in the .NET Core SDK. -->
+ <IsShippingPackage>false</IsShippingPackage>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" LinkBase="Shared" />
+ <Compile Include="$(ToolSharedSourceRoot)CommandLine\**\*.cs" LinkBase="Shared" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <Reference Include="System.IdentityModel.Tokens.Jwt" />
+ <Reference Include="Microsoft.Extensions.Configuration.Abstractions" />
+ <Reference Include="Microsoft.Extensions.Configuration" />
+ <Reference Include="Microsoft.Extensions.Configuration.UserSecrets" />
+ </ItemGroup>
+</Project> \ No newline at end of file
diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs
new file mode 100644
index 0000000000..08e003c8e4
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs
@@ -0,0 +1,102 @@
+// 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.IO;
+using Microsoft.Extensions.Configuration.UserSecrets;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests;
+
+public class UserJwtsTestFixture : IDisposable
+{
+ private Stack<Action> _disposables = new Stack<Action>();
+ private string TestSecretsId = Guid.NewGuid().ToString();
+
+ private const string ProjectTemplate = @"<Project Sdk=""Microsoft.NET.Sdk"">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net7.0</TargetFramework>
+ {0}
+ <EnableDefaultCompileItems>false</EnableDefaultCompileItems>
+ </PropertyGroup>
+</Project>";
+
+ private const string LaunchSettingsTemplate = @"
+{
+ ""profiles"": {
+ ""HttpApiSampleApp"": {
+ ""commandName"": ""Project"",
+ ""dotnetRunMessages"": true,
+ ""launchBrowser"": true,
+ ""applicationUrl"": ""https://localhost:5001;http://localhost:5000"",
+ ""environmentVariables"": {
+ ""ASPNETCORE_ENVIRONMENT"": ""Development""
+ }
+ }
+ }
+}";
+
+ public string CreateProject(bool hasSecret = true)
+ {
+ var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "userjwtstest", Guid.NewGuid().ToString()));
+ Directory.CreateDirectory(Path.Combine(projectPath.FullName, "Properties"));
+ var prop = hasSecret ? $"<UserSecretsId>{TestSecretsId}</UserSecretsId>" : string.Empty;
+ if (hasSecret)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(TestSecretsId)));
+ }
+
+ File.WriteAllText(
+ Path.Combine(projectPath.FullName, "TestProject.csproj"),
+ string.Format(CultureInfo.InvariantCulture, ProjectTemplate, prop));
+
+ File.WriteAllText(Path.Combine(projectPath.FullName, "Properties", "launchSettings.json"),
+ LaunchSettingsTemplate);
+
+ File.WriteAllText(
+ Path.Combine(projectPath.FullName, "appsettings.Development.json"),
+ "{}");
+
+ if (hasSecret)
+ {
+ _disposables.Push(() =>
+ {
+ try
+ {
+ var secretsDir = Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(TestSecretsId));
+ TryDelete(TestSecretsId);
+ }
+ catch { }
+ });
+ }
+
+ _disposables.Push(() => TryDelete(projectPath.FullName));
+
+ return projectPath.FullName;
+ }
+
+ private static void TryDelete(string directory)
+ {
+ try
+ {
+ if (Directory.Exists(directory))
+ {
+ Directory.Delete(directory, true);
+ }
+ }
+ catch (Exception)
+ {
+ Console.WriteLine("Failed to delete " + directory);
+ }
+ }
+
+ public void Dispose()
+ {
+ while (_disposables.Count > 0)
+ {
+ _disposables.Pop()?.Invoke();
+ }
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs
new file mode 100644
index 0000000000..ac0cf3d957
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs
@@ -0,0 +1,146 @@
+// 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.IO;
+using System.Text;
+using Microsoft.AspNetCore.Testing;
+using Microsoft.Extensions.Tools.Internal;
+using Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
+using Xunit;
+using Xunit.Abstractions;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests;
+
+public class UserJwtsTests : IClassFixture<UserJwtsTestFixture>
+{
+ private readonly TestConsole _console;
+ private readonly UserJwtsTestFixture _fixture;
+ private readonly ITestOutputHelper _testOut;
+
+ public UserJwtsTests(UserJwtsTestFixture fixture, ITestOutputHelper output)
+ {
+ _fixture = fixture;
+ _testOut = output;
+ _console = new TestConsole(output);
+ }
+
+ [Fact]
+ public void List_NoTokensForNewProject()
+ {
+ var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
+ var app = new Program(_console);
+
+ app.Run(new[] { "list", "--project", project });
+ Assert.Contains("No JWTs created yet!", _console.GetOutput());
+ }
+
+ [Fact]
+ public void List_HandlesNoSecretsInProject()
+ {
+ var project = Path.Combine(_fixture.CreateProject(false), "TestProject.csproj");
+ var app = new Program(_console);
+
+ app.Run(new[] { "list", "--project", project });
+ Assert.Contains("Project does not contain a user secrets ID.", _console.GetOutput());
+ }
+
+ [Fact]
+ public void Create_WarnsOnNoSecretInproject()
+ {
+ var project = Path.Combine(_fixture.CreateProject(false), "TestProject.csproj");
+ var app = new Program(_console);
+
+ app.Run(new[] { "create", "--project", project });
+ Assert.Contains("Project does not contain a user secrets ID.", _console.GetOutput());
+ }
+
+ [Fact]
+ public void Create_WritesGeneratedTokenToDisk()
+ {
+ var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
+ var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
+ var app = new Program(_console);
+
+ app.Run(new[] { "create", "--project", project });
+ Assert.Contains("New JWT saved", _console.GetOutput());
+ Assert.Contains("dotnet-user-jwts", File.ReadAllText(appsettings));
+ }
+
+ [Fact]
+ public void Print_ReturnsNothingForMissingToken()
+ {
+ var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
+ var app = new Program(_console);
+
+ app.Run(new[] { "print", "invalid-id", "--project", project });
+ Assert.Contains("No token with ID 'invalid-id' found", _console.GetOutput());
+ }
+
+ [Fact]
+ public void List_ReturnsIdForGeneratedToken()
+ {
+ var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
+ var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
+ var app = new Program(_console);
+
+ app.Run(new[] { "create", "--project", project, "--scheme", "MyCustomScheme" });
+ Assert.Contains("New JWT saved", _console.GetOutput());
+
+ app.Run(new[] { "list", "--project", project });
+ Assert.Contains("MyCustomScheme", _console.GetOutput());
+ }
+
+ [Fact]
+ public void Delete_RemovesGeneratedToken()
+ {
+ var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
+ var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
+ var app = new Program(_console);
+
+ app.Run(new[] { "create", "--project", project });
+ var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'");
+ var id = matches.SingleOrDefault().Groups[1].Value;
+ app.Run(new[] { "create", "--project", project, "--scheme", "Scheme2" });
+
+ app.Run(new[] { "delete", id, "--project", project });
+ var appsettingsContent = File.ReadAllText(appsettings);
+ Assert.DoesNotContain("Bearer", appsettingsContent);
+ Assert.Contains("Scheme2", appsettingsContent);
+ }
+
+ [Fact]
+ public void Clear_RemovesGeneratedTokens()
+ {
+ var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
+ var appsettings = Path.Combine(Path.GetDirectoryName(project), "appsettings.Development.json");
+ var app = new Program(_console);
+
+ app.Run(new[] { "create", "--project", project });
+ app.Run(new[] { "create", "--project", project, "--scheme", "Scheme2" });
+
+ Assert.Contains("New JWT saved", _console.GetOutput());
+
+ app.Run(new[] { "clear", "--project", project, "--force" });
+ var appsettingsContent = File.ReadAllText(appsettings);
+ Assert.DoesNotContain("Bearer", appsettingsContent);
+ Assert.DoesNotContain("Scheme2", appsettingsContent);
+ }
+
+ [Fact]
+ public void Key_CanResetSigningKey()
+ {
+ var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj");
+ var app = new Program(_console);
+
+ app.Run(new[] { "create", "--project", project });
+ app.Run(new[] { "key", "--project", project });
+ Assert.Contains("Signing Key:", _console.GetOutput());
+
+ app.Run(new[] { "key", "--reset", "--force", "--project", project });
+ Assert.Contains("New signing key created:", _console.GetOutput());
+ }
+}
diff --git a/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj b/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj
new file mode 100644
index 0000000000..84d7ec58c9
--- /dev/null
+++ b/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj
@@ -0,0 +1,16 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+ <AssemblyName>Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests</AssemblyName>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Compile Include="$(ToolSharedSourceRoot)TestHelpers\**\*.cs" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\src\dotnet-user-jwts.csproj" />
+ </ItemGroup>
+
+</Project> \ No newline at end of file