Skip to content

Commit b0a348c

Browse files
captainsafiaDamianEdwardsBrennanConroy
authored
Fix up user-jwts interactions (#42125)
* Fix up user-jwts interactions * Address feedback * Apply suggestions from code review Co-authored-by: Damian Edwards <[email protected]> * Apply suggestions from code review Co-authored-by: Damian Edwards <[email protected]> * Update src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs Co-authored-by: Damian Edwards <[email protected]> * Apply suggestions from code review Co-authored-by: Brennan <[email protected]> * Warn on invalid expires on argument combo Co-authored-by: Damian Edwards <[email protected]> Co-authored-by: Brennan <[email protected]>
1 parent 7ba42fb commit b0a348c

File tree

8 files changed

+310
-60
lines changed

8 files changed

+310
-60
lines changed

src/Tools/dotnet-user-jwts/src/Commands/CreateCommand.cs

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Globalization;
55
using System.Linq;
6+
using System.Text;
67
using Microsoft.Extensions.CommandLineUtils;
78
using Microsoft.Extensions.Tools.Internal;
89

@@ -11,7 +12,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
1112
internal sealed class CreateCommand
1213
{
1314
private static readonly string[] _dateTimeFormats = new[] {
14-
"yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy/MM/dd", "yyyy/MM/dd HH:mm" };
15+
"yyyy-MM-dd", "yyyy-MM-dd HH:mm", "yyyy/MM/dd", "yyyy/MM/dd HH:mm", "yyyy-MM-ddTHH:mm:ss.fffffffzzz" };
1516
private static readonly string[] _timeSpanFormats = new[] {
1617
@"d\dh\hm\ms\s", @"d\dh\hm\m", @"d\dh\h", @"d\d",
1718
@"h\hm\ms\s", @"h\hm\m", @"h\h",
@@ -32,7 +33,7 @@ public static void Register(ProjectCommandLineApplication app)
3233
);
3334

3435
var nameOption = cmd.Option(
35-
"--name",
36+
"-n|--name",
3637
Resources.CreateCommand_NameOption_Description,
3738
CommandOptionType.SingleValue);
3839

@@ -80,20 +81,20 @@ public static void Register(ProjectCommandLineApplication app)
8081

8182
cmd.OnExecute(() =>
8283
{
83-
var (options, isValid) = ValidateArguments(
84+
var (options, isValid, optionsString) = ValidateArguments(
8485
cmd.Reporter, cmd.ProjectOption, schemeNameOption, nameOption, audienceOption, issuerOption, notBeforeOption, expiresOnOption, validForOption, rolesOption, scopesOption, claimsOption);
8586

8687
if (!isValid)
8788
{
8889
return 1;
8990
}
9091

91-
return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options);
92+
return Execute(cmd.Reporter, cmd.ProjectOption.Value(), options, optionsString);
9293
});
9394
});
9495
}
9596

96-
private static (JwtCreatorOptions, bool) ValidateArguments(
97+
private static (JwtCreatorOptions, bool, string) ValidateArguments(
9798
IReporter reporter,
9899
CommandOption projectOption,
99100
CommandOption schemeNameOption,
@@ -109,16 +110,22 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
109110
{
110111
var isValid = true;
111112
var project = DevJwtCliHelpers.GetProject(projectOption.Value());
113+
112114
var scheme = schemeNameOption.HasValue() ? schemeNameOption.Value() : "Bearer";
115+
var optionsString = schemeNameOption.HasValue() ? $"{Resources.JwtPrint_Scheme}: {scheme}{Environment.NewLine}" : string.Empty;
116+
113117
var name = nameOption.HasValue() ? nameOption.Value() : Environment.UserName;
118+
optionsString += $"{Resources.JwtPrint_Name}: {name}{Environment.NewLine}";
114119

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

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

133141
var expiresOn = notBefore.AddMonths(3);
@@ -138,6 +146,17 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
138146
reporter.Error(Resources.FormatCreateCommand_InvalidDate_Error("--expires-on"));
139147
isValid = false;
140148
}
149+
150+
if (validForOption.HasValue())
151+
{
152+
reporter.Error(Resources.CreateCommand_InvalidExpiresOn_Error);
153+
isValid = false;
154+
}
155+
else
156+
{
157+
optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}";
158+
}
159+
141160
}
142161

143162
if (validForOption.HasValue())
@@ -147,10 +166,23 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
147166
reporter.Error(Resources.FormatCreateCommand_InvalidPeriod_Error("--valid-for"));
148167
}
149168
expiresOn = notBefore.Add(validForValue);
169+
170+
if (expiresOnOption.HasValue())
171+
{
172+
reporter.Error(Resources.CreateCommand_InvalidExpiresOn_Error);
173+
isValid = false;
174+
}
175+
else
176+
{
177+
optionsString += $"{Resources.JwtPrint_ExpiresOn}: {expiresOn:O}{Environment.NewLine}";
178+
}
150179
}
151180

