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 all 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
53 changes: 46 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,17 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--expires-on"));
isValid = false;
}

if (validForOption.HasValue())
{
reporter.Error(Resources.CreateCommand_InvalidExpiresOn_Error);
isValid = false;
}
else
{
optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}";
}

}

if (validForOption.HasValue())
Expand All @@ -147,10 +166,23 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
reporter.Error(Resources.FormatCreateCommand_InvalidPeriod_Error("--valid-for"));
}
expiresOn = notBefore.Add(validForValue);

if (expiresOnOption.HasValue())
{
reporter.Error(Resources.CreateCommand_InvalidExpiresOn_Error);
isValid = false;
}
else
{
optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}";
}
}

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 +192,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 +207,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 +233,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_Audiences, Resources.JwtPrint_IssuedOn, Resources.JwtPrint_ExpiresOn);
Copy link
Member

Choose a reason for hiding this comment

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

Idk if you saw my question, should audience and name both be here? I think the issue mentioned including both.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, so the issue refers to showing the name in the print command. For list, I figured it would make sense to limit it to the properties that impact the JWT's behavior at runtime. Also, I wanted to be a little cautious about having too many columns in the table since our ConsoleTable implementation is pretty rudimentary.


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
Loading