-
Notifications
You must be signed in to change notification settings - Fork 76
Add jwt create command and createJwtToken endpoint. Closes #882 #891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
waldekmastykarz
merged 11 commits into
dotnet:main
from
garrytrinder:882-create-jwt-token-command
Sep 25, 2024
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
43bfcf6
Add jwt create command. Closes #882
garrytrinder 0183ad9
Address PR review comments
garrytrinder 70683c4
Update claims parsing and validation
garrytrinder 4986cbe
Update claims parsing for handling duplicate keys
garrytrinder 19a1be4
Update token generation to ignore registered claims passed in claims …
garrytrinder 8ab1039
Fix audience default value
garrytrinder 36d299e
Add scp and roles to ignored claims
garrytrinder 8d43284
Refactor claim parsing to use tuple
garrytrinder 0c7fab4
Refactor options, add custom binding, add API endpoint, centralise de…
garrytrinder aa482dc
Refactor ExpiresOn default
garrytrinder a8b3d4f
Update option descriptions
garrytrinder File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
namespace Microsoft.DevProxy.ApiControllers; | ||
|
||
public class JwtOptions | ||
{ | ||
public string? Name { get; set; } | ||
public IEnumerable<string>? Audiences { get; set; } | ||
public string? Issuer { get; set; } | ||
public IEnumerable<string>? Roles { get; set; } | ||
public IEnumerable<string>? Scopes { get; set; } | ||
public Dictionary<string, string>? Claims { get; set; } | ||
public double ValidFor { get; set; } | ||
} | ||
|
||
public class JwtInfo | ||
{ | ||
public required string Token { get; set; } | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
using Microsoft.DevProxy.ApiControllers; | ||
using System.CommandLine; | ||
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> | ||
{ | ||
private readonly Option<string> _nameOption = nameOption; | ||
private readonly Option<IEnumerable<string>> _audiencesOption = audiencesOption; | ||
private readonly Option<string> _issuerOption = issuerOption; | ||
private readonly Option<IEnumerable<string>> _rolesOption = rolesOption; | ||
private readonly Option<IEnumerable<string>> _scopesOption = scopesOption; | ||
private readonly Option<Dictionary<string, string>> _claimsOption = claimsOption; | ||
private readonly Option<double> _validForOption = validForOption; | ||
|
||
protected override JwtOptions GetBoundValue(BindingContext bindingContext) | ||
{ | ||
return new JwtOptions | ||
{ | ||
Name = bindingContext.ParseResult.GetValueForOption(_nameOption), | ||
Audiences = bindingContext.ParseResult.GetValueForOption(_audiencesOption), | ||
Issuer = bindingContext.ParseResult.GetValueForOption(_issuerOption), | ||
Roles = bindingContext.ParseResult.GetValueForOption(_rolesOption), | ||
Scopes = bindingContext.ParseResult.GetValueForOption(_scopesOption), | ||
Claims = bindingContext.ParseResult.GetValueForOption(_claimsOption), | ||
ValidFor = bindingContext.ParseResult.GetValueForOption(_validForOption) | ||
}; | ||
} | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
|
||
using Microsoft.DevProxy.ApiControllers; | ||
using Microsoft.IdentityModel.Tokens; | ||
using System.Globalization; | ||
using System.IdentityModel.Tokens.Jwt; | ||
using System.Security.Claims; | ||
using System.Security.Cryptography; | ||
using System.Security.Principal; | ||
|
||
namespace Microsoft.DevProxy.CommandHandlers; | ||
|
||
internal static class JwtCommandHandler | ||
{ | ||
internal static void GetToken(JwtOptions jwtOptions) | ||
{ | ||
var token = JwtTokenGenerator.CreateToken(jwtOptions); | ||
|
||
Console.WriteLine(token); | ||
} | ||
} | ||
|
||
internal static class JwtTokenGenerator | ||
{ | ||
internal static string CreateToken(JwtOptions jwtOptions) | ||
{ | ||
var options = JwtCreatorOptions.Create(jwtOptions); | ||
|
||
var jwtIssuer = new JwtIssuer( | ||
options.Issuer, | ||
RandomNumberGenerator.GetBytes(32) | ||
); | ||
|
||
var jwtToken = jwtIssuer.Create(options); | ||
|
||
var jwt = Jwt.Create( | ||
options.Scheme, | ||
jwtToken, | ||
new JwtSecurityTokenHandler().WriteToken(jwtToken), | ||
options.Scopes, | ||
options.Roles, | ||
garrytrinder marked this conversation as resolved.
Show resolved
Hide resolved
|
||
options.Claims | ||
); | ||
|
||
return jwt.Token; | ||
} | ||
} | ||
|
||
internal sealed class JwtIssuer(string issuer, byte[] signingKeyMaterial) | ||
{ | ||
private readonly SymmetricSecurityKey _signingKey = new(signingKeyMaterial); | ||
|
||
public string Issuer { get; } = issuer; | ||
|
||
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("scp", s))); | ||
} | ||
|
||
if (options.Roles is { } rolesToAdd) | ||
{ | ||
identity.AddClaims(rolesToAdd.Select(r => new Claim("roles", r))); | ||
} | ||
|
||
if (options.Claims is { Count: > 0 } claimsToAdd) | ||
{ | ||
// filter out registered claims | ||
// https://www.rfc-editor.org/rfc/rfc7519#section-4.1 | ||
claimsToAdd.Remove(JwtRegisteredClaimNames.Iss); | ||
claimsToAdd.Remove(JwtRegisteredClaimNames.Sub); | ||
claimsToAdd.Remove(JwtRegisteredClaimNames.Aud); | ||
claimsToAdd.Remove(JwtRegisteredClaimNames.Exp); | ||
claimsToAdd.Remove(JwtRegisteredClaimNames.Nbf); | ||
claimsToAdd.Remove(JwtRegisteredClaimNames.Iat); | ||
claimsToAdd.Remove(JwtRegisteredClaimNames.Jti); | ||
claimsToAdd.Remove("scp"); | ||
claimsToAdd.Remove("roles"); | ||
|
||
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.ToList() 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; | ||
} | ||
} | ||
|
||
internal record Jwt(string Id, string Scheme, string Name, string Audience, DateTimeOffset NotBefore, DateTimeOffset Expires, DateTimeOffset Issued, string Token) | ||
{ | ||
public IEnumerable<string> Scopes { get; set; } = []; | ||
|
||
public IEnumerable<string> Roles { get; set; } = []; | ||
|
||
public IDictionary<string, string> CustomClaims { get; set; } = new Dictionary<string, string>(); | ||
|
||
public static Jwt Create( | ||
string scheme, | ||
JwtSecurityToken token, | ||
string encodedToken, | ||
IEnumerable<string> scopes, | ||
IEnumerable<string> roles, | ||
IDictionary<string, string> customClaims) | ||
{ | ||
return new Jwt(token.Id, scheme, token.Subject, string.Join(", ", token.Audiences), token.ValidFrom, token.ValidTo, token.IssuedAt, encodedToken) | ||
{ | ||
Scopes = scopes, | ||
Roles = roles, | ||
CustomClaims = customClaims | ||
}; | ||
} | ||
} | ||
|
||
internal sealed record JwtCreatorOptions | ||
{ | ||
public required string Scheme { get; init; } | ||
public required string Name { get; init; } | ||
public required IEnumerable<string> Audiences { get; init; } | ||
public required string Issuer { get; init; } | ||
public DateTime NotBefore { get; init; } | ||
public DateTime ExpiresOn { get; init; } | ||
public required IEnumerable<string> Roles { get; init; } | ||
public required IEnumerable<string> Scopes { get; init; } | ||
public required Dictionary<string, string> Claims { get; init; } | ||
|
||
public static JwtCreatorOptions Create(JwtOptions options) | ||
{ | ||
return new JwtCreatorOptions | ||
{ | ||
Scheme = "Bearer", | ||
Name = options.Name ?? "Dev Proxy", | ||
Audiences = options.Audiences ?? ["https://myserver.com"], | ||
Issuer = options.Issuer ?? "dev-proxy", | ||
Roles = options.Roles ?? [], | ||
Scopes = options.Scopes ?? [], | ||
Claims = options.Claims ?? [], | ||
NotBefore = DateTime.UtcNow, | ||
ExpiresOn = DateTime.UtcNow.AddMinutes(options.ValidFor == 0 ? 60 : options.ValidFor) | ||
}; | ||
} | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.