Skip to content

Fix up user-jwts interactions #42125

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
merged 7 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Globalization;
using System.Linq;
using System.Text;
using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Tools.Internal;

Expand All @@ -11,7 +12,7 @@ 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" };
"yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy/MM/dd", "yyyy/MM/dd HH:mm", "yyyy-MM-ddTHH:mm:ss.fffffffzzz" };
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",
Expand All @@ -32,7 +33,7 @@ public static void Register(ProjectCommandLineApplication app)
);

var nameOption = cmd.Option(
"--name",
"-n|--name",
Resources.CreateCommand_NameOption_Description,
CommandOptionType.SingleValue);

Expand Down Expand Up @@ -80,20 +81,20 @@ public static void Register(ProjectCommandLineApplication app)

cmd.OnExecute(() =>
{
var (options, isValid) = ValidateArguments(
var (options, isValid, optionsString) = 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);
return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString);
});
});
}

private static (JwtCreatorOptions, bool) ValidateArguments(
private static (JwtCreatorOptions, bool, string) ValidateArguments(
IReporter reporter,
CommandOption projectOption,
CommandOption schemeNameOption,
Expand All @@ -109,16 +110,22 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
{
var isValid = true;
var project = DevJwtCliHelpers.GetProject(projectOption.Value());

var scheme = schemeNameOption.HasValue() ? schemeNameOption.Value() : "Bearer";
var optionsString = schemeNameOption.HasValue() ? $"{Resources.JwtPrint_Scheme}: {scheme}{Environment.NewLine}" : string.Empty;

var name = nameOption.HasValue() ? nameOption.Value() : Environment.UserName;
optionsString += $"{Resources.JwtPrint_Name}: {name}{Environment.NewLine}";

var audience = audienceOption.HasValue() ? audienceOption.Values : DevJwtCliHelpers.GetAudienceCandidatesFromLaunchSettings(project).ToList();
optionsString += audienceOption.HasValue() ? $"{Resources.JwtPrint_Audiences}: {audience}{Environment.NewLine}" : string.Empty;
if (audience is null)
{
reporter.Error(Resources.CreateCommand_NoAudience_Error);
isValid = false;
}
var issuer = issuerOption.HasValue() ? issuerOption.Value() : DevJwtsDefaults.Issuer;
optionsString += issuerOption.HasValue() ? $"{Resources.JwtPrint_Issuer}: {issuer}{Environment.NewLine}" : string.Empty;

var notBefore = DateTime.UtcNow;
if (notBeforeOption.HasValue())
Expand All @@ -128,6 +135,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--not-before"));
isValid = false;
}
optionsString += $"{Resources.JwtPrint_NotBefore}: {notBefore:O}{Environment.NewLine}";
}

var expiresOn = notBefore.AddMonths(3);
Expand All @@ -138,6 +146,7 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--expires-on"));
isValid = false;
}
optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}";
}

if (validForOption.HasValue())
Expand All @@ -147,10 +156,14 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
reporter.Error(Resources.FormatCreateCommand_InvalidPeriod_Error("--valid-for"));
}
expiresOn = notBefore.Add(validForValue);
optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

expiresOnOption and validForOption conflict, is there a warning/error for this? Should we only print one of them?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call! I think we probably want to throw an error and treat the input arguments as invalid in this case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I think that's what I did in the original prototype, you have to specify either/or.

}

var roles = rolesOption.HasValue() ? rolesOption.Values : new List<string>();
optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: [{string.Join(',', roles)}]{Environment.NewLine}" : string.Empty;

var scopes = scopesOption.HasValue() ? scopesOption.Values : new List<string>();
optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(',', scopes)}{Environment.NewLine}" : string.Empty;

var claims = new Dictionary<string, string>();
if (claimsOption.HasValue())
Expand All @@ -160,9 +173,13 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
reporter.Error(Resources.CreateCommand_InvalidClaims_Error);
isValid = false;
}
optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(',', claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]{Environment.NewLine}";
}

return (new JwtCreatorOptions(scheme, name, audience, issuer, notBefore, expiresOn, roles, scopes, claims), isValid);
return (
new JwtCreatorOptions(scheme, name, audience, issuer, notBefore, expiresOn, roles, scopes, claims),
isValid,
optionsString);

