diff options
author | Safia Abdalla <safia@microsoft.com> | 2022-07-07 02:03:37 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-07-07 06:52:25 +0300 |
commit | accc045a64e642963471efc7b826851be6ab0294 (patch) | |
tree | a667e96f47a195c98355a2a0e15519d040f58fff | |
parent | 5c3a312e6c8798efb62b9b5ccd0ea8620be11118 (diff) |
Fix up signing key handling in user-jwts and runtimesaf/config-fixes-jwt
7 files changed, 108 insertions, 29 deletions
diff --git a/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs b/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs index 995eed4203..fba8afab01 100644 --- a/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs +++ b/src/Security/Authentication/JwtBearer/src/JwtBearerConfigureOptions.cs @@ -13,18 +13,15 @@ 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) + /// <param name="configurationProvider">An <see cref="IAuthenticationConfigurationProvider"/> instance.</param>\ + public JwtBearerConfigureOptions(IAuthenticationConfigurationProvider configurationProvider) { _authenticationConfigurationProvider = configurationProvider; - _configuration = configuration; } /// <inheritdoc /> @@ -51,7 +48,7 @@ internal sealed class JwtBearerConfigureOptions : IConfigureNamedOptions<JwtBear ValidateAudience = audiences.Length > 0, ValidAudiences = audiences, ValidateIssuerSigningKey = true, - IssuerSigningKey = GetIssuerSigningKey(_configuration, issuer), + IssuerSigningKey = GetIssuerSigningKey(configSection, issuer), }; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index 5f45ce7b98..da19d155dd 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -221,7 +221,7 @@ internal sealed class CreateCommand { return 1; } - var keyMaterial = DevJwtCliHelpers.GetOrCreateSigningKeyMaterial(userSecretsId); + var keyMaterial = DevJwtCliHelpers.GetOrCreateSigningKeyMaterial(userSecretsId, options.Scheme, options.Issuer); var jwtIssuer = new JwtIssuer(options.Issuer, keyMaterial); var jwtToken = jwtIssuer.Create(options); diff --git a/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs index a90eb4df14..6e3628c3ba 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/KeyCommand.cs @@ -15,6 +15,18 @@ internal sealed class KeyCommand { cmd.Description = Resources.KeyCommand_Description; + var schemeOption = cmd.Option( + "--scheme", + Resources.KeyCommand_SchemeOption_Description, + CommandOptionType.SingleValue + ); + + var issuerOption = cmd.Option( + "--issuer", + Resources.KeyCommand_IssuerOption_Description, + CommandOptionType.SingleValue + ); + var resetOption = cmd.Option( "--reset", Resources.KeyCommand_ResetOption_Description, @@ -29,12 +41,16 @@ internal sealed class KeyCommand cmd.OnExecute(() => { - return Execute(cmd.Reporter, cmd.ProjectOption.Value(), resetOption.HasValue(), forceOption.HasValue()); + return Execute(cmd.Reporter, + cmd.ProjectOption.Value(), + schemeOption.Value() ?? DevJwtsDefaults.Scheme, + issuerOption.Value() ?? DevJwtsDefaults.Issuer, + resetOption.HasValue(), forceOption.HasValue()); }); }); } - private static int Execute(IReporter reporter, string projectPath, bool reset, bool force) + private static int Execute(IReporter reporter, string projectPath, string schemeName, string issuer, bool reset, bool force) { if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var _, out var userSecretsId)) { @@ -54,15 +70,15 @@ internal sealed class KeyCommand } } - var key = DevJwtCliHelpers.CreateSigningKeyMaterial(userSecretsId, reset: true); + var key = DevJwtCliHelpers.CreateSigningKeyMaterial(userSecretsId, schemeName, issuer, reset: true); reporter.Output(Resources.FormatKeyCommand_KeyCreated(Convert.ToBase64String(key))); - return 0; + return 0; } var projectConfiguration = new ConfigurationBuilder() .AddUserSecrets(userSecretsId) .Build(); - var signingKeyMaterial = projectConfiguration[DevJwtsDefaults.SigningKeyConfigurationKey]; + var signingKeyMaterial = projectConfiguration[$"{schemeName}:{issuer}:{DevJwtsDefaults.SigningKeyConfigurationKey}"]; if (signingKeyMaterial is null) { diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index 2dbdda44f2..6ed8c54eeb 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -61,13 +61,13 @@ internal static class DevJwtCliHelpers return true; } - public static byte[] GetOrCreateSigningKeyMaterial(string userSecretsId) + public static byte[] GetOrCreateSigningKeyMaterial(string userSecretsId, string schemeName, string issuer) { var projectConfiguration = new ConfigurationBuilder() .AddUserSecrets(userSecretsId) .Build(); - var signingKeyMaterial = projectConfiguration[DevJwtsDefaults.SigningKeyConfigurationKey]; + var signingKeyMaterial = projectConfiguration[$"{schemeName}:{issuer}:{DevJwtsDefaults.SigningKeyConfigurationKey}"]; var keyMaterial = new byte[DevJwtsDefaults.SigningKeyLength]; if (signingKeyMaterial is not null && Convert.TryFromBase64String(signingKeyMaterial, keyMaterial, out var bytesWritten) && bytesWritten == DevJwtsDefaults.SigningKeyLength) @@ -75,10 +75,10 @@ internal static class DevJwtCliHelpers return keyMaterial; } - return CreateSigningKeyMaterial(userSecretsId); + return CreateSigningKeyMaterial(userSecretsId, schemeName, issuer); } - public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = false) + public static byte[] CreateSigningKeyMaterial(string userSecretsId, string schemeName, string issuer, bool reset = false) { // Create signing material and save to user secrets var newKeyMaterial = System.Security.Cryptography.RandomNumberGenerator.GetBytes(DevJwtsDefaults.SigningKeyLength); @@ -96,12 +96,13 @@ internal static class DevJwtCliHelpers } secrets ??= new JsonObject(); + var key = $"{schemeName}:{issuer}:{DevJwtsDefaults.SigningKeyConfigurationKey}"; - if (reset && secrets.ContainsKey(DevJwtsDefaults.SigningKeyConfigurationKey)) + if (reset && secrets.ContainsKey(key)) { - secrets.Remove(DevJwtsDefaults.SigningKeyConfigurationKey); + secrets.Remove(key); } - secrets.Add(DevJwtsDefaults.SigningKeyConfigurationKey, JsonValue.Create(Convert.ToBase64String(newKeyMaterial))); + secrets.Add(key, JsonValue.Create(Convert.ToBase64String(newKeyMaterial))); using var secretsWriteStream = new FileStream(secretsFilePath, FileMode.Create, FileAccess.Write); JsonSerializer.Serialize(secretsWriteStream, secrets); diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs index 595d7c510b..13a1375b69 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtDefaults.cs @@ -5,9 +5,10 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; internal static class DevJwtsDefaults { + public static string Scheme => "Bearer"; public static string Issuer => "dotnet-user-jwts"; - public static string SigningKeyConfigurationKey => $"{Issuer}:KeyMaterial"; + public static string SigningKeyConfigurationKey => "KeyMaterial"; public static int SigningKeyLength => 32; } diff --git a/src/Tools/dotnet-user-jwts/src/Resources.resx b/src/Tools/dotnet-user-jwts/src/Resources.resx index 06fbf4f2ee..f20ea93812 100644 --- a/src/Tools/dotnet-user-jwts/src/Resources.resx +++ b/src/Tools/dotnet-user-jwts/src/Resources.resx @@ -246,6 +246,9 @@ <data name="KeyCommand_ForceOption_Description" xml:space="preserve"> <value>Don't prompt for confirmation before resetting the signing key.</value> </data> + <data name="KeyCommand_IssuerOption_Description" xml:space="preserve"> + <value>The issuer associated with the signing key to be deleted. Defaults to 'dotnet-user-jwts'.</value> + </data> <data name="KeyCommand_KeyCreated" xml:space="preserve"> <value>New signing key created: '{0}'</value> </data> @@ -258,6 +261,9 @@ <data name="KeyCommand_ResetOption_Description" xml:space="preserve"> <value>Reset the signing key. This will invalidate all previously issued JWTs for this project.</value> </data> + <data name="KeyCommand_SchemeOption_Description" xml:space="preserve"> + <value>The scheme name associated with the signing key to be deleted. Defaults to 'Bearer'.</value> + </data> <data name="ListCommand_Description" xml:space="preserve"> <value>Lists the JWTs issued for the project</value> </data> diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs index b8e0902112..649f0517e8 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -116,7 +116,7 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> app.Run(new[] { "remove", id, "--project", project }); var appsettingsContent = File.ReadAllText(appsettings); - Assert.DoesNotContain("Bearer", appsettingsContent); + Assert.DoesNotContain(DevJwtsDefaults.Scheme, appsettingsContent); Assert.Contains("Scheme2", appsettingsContent); } @@ -134,7 +134,7 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> app.Run(new[] { "clear", "--project", project, "--force" }); var appsettingsContent = File.ReadAllText(appsettings); - Assert.DoesNotContain("Bearer", appsettingsContent); + Assert.DoesNotContain(DevJwtsDefaults.Scheme, appsettingsContent); Assert.DoesNotContain("Scheme2", appsettingsContent); } @@ -175,7 +175,7 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> using FileStream openStream = File.OpenRead(secretsFilePath); var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream); Assert.NotNull(secretsJson); - Assert.True(secretsJson.ContainsKey(DevJwtsDefaults.SigningKeyConfigurationKey)); + Assert.True(secretsJson.ContainsKey($"{DevJwtsDefaults.Scheme}:{DevJwtsDefaults.Issuer}:{DevJwtsDefaults.SigningKeyConfigurationKey}")); Assert.True(secretsJson.TryGetPropertyValue("Foo", out var fooField)); Assert.Equal("baz", fooField["Bar"].GetValue<string>()); } @@ -263,7 +263,7 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> Assert.Contains($"ID: {id}", output); Assert.Contains($"Name: {Environment.UserName}", output); - Assert.Contains($"Scheme: Bearer", output); + Assert.Contains($"Scheme: {DevJwtsDefaults.Scheme}", output); Assert.Contains($"Audience(s): http://localhost:23528, https://localhost:44395, https://localhost:5001, http://localhost:5000", output); } @@ -282,7 +282,7 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> Assert.Contains($"ID: {id}", output); Assert.Contains($"Name: {Environment.UserName}", output); - Assert.Contains($"Scheme: Bearer", output); + Assert.Contains($"Scheme: {DevJwtsDefaults.Scheme}", output); Assert.Contains($"Audience(s): http://localhost:23528, https://localhost:44395, https://localhost:5001, http://localhost:5000", output); Assert.Contains($"Roles: [foobar]", output); Assert.DoesNotContain("Custom Claims", output); @@ -303,7 +303,7 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> Assert.Contains($"ID: {id}", output); Assert.Contains($"Name: {Environment.UserName}", output); - Assert.Contains($"Scheme: Bearer", output); + Assert.Contains($"Scheme: {DevJwtsDefaults.Scheme}", output); Assert.Contains($"Audience(s): http://localhost:23528, https://localhost:44395, https://localhost:5001, http://localhost:5000", output); Assert.Contains($"Scopes: none", output); Assert.Contains($"Roles: [none]", output); @@ -321,7 +321,7 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> var deserialized = JsonSerializer.Deserialize<Jwt>(output); Assert.NotNull(deserialized); - Assert.Equal("Bearer", deserialized.Scheme); + Assert.Equal(DevJwtsDefaults.Scheme, deserialized.Scheme); Assert.Equal(Environment.UserName, deserialized.Name); } @@ -374,7 +374,7 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> using FileStream openStream = File.OpenRead(secretsFilePath); var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream); Assert.NotNull(secretsJson); - Assert.True(secretsJson.ContainsKey(DevJwtsDefaults.SigningKeyConfigurationKey)); + Assert.True(secretsJson.ContainsKey($"{DevJwtsDefaults.Scheme}:{DevJwtsDefaults.Issuer}:{DevJwtsDefaults.SigningKeyConfigurationKey}")); Assert.True(secretsJson.TryGetPropertyValue("Foo", out var fooField)); Assert.Equal("baz", fooField["Bar"].GetValue<string>()); } @@ -384,7 +384,6 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> { var projectPath = _fixture.CreateProject(); var project = Path.Combine(projectPath, "TestProject.csproj"); - var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(_fixture.TestSecretsId); var app = new Program(_console); app.Run(new[] { "create", "--project", project}); @@ -396,4 +395,63 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture> Assert.Contains("New JWT saved", output); Assert.Contains($"Audience(s): http://localhost:23528, https://localhost:44395, https://localhost:5001, http://localhost:5000", output); } + + [Fact] + public async Task Create_SupportsSettingACustomIssuerAndScheme() + { + var projectPath = _fixture.CreateProject(); + var project = Path.Combine(projectPath, "TestProject.csproj"); + var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(_fixture.TestSecretsId); + + var app = new Program(_console); + app.Run(new[] { "create", "--project", project, "--issuer", "test-issuer", "--scheme", "test-scheme" }); + var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'"); + var id = matches.SingleOrDefault().Groups[1].Value; + app.Run(new[] { "print", id, "--project", project, "--show-all" }); + var output = _console.GetOutput(); + + Assert.Contains("New JWT saved", output); + + using FileStream openStream = File.OpenRead(secretsFilePath); + var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream); + Assert.True(secretsJson.ContainsKey($"test-scheme:test-issuer:KeyMaterial")); + } + + [Fact] + public async Task Create_SupportsSettingMutlipleIssuersAndSingleScheme() + { + var projectPath = _fixture.CreateProject(); + var project = Path.Combine(projectPath, "TestProject.csproj"); + var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(_fixture.TestSecretsId); + + var app = new Program(_console); + app.Run(new[] { "create", "--project", project, "--issuer", "test-issuer", "--scheme", "test-scheme" }); + app.Run(new[] { "create", "--project", project, "--issuer", "test-issuer-2", "--scheme", "test-scheme" }); + + Assert.Contains("New JWT saved", _console.GetOutput()); + + using FileStream openStream = File.OpenRead(secretsFilePath); + var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream); + Assert.True(secretsJson.ContainsKey($"test-scheme:test-issuer:KeyMaterial")); + Assert.True(secretsJson.ContainsKey($"test-scheme:test-issuer-2:KeyMaterial")); + } + + [Fact] + public async Task Create_SupportsSettingSingleIssuerAndMultipleSchemes() + { + var projectPath = _fixture.CreateProject(); + var project = Path.Combine(projectPath, "TestProject.csproj"); + var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(_fixture.TestSecretsId); + + var app = new Program(_console); + app.Run(new[] { "create", "--project", project, "--issuer", "test-issuer", "--scheme", "test-scheme" }); + app.Run(new[] { "create", "--project", project, "--issuer", "test-issuer", "--scheme", "test-scheme-2" }); + + Assert.Contains("New JWT saved", _console.GetOutput()); + + using FileStream openStream = File.OpenRead(secretsFilePath); + var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream); + Assert.True(secretsJson.ContainsKey($"test-scheme:test-issuer:KeyMaterial")); + Assert.True(secretsJson.ContainsKey($"test-scheme-2:test-issuer:KeyMaterial")); + } } |