Skip to content

Commit

Permalink
Added SigningKey parameter to JWT Token generator (fixes #913) (#914)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
Co-authored-by: Waldek Mastykarz <[email protected]>
  • Loading branch information
3 people authored Oct 18, 2024
1 parent 181aaac commit 3f7ec21
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 10 deletions.
5 changes: 5 additions & 0 deletions dev-proxy/ApiControllers/ProxyController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
8 changes: 5 additions & 3 deletions dev-proxy/CommandHandlers/JwtBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
using System.CommandLine.Binding;

namespace Microsoft.DevProxy.CommandHandlers
{
public class JwtBinder(Option<string> nameOption, Option<IEnumerable<string>> audiencesOption, Option<string> issuerOption, Option<IEnumerable<string>> rolesOption, Option<IEnumerable<string>> scopesOption, Option<Dictionary<string, string>> claimsOption, Option<double> validForOption) : BinderBase<JwtOptions>
{
public class JwtBinder(Option<string> nameOption, Option<IEnumerable<string>> audiencesOption, Option<string> issuerOption, Option<IEnumerable<string>> rolesOption, Option<IEnumerable<string>> scopesOption, Option<Dictionary<string, string>> claimsOption, Option<double> validForOption, Option<string> signingKeyOption) : BinderBase<JwtOptions>
{
private readonly Option<string> _nameOption = nameOption;
private readonly Option<IEnumerable<string>> _audiencesOption = audiencesOption;
Expand All @@ -16,6 +16,7 @@ public class JwtBinder(Option<string> nameOption, Option<IEnumerable<string>> au
private readonly Option<IEnumerable<string>> _scopesOption = scopesOption;
private readonly Option<Dictionary<string, string>> _claimsOption = claimsOption;
private readonly Option<double> _validForOption = validForOption;
private readonly Option<string> _signingKeyOption = signingKeyOption;

protected override JwtOptions GetBoundValue(BindingContext bindingContext)
{
Expand All @@ -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)
};
}
}
Expand Down
6 changes: 5 additions & 1 deletion dev-proxy/Jwt/JwtCreatorOptions.cs
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,6 +16,7 @@ internal sealed record JwtCreatorOptions
public required IEnumerable<string> Roles { get; init; }
public required IEnumerable<string> Scopes { get; init; }
public required Dictionary<string, string> Claims { get; init; }
public required string SigningKey { get; init; }

public static JwtCreatorOptions Create(JwtOptions options)
{
Expand All @@ -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)
};
}
}
1 change: 1 addition & 0 deletions dev-proxy/Jwt/JwtOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ public class JwtOptions
public IEnumerable<string>? Scopes { get; set; }
public Dictionary<string, string>? Claims { get; set; }
public double? ValidFor { get; set; }
public string? SigningKey { get; set; }
}
4 changes: 2 additions & 2 deletions dev-proxy/Jwt/JwtTokenGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);
Expand Down
29 changes: 25 additions & 4 deletions dev-proxy/ProxyHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -382,14 +382,15 @@ public RootCommand GetRootCommand(ILogger logger)

var jwtClaimsOption = new Option<Dictionary<string, string>>("--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<string, string>();
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 ?? [];
}
Expand All @@ -416,7 +417,26 @@ public RootCommand GetRootCommand(ILogger logger)
jwtValidForOption.AddAlias("-v");
jwtCreateCommand.AddOption(jwtValidForOption);

jwtCreateCommand.SetHandler(
var jwtSigningKeyOption = new Option<string>("--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,
Expand All @@ -425,7 +445,8 @@ public RootCommand GetRootCommand(ILogger logger)
jwtRolesOption,
jwtScopesOption,
jwtClaimsOption,
jwtValidForOption
jwtValidForOption,
jwtSigningKeyOption
)
);
jwtCommand.Add(jwtCreateCommand);
Expand Down

0 comments on commit 3f7ec21

Please sign in to comment.