static bool ParseDate(string datetime, out DateTime parsedDateTime) =>
DateTime.TryParseExact(datetime, _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsedDateTime);
Expand All @@ -171,7 +188,8 @@ static bool ParseDate(string datetime, out DateTime parsedDateTime) =>
private static int Execute(
IReporter reporter,
string projectPath,
JwtCreatorOptions options)
JwtCreatorOptions options,
string optionsString)
{
if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId))
{
Expand All @@ -196,6 +214,8 @@ private static int Execute(
settingsToWrite.Save(appsettingsFilePath);

reporter.Output(Resources.FormatCreateCommand_Confirmed(jwtToken.Id));
reporter.Output(optionsString);
reporter.Output($"{Resources.JwtPrint_Token}: {jwt.Token}");

return 0;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ private static int Execute(IReporter reporter, string projectPath, bool showToke
if (jwtStore.Jwts is { Count: > 0 } jwts)
{
var table = new ConsoleTable(reporter);
table.AddColumns("Id", "Scheme Name", "Audience", "Issued", "Expires");
table.AddColumns(Resources.JwtPrint_Id, Resources.JwtPrint_Scheme, Resources.JwtPrint_Name, Resources.JwtPrint_IssuedOn, Resources.JwtPrint_ExpiresOn);

if (showTokens)
{
table.AddColumns("Encoded Token");
table.AddColumns(Resources.JwtPrint_Token);
}

foreach (var jwtRow in jwts)
Expand Down
23 changes: 9 additions & 14 deletions src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ public static void Register(ProjectCommandLineApplication app)
cmd.Description = Resources.PrintCommand_Description;

var idArgument = cmd.Argument("[id]", Resources.PrintCommand_IdArgument_Description);

var showFullOption = cmd.Option(
"--show-full",
Resources.PrintCommand_ShowFullOption_Description,
CommandOptionType.NoValue);
var showAllOption = cmd.Option("--show-all", Resources.PrintCommand_ShowAllOption_Description, CommandOptionType.NoValue);

cmd.HelpOption("-h|--help");

Expand All @@ -30,12 +26,16 @@ public static void Register(ProjectCommandLineApplication app)
cmd.ShowHelp();
return 0;
}
return Execute(cmd.Reporter, cmd.ProjectOption.Value(), idArgument.Value, showFullOption.HasValue());
return Execute(
cmd.Reporter,
cmd.ProjectOption.Value(),
idArgument.Value,
showAllOption.HasValue());
});
});
}

private static int Execute(IReporter reporter, string projectPath, string id, bool showFull)
private static int Execute(IReporter reporter, string projectPath, string id, bool showAll)
{
if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var _, out var userSecretsId))
{
Expand All @@ -50,13 +50,8 @@ private static int Execute(IReporter reporter, string projectPath, string id, bo
}

reporter.Output(Resources.FormatPrintCommand_Confirmed(id));
JwtSecurityToken fullToken;

if (showFull)
{
fullToken = JwtIssuer.Extract(jwt.Token);
DevJwtCliHelpers.PrintJwt(reporter, jwt, fullToken);
}
JwtSecurityToken fullToken = JwtIssuer.Extract(jwt.Token);
DevJwtCliHelpers.PrintJwt(reporter, jwt, showAll, fullToken);

return 0;
}
Expand Down
45 changes: 39 additions & 6 deletions src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Tools.Internal;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;

Expand Down Expand Up @@ -145,16 +146,48 @@ public static string[] GetAudienceCandidatesFromLaunchSettings(string project)
return null;
}

public static void PrintJwt(IReporter reporter, Jwt jwt, JwtSecurityToken fullToken = null)
public static void PrintJwt(IReporter reporter, Jwt jwt, bool showAll, JwtSecurityToken fullToken = null)
{
reporter.Output(JsonSerializer.Serialize(jwt, new JsonSerializerOptions { WriteIndented = true }));
reporter.Output($"{Resources.JwtPrint_Id}: {jwt.Id}");
reporter.Output($"{Resources.JwtPrint_Name}: {jwt.Name}");
reporter.Output($"{Resources.JwtPrint_Scheme}: {jwt.Scheme}");
reporter.Output($"{Resources.JwtPrint_Audiences}: {jwt.Audience}");
reporter.Output($"{Resources.JwtPrint_NotBefore}: {jwt.NotBefore:O}");
reporter.Output($"{Resources.JwtPrint_ExpiresOn}: {jwt.Expires:O}");
reporter.Output($"{Resources.JwtPrint_IssuedOn}: {jwt.Issued:O}");

if (!jwt.Scopes.IsNullOrEmpty() || showAll)
{
var scopesValue = jwt.Scopes.IsNullOrEmpty()
? "none"
: string.Join(',', jwt.Scopes);
reporter.Output($"{Resources.JwtPrint_Scopes}: {scopesValue}");
}

if (!jwt.Roles.IsNullOrEmpty() || showAll)
{
var rolesValue = jwt.Roles.IsNullOrEmpty()
? "none"
: String.Join(',', jwt.Roles);
reporter.Output($"{Resources.JwtPrint_Roles}: [{rolesValue}]");
}

if (fullToken is not null)
if (!jwt.CustomClaims.IsNullOrEmpty() || showAll)
{
reporter.Output($"Token Header: {fullToken.Header.SerializeToJson()}");
reporter.Output($"Token Payload: {fullToken.Payload.SerializeToJson()}");
var customClaimsValue = jwt.CustomClaims.IsNullOrEmpty()
? "none"
: string.Join(',', jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}"));
reporter.Output($"{Resources.JwtPrint_CustomClaims}: [{customClaimsValue}]");
}
reporter.Output($"Compact Token: {jwt.Token}");

if (showAll)
{
reporter.Output($"{Resources.JwtPrint_TokenHeader}: {fullToken.Header.SerializeToJson()}");
reporter.Output($"{Resources.JwtPrint_TokenPayload}: {fullToken.Payload.SerializeToJson()}");
}

var tokenValueFieldName = showAll ? Resources.JwtPrint_CompactToken : Resources.JwtPrint_Token;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this backward?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intentional. When the user provides the showAll flag, we'll print the full token with the header and payload, when that happens we're printing "Compact" token to make it clear that the value is the encoded token. Same doesn't need to be done if we never print the full token.

reporter.Output($"{tokenValueFieldName}: {jwt.Token}");
}

