From 3f7ec21317e7c0264e6087c40bc470a8359e6e55 Mon Sep 17 00:00:00 2001 From: Luca Congiu Date: Fri, 18 Oct 2024 10:07:58 +0200 Subject: [PATCH] Added SigningKey parameter to JWT Token generator (fixes #913) (#914) * Added SigningKey parameter to JWT Token generator fixes #913 * Added BadRequest result if signing key lenght is lower then 32 or empty --------- Co-authored-by: Luca Congiu Co-authored-by: Waldek Mastykarz --- dev-proxy/ApiControllers/ProxyController.cs | 5 ++++ dev-proxy/CommandHandlers/JwtBinder.cs | 8 +++--- dev-proxy/Jwt/JwtCreatorOptions.cs | 6 ++++- dev-proxy/Jwt/JwtOptions.cs | 1 + dev-proxy/Jwt/JwtTokenGenerator.cs | 4 +-- dev-proxy/ProxyHost.cs | 29 ++++++++++++++++++--- 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/dev-proxy/ApiControllers/ProxyController.cs b/dev-proxy/ApiControllers/ProxyController.cs index 41fdd0cc..573b642c 100644 --- a/dev-proxy/ApiControllers/ProxyController.cs +++ b/dev-proxy/ApiControllers/ProxyController.cs @@ -55,6 +55,11 @@ public void StopProxy() [HttpPost("createJwtToken")] public IActionResult CreateJwtToken([FromBody] JwtOptions jwtOptions) { + if (jwtOptions.SigningKey != null && jwtOptions.SigningKey.Length < 32) + { + return BadRequest("The specified signing key is too short. A signing key must be at least 32 characters."); + } + var token = JwtTokenGenerator.CreateToken(jwtOptions); return Ok(new JwtInfo { Token = token }); diff --git a/dev-proxy/CommandHandlers/JwtBinder.cs b/dev-proxy/CommandHandlers/JwtBinder.cs index 0b768484..0de6788f 100644 --- a/dev-proxy/CommandHandlers/JwtBinder.cs +++ b/dev-proxy/CommandHandlers/JwtBinder.cs @@ -6,8 +6,8 @@ using System.CommandLine.Binding; namespace Microsoft.DevProxy.CommandHandlers -{ - public class JwtBinder(Option nameOption, Option> audiencesOption, Option issuerOption, Option> rolesOption, Option> scopesOption, Option> claimsOption, Option validForOption) : BinderBase +{ + public class JwtBinder(Option nameOption, Option> audiencesOption, Option issuerOption, Option> rolesOption, Option> scopesOption, Option> claimsOption, Option validForOption, Option signingKeyOption) : BinderBase { private readonly Option _nameOption = nameOption; private readonly Option> _audiencesOption = audiencesOption; @@ -16,6 +16,7 @@ public class JwtBinder(Option nameOption, Option> au private readonly Option> _scopesOption = scopesOption; private readonly Option> _claimsOption = claimsOption; private readonly Option _validForOption = validForOption; + private readonly Option _signingKeyOption = signingKeyOption; protected override JwtOptions GetBoundValue(BindingContext bindingContext) { @@ -27,7 +28,8 @@ protected override JwtOptions GetBoundValue(BindingContext bindingContext) Roles = bindingContext.ParseResult.GetValueForOption(_rolesOption), Scopes = bindingContext.ParseResult.GetValueForOption(_scopesOption), Claims = bindingContext.ParseResult.GetValueForOption(_claimsOption), - ValidFor = bindingContext.ParseResult.GetValueForOption(_validForOption) + ValidFor = bindingContext.ParseResult.GetValueForOption(_validForOption), + SigningKey = bindingContext.ParseResult.GetValueForOption(_signingKeyOption) }; } } diff --git a/dev-proxy/Jwt/JwtCreatorOptions.cs b/dev-proxy/Jwt/JwtCreatorOptions.cs index 4a7dcec4..2a989361 100644 --- a/dev-proxy/Jwt/JwtCreatorOptions.cs +++ b/dev-proxy/Jwt/JwtCreatorOptions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Security.Cryptography; + namespace Microsoft.DevProxy.Jwt; internal sealed record JwtCreatorOptions @@ -14,6 +16,7 @@ internal sealed record JwtCreatorOptions public required IEnumerable Roles { get; init; } public required IEnumerable Scopes { get; init; } public required Dictionary Claims { get; init; } + public required string SigningKey { get; init; } public static JwtCreatorOptions Create(JwtOptions options) { @@ -27,7 +30,8 @@ public static JwtCreatorOptions Create(JwtOptions options) Scopes = options.Scopes ?? [], Claims = options.Claims ?? [], NotBefore = DateTime.UtcNow, - ExpiresOn = DateTime.UtcNow.AddMinutes(options.ValidFor ?? 60) + ExpiresOn = DateTime.UtcNow.AddMinutes(options.ValidFor ?? 60), + SigningKey = (string.IsNullOrEmpty(options.SigningKey) ? RandomNumberGenerator.GetHexString(32) : options.SigningKey) }; } } \ No newline at end of file diff --git a/dev-proxy/Jwt/JwtOptions.cs b/dev-proxy/Jwt/JwtOptions.cs index b84933f4..0e38d9b8 100644 --- a/dev-proxy/Jwt/JwtOptions.cs +++ b/dev-proxy/Jwt/JwtOptions.cs @@ -12,4 +12,5 @@ public class JwtOptions public IEnumerable? Scopes { get; set; } public Dictionary? Claims { get; set; } public double? ValidFor { get; set; } + public string? SigningKey { get; set; } } diff --git a/dev-proxy/Jwt/JwtTokenGenerator.cs b/dev-proxy/Jwt/JwtTokenGenerator.cs index 1222ed77..41a53e86 100644 --- a/dev-proxy/Jwt/JwtTokenGenerator.cs +++ b/dev-proxy/Jwt/JwtTokenGenerator.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. using System.IdentityModel.Tokens.Jwt; -using System.Security.Cryptography; +using System.Text; namespace Microsoft.DevProxy.Jwt; @@ -14,7 +14,7 @@ internal static string CreateToken(JwtOptions jwtOptions) var jwtIssuer = new JwtIssuer( options.Issuer, - RandomNumberGenerator.GetBytes(32) + Encoding.UTF8.GetBytes(options.SigningKey) ); var jwtToken = jwtIssuer.CreateSecurityToken(options); diff --git a/dev-proxy/ProxyHost.cs b/dev-proxy/ProxyHost.cs index bcdc8c69..389a8600 100755 --- a/dev-proxy/ProxyHost.cs +++ b/dev-proxy/ProxyHost.cs @@ -382,14 +382,15 @@ public RootCommand GetRootCommand(ILogger logger) var jwtClaimsOption = new Option>("--claims", description: "Claims to add to the token. Specify once for each claim in the format \"name:value\".", - parseArgument: result => { + parseArgument: result => + { var claims = new Dictionary(); foreach (var token in result.Tokens) { var claim = token.Value.Split(":"); if (claim.Length != 2) - { + { result.ErrorMessage = $"Invalid claim format: '{token.Value}'. Expected format is name:value."; return claims ?? []; } @@ -416,7 +417,26 @@ public RootCommand GetRootCommand(ILogger logger) jwtValidForOption.AddAlias("-v"); jwtCreateCommand.AddOption(jwtValidForOption); - jwtCreateCommand.SetHandler( + var jwtSigningKeyOption = new Option("--signing-key", "The signing key to sign the token. Minimum length is 32 characters."); + jwtSigningKeyOption.AddAlias("-k"); + jwtSigningKeyOption.AddValidator(input => + { + try + { + var value = input.GetValueForOption(jwtSigningKeyOption); + if (string.IsNullOrWhiteSpace(value) || value.Length < 32) + { + input.ErrorMessage = $"Requires option '--{jwtSigningKeyOption.Name}' to be at least 32 characters"; + } + } + catch (InvalidOperationException ex) + { + input.ErrorMessage = ex.Message; + } + }); + jwtCreateCommand.AddOption(jwtSigningKeyOption); + + jwtCreateCommand.SetHandler( JwtCommandHandler.GetToken, new JwtBinder( jwtNameOption, @@ -425,7 +445,8 @@ public RootCommand GetRootCommand(ILogger logger) jwtRolesOption, jwtScopesOption, jwtClaimsOption, - jwtValidForOption + jwtValidForOption, + jwtSigningKeyOption ) ); jwtCommand.Add(jwtCreateCommand);