152181
var roles = rolesOption.HasValue() ? rolesOption.Values : new List<string>();
182+
optionsString += rolesOption.HasValue() ? $"{Resources.JwtPrint_Roles}: [{string.Join(", ", roles)}]{Environment.NewLine}" : string.Empty;
183+
153184
var scopes = scopesOption.HasValue() ? scopesOption.Values : new List<string>();
185+
optionsString += scopesOption.HasValue() ? $"{Resources.JwtPrint_Scopes}: {string.Join(", ", scopes)}{Environment.NewLine}" : string.Empty;
154186

155187
var claims = new Dictionary<string, string>();
156188
if (claimsOption.HasValue())
@@ -160,9 +192,13 @@ private static (JwtCreatorOptions, bool) ValidateArguments(
160192
reporter.Error(Resources.CreateCommand_InvalidClaims_Error);
161193
isValid = false;
162194
}
195+
optionsString += $"{Resources.JwtPrint_CustomClaims}: [{string.Join(", ", claims.Select(kvp => $"{kvp.Key}={kvp.Value}"))}]{Environment.NewLine}";
163196
}
164197

165-
return (new JwtCreatorOptions(scheme, name, audience, issuer, notBefore, expiresOn, roles, scopes, claims), isValid);
198+
return (
199+
new JwtCreatorOptions(scheme, name, audience, issuer, notBefore, expiresOn, roles, scopes, claims),
200+
isValid,
201+
optionsString);
166202

167203
static bool ParseDate(string datetime, out DateTime parsedDateTime) =>
168204
DateTime.TryParseExact(datetime, _dateTimeFormats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out parsedDateTime);
@@ -171,7 +207,8 @@ static bool ParseDate(string datetime, out DateTime parsedDateTime) =>
171207
private static int Execute(
172208
IReporter reporter,
173209
string projectPath,
174-
JwtCreatorOptions options)
210+
JwtCreatorOptions options,
211+
string optionsString)
175212
{
176213
if (!DevJwtCliHelpers.GetProjectAndSecretsId(projectPath, reporter, out var project, out var userSecretsId))
177214
{
@@ -196,6 +233,8 @@ private static int Execute(
196233
settingsToWrite.Save(appsettingsFilePath);
197234

198235
reporter.Output(Resources.FormatCreateCommand_Confirmed(jwtToken.Id));
236+
reporter.Output(optionsString);
237+
reporter.Output($"{Resources.JwtPrint_Token}: {jwt.Token}");
199238

200239
return 0;
201240
}

src/Tools/dotnet-user-jwts/src/Commands/ListCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,11 @@ private static int Execute(IReporter reporter, string projectPath, bool showToke
4242
if (jwtStore.Jwts is { Count: > 0 } jwts)
4343
{
4444
var table = new ConsoleTable(reporter);
45-
table.AddColumns("Id", "Scheme Name", "Audience", "Issued", "Expires");
45+
table.AddColumns(Resources.JwtPrint_Id, Resources.JwtPrint_Scheme, Resources.JwtPrint_Audiences, Resources.JwtPrint_IssuedOn, Resources.JwtPrint_ExpiresOn);
4646

4747
if (showTokens)
4848
{
49-
table.AddColumns("Encoded Token");
49+
table.AddColumns(Resources.JwtPrint_Token);
5050
}
5151

5252
foreach (var jwtRow in jwts)

src/Tools/dotnet-user-jwts/src/Commands/PrintCommand.cs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,7 @@ public static void Register(ProjectCommandLineApplication app)
1515
cmd.Description = Resources.PrintCommand_Description;
1616

1717
var idArgument = cmd.Argument("[id]", Resources.PrintCommand_IdArgument_Description);
18-
19-
var showFullOption = cmd.Option(
20-
"--show-full",
21-
Resources.PrintCommand_ShowFullOption_Description,
22-
CommandOptionType.NoValue);
18+
var showAllOption = cmd.Option("--show-all", Resources.PrintCommand_ShowAllOption_Description, CommandOptionType.NoValue);
2319

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

@@ -30,12 +26,16 @@ public static void Register(ProjectCommandLineApplication app)
3026
cmd.ShowHelp();
3127
return 0;
3228
}
33-
return Execute(cmd.Reporter, cmd.ProjectOption.Value(), idArgument.Value, showFullOption.HasValue());
29+
return Execute(
30+
cmd.Reporter,
31+
cmd.ProjectOption.Value(),
32+
idArgument.Value,
33+
showAllOption.HasValue());
3434
});
3535
});
3636
}
3737

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

5252
reporter.Output(Resources.FormatPrintCommand_Confirmed(id));
53-
JwtSecurityToken fullToken;
54-
55-
if (showFull)
56-
{
57-
fullToken = JwtIssuer.Extract(jwt.Token);
58-
DevJwtCliHelpers.PrintJwt(reporter, jwt, fullToken);
59-
}
53+
JwtSecurityToken fullToken = JwtIssuer.Extract(jwt.Token);
54+
DevJwtCliHelpers.PrintJwt(reporter, jwt, showAll, fullToken);
6055