public static bool TryParseClaims(List<string> input, out Dictionary<string, string> claims)
Expand Down
2 changes: 1 addition & 1 deletion src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public static Jwt Create(
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)
return new Jwt(token.Id, scheme, token.Subject, string.Join(", ", token.Audiences), token.ValidFrom, token.ValidTo, token.IssuedAt, encodedToken)
{
Scopes = scopes,
Roles = roles,
Expand Down
5 changes: 5 additions & 0 deletions src/Tools/dotnet-user-jwts/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public void Run(string[] args)
{
userJwts.Execute(args);
}
catch (CommandParsingException parsingException)
{
_reporter.Error(parsingException.Message);
userJwts.ShowHelp();
}
catch (Exception ex)
{
_reporter.Error(ex.Message);
Expand Down
49 changes: 47 additions & 2 deletions src/Tools/dotnet-user-jwts/src/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,51 @@
<data name="CreateCommand_ValidForOption_Description" xml:space="preserve">
<value>The period the JWT should expire after. Specify using a number followed by a duration 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.</value>
</data>
<data name="JwtPrint_Audiences" xml:space="preserve">
<value>Audience(s)</value>
</data>
<data name="JwtPrint_CompactToken" xml:space="preserve">
<value>Compact Token</value>
</data>
<data name="JwtPrint_CustomClaims" xml:space="preserve">
<value>Custom Claims</value>
</data>
<data name="JwtPrint_ExpiresOn" xml:space="preserve">
<value>Expires On</value>
</data>
<data name="JwtPrint_Id" xml:space="preserve">
<value>ID</value>
</data>
<data name="JwtPrint_IssuedOn" xml:space="preserve">
<value>Issued On</value>
</data>
<data name="JwtPrint_Issuer" xml:space="preserve">
<value>Issuer</value>
</data>
<data name="JwtPrint_Name" xml:space="preserve">
<value>Name</value>
</data>
<data name="JwtPrint_NotBefore" xml:space="preserve">
<value>Not Before</value>
</data>
<data name="JwtPrint_Roles" xml:space="preserve">
<value>Roles</value>
</data>
<data name="JwtPrint_Scheme" xml:space="preserve">
<value>Scheme</value>
</data>
<data name="JwtPrint_Scopes" xml:space="preserve">
<value>Scopes</value>
</data>
<data name="JwtPrint_Token" xml:space="preserve">
<value>Token</value>
</data>
<data name="JwtPrint_TokenHeader" xml:space="preserve">
<value>Token Header</value>
</data>
<data name="JwtPrint_TokenPayload" xml:space="preserve">
<value>Token Payload</value>
</data>
<data name="KeyCommand_Canceled" xml:space="preserve">
<value>Key reset canceled.</value>
</data>
Expand Down Expand Up @@ -234,8 +279,8 @@
<data name="PrintCommand_NoJwtFound" xml:space="preserve">
<value>No token with ID '{0}' found.</value>
</data>
<data name="PrintCommand_ShowFullOption_Description" xml:space="preserve">
<value>Whether to show the full JWT contents in addition to the compact serialized format.</value>
<data name="PrintCommand_ShowAllOption_Description" xml:space="preserve">
<value>Whether to show all details associated with the JWT.</value>
</data>
<data name="ProjectOption_Description" xml:space="preserve">
<value>The path of the project to operate on. Defaults to the project in the current directory.</value>
Expand Down
Loading