From 060ba2e78abf0a787daf539613c4781c227de954 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 9 Jun 2022 08:46:28 -0700 Subject: [PATCH 1/7] Fix up user-jwts interactions --- .../src/Commands/CreateCommand.cs | 34 +++-- .../src/Commands/ListCommand.cs | 4 +- .../src/Commands/PrintCommand.cs | 23 ++-- .../src/Helpers/DevJwtCliHelpers.cs | 45 ++++++- src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs | 2 +- src/Tools/dotnet-user-jwts/src/Program.cs | 5 + src/Tools/dotnet-user-jwts/src/Resources.resx | 49 +++++++- .../dotnet-user-jwts/test/UserJwtsTests.cs | 117 ++++++++++++++++++ 8 files changed, 247 insertions(+), 32 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index a9eb9253852f..d68f73d069c0 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Linq; +using System.Text; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Tools.Internal; @@ -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", @@ -32,7 +33,7 @@ public static void Register(ProjectCommandLineApplication app) ); var nameOption = cmd.Option( - "--name", + "--name|-n", Resources.CreateCommand_NameOption_Description, CommandOptionType.SingleValue); @@ -80,7 +81,7 @@ 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) @@ -88,12 +89,12 @@ public static void Register(ProjectCommandLineApplication app) 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, @@ -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()) @@ -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); @@ -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()) @@ -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}"; } var roles = rolesOption.HasValue() ? rolesOption.Values : new List(); + optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: {string.Join(',', roles)} {Environment.NewLine}" : string.Empty; + var scopes = scopesOption.HasValue() ? scopesOption.Values : new List(); + optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(',', scopes)} {Environment.NewLine}" : string.Empty; var claims = new Dictionary(); if (claimsOption.HasValue()) @@ -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); @@ -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)) { @@ -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; } diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs index f74c67a3f4b4..dedd523de3e6 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs @@ -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) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs index 2842c13ef832..97873ad818df 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs @@ -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"); @@ -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)) { @@ -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; } diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index 155c581cbf6a..2d06a08dab63 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -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; @@ -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; + reporter.Output($"{tokenValueFieldName}: {jwt.Token}"); } public static bool TryParseClaims(List input, out Dictionary claims) diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs b/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs index e78112fa6cda..f465c9a95594 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs @@ -24,7 +24,7 @@ public static Jwt Create( IEnumerable roles = null, IDictionary 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, diff --git a/src/Tools/dotnet-user-jwts/src/Program.cs b/src/Tools/dotnet-user-jwts/src/Program.cs index 8967727ac24e..96ce31449cef 100644 --- a/src/Tools/dotnet-user-jwts/src/Program.cs +++ b/src/Tools/dotnet-user-jwts/src/Program.cs @@ -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); diff --git a/src/Tools/dotnet-user-jwts/src/Resources.resx b/src/Tools/dotnet-user-jwts/src/Resources.resx index b8b95ba88d07..4df05f6ca8da 100644 --- a/src/Tools/dotnet-user-jwts/src/Resources.resx +++ b/src/Tools/dotnet-user-jwts/src/Resources.resx @@ -183,6 +183,51 @@ 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. + + Audience(s) + + + Compact Token + + + Custom Claims + + + Expires On + + + ID + + + Issued On + + + Issuer + + + Name + + + Not Before + + + Roles + + + Scheme + + + Scopes + + + Token + + + Token Header + + + Token Payload + Key reset canceled. @@ -234,8 +279,8 @@ No token with ID '{0}' found. - - Whether to show the full JWT contents in addition to the compact serialized format. + + Whether to show all details associated with the JWT. The path of the project to operate on. Defaults to the project in the current directory. diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs index e0db638ae63d..c7f502adce59 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -147,4 +147,121 @@ public void Key_CanResetSigningKey() app.Run(new[] { "key", "--reset", "--force", "--project", project }); Assert.Contains("New signing key created:", _console.GetOutput()); } + + [Fact] + public void Command_ShowsHelpForInvalidCommand() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + var exception = Record.Exception(() => app.Run(new[] { "not-real", "--project", project })); + + Assert.Null(exception); + Assert.Contains("Unrecognized command or argument 'not-real'", _console.GetOutput()); + } + + [Fact] + public void CreateCommand_ShowsBasicTokenDetails() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + var output = _console.GetOutput(); + + Assert.Contains($"Name: {Environment.UserName}", output); + Assert.Contains("Token: ", output); + Assert.DoesNotContain("Scheme", output); + } + + [Fact] + public void CreateCommand_SupportsODateTimeFormats() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--expires-on", DateTime.Now.AddDays(2).ToString("O") }); + var output = _console.GetOutput(); + + Assert.Contains($"Name: {Environment.UserName}", output); + Assert.Contains("Token: ", output); + Assert.Contains("Expires On", output); + Assert.DoesNotContain("Scheme", output); + } + + [Fact] + public void CreateCommand_ShowsCustomizedTokenDetails() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--scheme", "customScheme" }); + var output = _console.GetOutput(); + + Assert.Contains($"Name: {Environment.UserName}", output); + Assert.Contains("Token: ", output); + Assert.Contains("Scheme: customScheme", output); + } + + [Fact] + public void PrintCommand_ShowsBasicOptions() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project }); + var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'"); + var id = matches.SingleOrDefault().Groups[1].Value; + + app.Run(new[] { "print", id, "--project", project }); + var output = _console.GetOutput(); + + Assert.Contains($"ID: {id}", output); + Assert.Contains($"Name: {Environment.UserName}", output); + Assert.Contains($"Scheme: Bearer", output); + Assert.Contains($"Audience(s): https://localhost:5001, http://localhost:5000", output); + } + + [Fact] + public void PrintCommand_ShowsCustomizedOptions() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--role", "foobar" }); + var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'"); + var id = matches.SingleOrDefault().Groups[1].Value; + + app.Run(new[] { "print", id, "--project", project }); + var output = _console.GetOutput(); + + Assert.Contains($"ID: {id}", output); + Assert.Contains($"Name: {Environment.UserName}", output); + Assert.Contains($"Scheme: Bearer", output); + Assert.Contains($"Audience(s): https://localhost:5001, http://localhost:5000", output); + Assert.Contains($"Roles: [foobar]", output); + Assert.DoesNotContain("Custom Claims", output); + } + + [Fact] + public void PrintComamnd_ShowsAllOptionsWithShowAll() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--claim", "foo=bar" }); + var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'"); + var id = matches.SingleOrDefault().Groups[1].Value; + + app.Run(new[] { "print", id, "--project", project, "--show-all" }); + var output = _console.GetOutput(); + + Assert.Contains($"ID: {id}", output); + Assert.Contains($"Name: {Environment.UserName}", output); + Assert.Contains($"Scheme: Bearer", output); + Assert.Contains($"Audience(s): https://localhost:5001, http://localhost:5000", output); + Assert.Contains($"Scopes: none", output); + Assert.Contains($"Roles: [none]", output); + Assert.Contains($"Custom Claims: [foo=bar]", output); + } } From d7757433b7f12fa65131977e4aed4a1d113f0e7d Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Fri, 10 Jun 2022 08:26:52 -0700 Subject: [PATCH 2/7] Address feedback --- src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index d68f73d069c0..f321f72235e3 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -160,7 +160,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( } var roles = rolesOption.HasValue() ? rolesOption.Values : new List(); - optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: {string.Join(',', roles)} {Environment.NewLine}" : string.Empty; + optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: [{string.Join(',', roles)}] {Environment.NewLine}" : string.Empty; var scopes = scopesOption.HasValue() ? scopesOption.Values : new List(); optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(',', scopes)} {Environment.NewLine}" : string.Empty; @@ -173,7 +173,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( reporter.Error(Resources.CreateCommand_InvalidClaims_Error); isValid = false; } - optionsString += $"{Resources.JwtPrint_CustomClaims}: {string.Join(',', claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))} {Environment.NewLine}"; + optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(',', claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}] {Environment.NewLine}"; } return ( From 16d55b6990484d18788583f07bf661d06fe0ebd7 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 15 Jun 2022 11:44:14 -0700 Subject: [PATCH 3/7] Apply suggestions from code review Co-authored-by: Damian Edwards --- src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index f321f72235e3..004d550b243a 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -33,7 +33,7 @@ public static void Register(ProjectCommandLineApplication app) ); var nameOption = cmd.Option( - "--name|-n", + "-n|--name", Resources.CreateCommand_NameOption_Description, CommandOptionType.SingleValue); @@ -112,7 +112,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( 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 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}"; From 9108364576b7a2ad39e6596f8d79dfa563f86b9b Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 15 Jun 2022 13:00:37 -0700 Subject: [PATCH 4/7] Apply suggestions from code review Co-authored-by: Damian Edwards --- .../dotnet-user-jwts/src/Commands/CreateCommand.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index 004d550b243a..ec8bb3700a55 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -125,7 +125,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( isValid = false; } var issuer = issuerOption.HasValue() ? issuerOption.Value() : DevJwtsDefaults.Issuer; - optionsString += issuerOption.HasValue() ? $"{Resources.JwtPrint_Issuer}: {issuer} {Environment.NewLine}" : string.Empty; + optionsString += issuerOption.HasValue() ? $"{Resources.JwtPrint_Issuer}: {issuer}{Environment.NewLine}" : string.Empty; var notBefore = DateTime.UtcNow; if (notBeforeOption.HasValue()) @@ -135,7 +135,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--not-before")); isValid = false; } - optionsString += $"{Resources.JwtPrint_NotBefore}: {notBefore:O} {Environment.NewLine}"; + optionsString += $"{Resources.JwtPrint_NotBefore}: {notBefore:O}{Environment.NewLine}"; } var expiresOn = notBefore.AddMonths(3); @@ -146,7 +146,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--expires-on")); isValid = false; } - optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O} {Environment.NewLine}"; + optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}"; } if (validForOption.HasValue()) @@ -156,14 +156,14 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( reporter.Error(Resources.FormatCreateCommand_InvalidPeriod_Error("--valid-for")); } expiresOn = notBefore.Add(validForValue); - optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O} {Environment.NewLine}"; + optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}"; } var roles = rolesOption.HasValue() ? rolesOption.Values : new List(); - optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: [{string.Join(',', roles)}] {Environment.NewLine}" : string.Empty; + optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: [{string.Join(',', roles)}]{Environment.NewLine}" : string.Empty; var scopes = scopesOption.HasValue() ? scopesOption.Values : new List(); - optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(',', scopes)} {Environment.NewLine}" : string.Empty; + optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(',', scopes)}{Environment.NewLine}" : string.Empty; var claims = new Dictionary(); if (claimsOption.HasValue()) @@ -173,7 +173,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( reporter.Error(Resources.CreateCommand_InvalidClaims_Error); isValid = false; } - optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(',', claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}] {Environment.NewLine}"; + optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(',', claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]{Environment.NewLine}"; } return ( From cf90596017f73e05ab96bdd31e56ea2130bddd19 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Wed, 15 Jun 2022 13:00:55 -0700 Subject: [PATCH 5/7] Update src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs Co-authored-by: Damian Edwards --- src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index ec8bb3700a55..2b867428378a 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -118,7 +118,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( 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; + optionsString += audienceOption.HasValue() ? $"{Resources.JwtPrint_Audiences}: {audience}{Environment.NewLine}" : string.Empty; if (audience is null) { reporter.Error(Resources.CreateCommand_NoAudience_Error); From e5e1ba855ac9f0921ca9aa7fd3f3f165acc4cb55 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 16 Jun 2022 11:25:10 -0700 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Brennan --- src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs | 6 +++--- src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs | 2 +- src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index 2b867428378a..d5234e8290c3 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -160,10 +160,10 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( } var roles = rolesOption.HasValue() ? rolesOption.Values : new List(); - optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: [{string.Join(',', roles)}]{Environment.NewLine}" : string.Empty; + optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: [{string.Join(", ", roles)}]{Environment.NewLine}" : string.Empty; var scopes = scopesOption.HasValue() ? scopesOption.Values : new List(); - optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(',', scopes)}{Environment.NewLine}" : string.Empty; + optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(", ", scopes)}{Environment.NewLine}" : string.Empty; var claims = new Dictionary(); if (claimsOption.HasValue()) @@ -173,7 +173,7 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( reporter.Error(Resources.CreateCommand_InvalidClaims_Error); isValid = false; } - optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(',', claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]{Environment.NewLine}"; + optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(", ", claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]{Environment.NewLine}"; } return ( diff --git a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs index dedd523de3e6..6ed346d20da2 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs @@ -42,7 +42,7 @@ private static int Execute(IReporter reporter, string projectPath, bool showToke if (jwtStore.Jwts is { Count: > 0 } jwts) { var table = new ConsoleTable(reporter); - table.AddColumns(Resources.JwtPrint_Id, Resources.JwtPrint_Scheme, Resources.JwtPrint_Name, Resources.JwtPrint_IssuedOn, Resources.JwtPrint_ExpiresOn); + table.AddColumns(Resources.JwtPrint_Id, Resources.JwtPrint_Scheme, Resources.JwtPrint_Audiences, Resources.JwtPrint_IssuedOn, Resources.JwtPrint_ExpiresOn); if (showTokens) { diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index 2d06a08dab63..eb300d533d1e 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -160,7 +160,7 @@ public static void PrintJwt(IReporter reporter, Jwt jwt, bool showAll, JwtSecuri { var scopesValue = jwt.Scopes.IsNullOrEmpty() ? "none" - : string.Join(',', jwt.Scopes); + : string.Join(", ", jwt.Scopes); reporter.Output($"{Resources.JwtPrint_Scopes}: {scopesValue}"); } @@ -168,7 +168,7 @@ public static void PrintJwt(IReporter reporter, Jwt jwt, bool showAll, JwtSecuri { var rolesValue = jwt.Roles.IsNullOrEmpty() ? "none" - : String.Join(',', jwt.Roles); + : String.Join(", ", jwt.Roles); reporter.Output($"{Resources.JwtPrint_Roles}: [{rolesValue}]"); } @@ -176,7 +176,7 @@ public static void PrintJwt(IReporter reporter, Jwt jwt, bool showAll, JwtSecuri { var customClaimsValue = jwt.CustomClaims.IsNullOrEmpty() ? "none" - : string.Join(',', jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}")); + : string.Join(", ", jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}")); reporter.Output($"{Resources.JwtPrint_CustomClaims}: [{customClaimsValue}]"); } From 697775281e5c769a822bea37f21a1b2b587d3ca8 Mon Sep 17 00:00:00 2001 From: Safia Abdalla Date: Thu, 16 Jun 2022 18:44:50 +0000 Subject: [PATCH 7/7] Warn on invalid expires on argument combo --- .../src/Commands/CreateCommand.cs | 23 +++++++- src/Tools/dotnet-user-jwts/src/Resources.resx | 59 ++++++++++--------- .../dotnet-user-jwts/test/UserJwtsTests.cs | 13 ++++ 3 files changed, 65 insertions(+), 30 deletions(-) diff --git a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs index d5234e8290c3..0b6dda38a89f 100644 --- a/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs +++ b/src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs @@ -146,7 +146,17 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--expires-on")); isValid = false; } - optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}"; + + if (validForOption.HasValue()) + { + reporter.Error(Resources.CreateCommand_InvalidExpiresOn_Error); + isValid = false; + } + else + { + optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}"; + } + } if (validForOption.HasValue()) @@ -156,7 +166,16 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments( reporter.Error(Resources.FormatCreateCommand_InvalidPeriod_Error("--valid-for")); } expiresOn = notBefore.Add(validForValue); - optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}"; + + 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(); diff --git a/src/Tools/dotnet-user-jwts/src/Resources.resx b/src/Tools/dotnet-user-jwts/src/Resources.resx index 4df05f6ca8da..3bd782f5b052 100644 --- a/src/Tools/dotnet-user-jwts/src/Resources.resx +++ b/src/Tools/dotnet-user-jwts/src/Resources.resx @@ -1,17 +1,17 @@  - @@ -156,6 +156,9 @@ The date provided for '{type}' could not be parsed. Dates must consist of a date and can include an optional timestamp. + + '--valid-for' and '--expires-on' are mutually exclusive flags. Provide either option but not both. + The period provided for '{0}' could not be parsed. Ensure you use a format like '10d', '22h', '45s' etc. @@ -297,4 +300,4 @@ No JWT with ID '{0}' found. - \ No newline at end of file + diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs index c7f502adce59..44a2a94798f7 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -203,6 +203,19 @@ public void CreateCommand_ShowsCustomizedTokenDetails() Assert.Contains("Scheme: customScheme", output); } + [Fact] + public void CreateCommand_DisplaysErrorForInvalidExpiresOnCombination() + { + var project = Path.Combine(_fixture.CreateProject(), "TestProject.csproj"); + var app = new Program(_console); + + app.Run(new[] { "create", "--project", project, "--expires-on", DateTime.UtcNow.AddDays(2).ToString("O"), "--valid-for", "2h" }); + var output = _console.GetOutput(); + + Assert.Contains($"'--valid-for' and '--expires-on' are mutually exclusive flags. Provide either option but not both.", output); + Assert.DoesNotContain("Expires On: ", output); + } + [Fact] public void PrintCommand_ShowsBasicOptions() {