6156
return 0;
6257
}

src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.Extensions.Configuration;
88
using Microsoft.Extensions.Configuration.UserSecrets;
99
using Microsoft.Extensions.Tools.Internal;
10+
using Microsoft.IdentityModel.Tokens;
1011

1112
namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
1213

@@ -145,16 +146,48 @@ public static string[] GetAudienceCandidatesFromLaunchSettings(string project)
145146
return null;
146147
}
147148

148-
public static void PrintJwt(IReporter reporter, Jwt jwt, JwtSecurityToken fullToken = null)
149+
public static void PrintJwt(IReporter reporter, Jwt jwt, bool showAll, JwtSecurityToken fullToken = null)
149150
{
150-
reporter.Output(JsonSerializer.Serialize(jwt, new JsonSerializerOptions { WriteIndented = true }));
151+
reporter.Output($"{Resources.JwtPrint_Id}: {jwt.Id}");
152+
reporter.Output($"{Resources.JwtPrint_Name}: {jwt.Name}");
153+
reporter.Output($"{Resources.JwtPrint_Scheme}: {jwt.Scheme}");
154+
reporter.Output($"{Resources.JwtPrint_Audiences}: {jwt.Audience}");
155+
reporter.Output($"{Resources.JwtPrint_NotBefore}: {jwt.NotBefore:O}");
156+
reporter.Output($"{Resources.JwtPrint_ExpiresOn}: {jwt.Expires:O}");
157+
reporter.Output($"{Resources.JwtPrint_IssuedOn}: {jwt.Issued:O}");
158+
159+
if (!jwt.Scopes.IsNullOrEmpty() || showAll)
160+
{
161+
var scopesValue = jwt.Scopes.IsNullOrEmpty()
162+
? "none"
163+
: string.Join(", ", jwt.Scopes);
164+
reporter.Output($"{Resources.JwtPrint_Scopes}: {scopesValue}");
165+
}
166+
167+
if (!jwt.Roles.IsNullOrEmpty() || showAll)
168+
{
169+
var rolesValue = jwt.Roles.IsNullOrEmpty()
170+
? "none"
171+
: String.Join(", ", jwt.Roles);
172+
reporter.Output($"{Resources.JwtPrint_Roles}: [{rolesValue}]");
173+
}
151174

152-
if (fullToken is not null)
175+
if (!jwt.CustomClaims.IsNullOrEmpty() || showAll)
153176
{
154-
reporter.Output($"Token Header: {fullToken.Header.SerializeToJson()}");
155-
reporter.Output($"Token Payload: {fullToken.Payload.SerializeToJson()}");
177+
var customClaimsValue = jwt.CustomClaims.IsNullOrEmpty()
178+
? "none"
179+
: string.Join(", ", jwt.CustomClaims.Select(kvp => $"{kvp.Key}={kvp.Value}"));
180+
reporter.Output($"{Resources.JwtPrint_CustomClaims}: [{customClaimsValue}]");
156181
}
157-
reporter.Output($"Compact Token: {jwt.Token}");
182+
183+
if (showAll)
184+
{
185+
reporter.Output($"{Resources.JwtPrint_TokenHeader}: {fullToken.Header.SerializeToJson()}");
186+
reporter.Output($"{Resources.JwtPrint_TokenPayload}: {fullToken.Payload.SerializeToJson()}");
187+
}
188+
189+
var tokenValueFieldName = showAll ? Resources.JwtPrint_CompactToken : Resources.JwtPrint_Token;
190+
reporter.Output($"{tokenValueFieldName}: {jwt.Token}");
158191
}
159192

160193
public static bool TryParseClaims(List<string> input, out Dictionary<string, string> claims)

src/Tools/dotnet-user-jwts/src/Helpers/Jwt.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static Jwt Create(
2424
IEnumerable<string> roles = null,
2525
IDictionary<string, string> customClaims = null)
2626
{
27-
return new Jwt(token.Id, scheme, token.Subject, token.Audiences.FirstOrDefault(), token.ValidFrom, token.ValidTo, token.IssuedAt, encodedToken)
27+
return new Jwt(token.Id, scheme, token.Subject, string.Join(", ", token.Audiences), token.ValidFrom, token.ValidTo, token.IssuedAt, encodedToken)
2828
{
2929
Scopes = scopes,
3030
Roles = roles,

src/Tools/dotnet-user-jwts/src/Program.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ public void Run(string[] args)
5151
{
5252
userJwts.Execute(args);
5353
}
54+
catch (CommandParsingException parsingException)
55+
{
56+
_reporter.Error(parsingException.Message);
57+
userJwts.ShowHelp();
58+
}
5459
catch (Exception ex)
5560
{
5661
_reporter.Error(ex.Message);

0 commit comments

Comments
 (0)