Skip to content

Commit 69d3755

Browse files
authored
Improve missing/malformed file handling in user-jwts (#42309)
* Improve missing/malformed file handling in user-jwts * Address feedback from review * Update src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs
1 parent d668f23 commit 69d3755

File tree

6 files changed

+200
-58
lines changed

6 files changed

+200
-58
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,9 @@ private static (JwtCreatorOptions, bool, string) ValidateArguments(
123123
var name = nameOption.HasValue() ? nameOption.Value() : Environment.UserName;
124124
optionsString += $"{Resources.JwtPrint_Name}: {name}{Environment.NewLine}";
125125

126-
var audience = audienceOption.HasValue() ? audienceOption.Values : DevJwtCliHelpers.GetAudienceCandidatesFromLaunchSettings(project).ToList();
127-
optionsString += audienceOption.HasValue() ? $"{Resources.JwtPrint_Audiences}: {audience}{Environment.NewLine}" : string.Empty;
128-
if (audience is null)
126+
var audience = audienceOption.HasValue() ? audienceOption.Values : DevJwtCliHelpers.GetAudienceCandidatesFromLaunchSettings(project);
127+
optionsString += audienceOption.HasValue() ? $"{Resources.JwtPrint_Audiences}: {string.Join(", ", audience)}{Environment.NewLine}" : string.Empty;
128+
if (audience is null || audience.Count == 0)
129129
{
130130
reporter.Error(Resources.CreateCommand_NoAudience_Error);
131131
isValid = false;

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

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.IdentityModel.Tokens.Jwt;
55
using System.Linq;
66
using System.Text.Json;
7+
using System.Text.Json.Nodes;
78
using Microsoft.Extensions.Configuration;
89
using Microsoft.Extensions.Configuration.UserSecrets;
910
using Microsoft.Extensions.Tools.Internal;
@@ -84,35 +85,36 @@ public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset =
8485
var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
8586
Directory.CreateDirectory(Path.GetDirectoryName(secretsFilePath));
8687

87-
IDictionary<string, string> secrets = null;
88+
JsonObject secrets = null;
8889
if (File.Exists(secretsFilePath))
8990
{
9091
using var secretsFileStream = new FileStream(secretsFilePath, FileMode.Open, FileAccess.Read);
9192
if (secretsFileStream.Length > 0)
9293
{
93-
secrets = JsonSerializer.Deserialize<IDictionary<string, string>>(secretsFileStream) ?? new Dictionary<string, string>();
94+
secrets = JsonSerializer.Deserialize<JsonObject>(secretsFileStream);
9495
}
9596
}
9697

97-
secrets ??= new Dictionary<string, string>();
98+
secrets ??= new JsonObject();
9899

99100
if (reset && secrets.ContainsKey(DevJwtsDefaults.SigningKeyConfigurationKey))
100101
{
101102
secrets.Remove(DevJwtsDefaults.SigningKeyConfigurationKey);
102103
}
103-
secrets.Add(DevJwtsDefaults.SigningKeyConfigurationKey, Convert.ToBase64String(newKeyMaterial));
104+
secrets.Add(DevJwtsDefaults.SigningKeyConfigurationKey, JsonValue.Create(Convert.ToBase64String(newKeyMaterial)));
104105

105106
using var secretsWriteStream = new FileStream(secretsFilePath, FileMode.Create, FileAccess.Write);
106107
JsonSerializer.Serialize(secretsWriteStream, secrets);
107108

108109
return newKeyMaterial;
109110
}
110111

111-
public static string[] GetAudienceCandidatesFromLaunchSettings(string project)
112+
public static List<string> GetAudienceCandidatesFromLaunchSettings(string project)
112113
{
113114
ArgumentException.ThrowIfNullOrEmpty(nameof(project));
114115

115116
var launchSettingsFilePath = Path.Combine(Path.GetDirectoryName(project)!, "Properties", "launchSettings.json");
117+
var applicationUrls = new List<string>();
116118
if (File.Exists(launchSettingsFilePath))
117119
{
118120
using var launchSettingsFileStream = new FileStream(launchSettingsFilePath, FileMode.Open, FileAccess.Read);
@@ -124,26 +126,65 @@ public static string[] GetAudienceCandidatesFromLaunchSettings(string project)
124126
var profilesEnumerator = profiles.EnumerateObject();
125127
foreach (var profile in profilesEnumerator)
126128
{
127-
if (profile.Value.TryGetProperty("commandName", out var commandName))
129+
if (ExtractKestrelUrlsFromProfile(profile) is { } kestrelUrls)
128130
{
129-
if (commandName.ValueEquals("Project"))
130-
{
131-
if (profile.Value.TryGetProperty("applicationUrl", out var applicationUrl))
132-
{
133-
var value = applicationUrl.GetString();
134-
if (value is { } applicationUrls)
135-
{
136-
return applicationUrls.Split(';');
137-
}
138-
}
139-
}
131+
applicationUrls.AddRange(kestrelUrls);
132+
}
133+
134+
if (ExtractIISExpressUrlFromProfile(profile) is { } iisUrls)
135+
{
136+
applicationUrls.AddRange(iisUrls);
140137
}
141138
}
142139
}
143140
}
144141
}
145142

146-
return null;
143+
return applicationUrls;
144+
145+
static List<string> ExtractIISExpressUrlFromProfile(JsonProperty profile)
146+
{
147+
if (profile.NameEquals("iisSettings"))
148+
{
149+
if (profile.Value.TryGetProperty("iisExpress", out var iisExpress))
150+
{
151+
List<string> iisUrls = new();
152+
if (iisExpress.TryGetProperty("applicationUrl", out var iisUrl))
153+
{
154+
iisUrls.Add(iisUrl.GetString());
155+
}
156+
157+
if (iisExpress.TryGetProperty("sslPort", out var sslPort))
158+
{
159+
iisUrls.Add($"https://localhost:{sslPort.GetInt32()}");
160+
}
161+
162+
return iisUrls;
163+
}
164+
}
165+
166+
return null;
167+
}
168+
169+
static string[] ExtractKestrelUrlsFromProfile(JsonProperty profile)
170+
{
171+
if (profile.Value.TryGetProperty("commandName", out var commandName))
172+
{
173+
if (commandName.ValueEquals("Project"))
174+
{
175+
if (profile.Value.TryGetProperty("applicationUrl", out var applicationUrl))
176+
{
177+
var value = applicationUrl.GetString();
178+
if (value is { } urls)
179+
{
180+
return urls.Split(';');
181+
}
182+
}
183+
}
184+
}
185+
186+
return null;
187+
}
147188
}
148189

149190
public static void PrintJwt(IReporter reporter, Jwt jwt, bool showAll, JwtSecurityToken fullToken = null)
@@ -163,12 +204,12 @@ public static void PrintJwt(IReporter reporter, Jwt jwt, bool showAll, JwtSecuri
163204
: string.Join(", ", jwt.Scopes);
164205
reporter.Output($"{Resources.JwtPrint_Scopes}: {scopesValue}");
165206
}
166-
207+
167208
if (!jwt.Roles.IsNullOrEmpty() || showAll)
168209
{
169210
var rolesValue = jwt.Roles.IsNullOrEmpty()
170211
? "none"
171-
: String.Join(", ", jwt.Roles);
212+
: string.Join(", ", jwt.Roles);
172213
reporter.Output($"{Resources.JwtPrint_Roles}: [{rolesValue}]");
173214
}
174215

src/Tools/dotnet-user-jwts/src/Resources.resx

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -169,7 +169,7 @@
169169
<value>The name of the user to create the JWT for. Defaults to the current environment user.</value>
170170
</data>
171171
<data name="CreateCommand_NoAudience_Error" xml:space="preserve">
172-
<value>Could not determine the project's HTTPS URL. Please specify an audience for the JWT using the --audience option.</value>
172+
<value>Could not determine the project's URL. Please specify an audience for the JWT using the --audience option or ensure that launchSettings.json is configured properly.</value>
173173
</data>
174174
<data name="CreateCommand_NotBeforeOption_Description" xml:space="preserve">
175175
<value>The UTC date &amp; time the JWT should not be valid before in the format 'yyyy-MM-dd [[HH:mm[[:ss]]]]'. Defaults to the date &amp; time the JWT is created.</value>
@@ -303,4 +303,4 @@
303303
<data name="RemoveCommand_NoJwtFound" xml:space="preserve">
304304
<value>No JWT with ID '{0}' found.</value>
305305
</data>
306-
</root>
306+
</root>

src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@
3131
<Reference Include="Microsoft.Extensions.Configuration" />
3232
<Reference Include="Microsoft.Extensions.Configuration.UserSecrets" />
3333
</ItemGroup>
34-
</Project>
34+
35+
<ItemGroup>
36+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests" />
37+
</ItemGroup>
38+
</Project>

src/Tools/dotnet-user-jwts/test/UserJwtsTestFixture.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests;
1212
public class UserJwtsTestFixture : IDisposable
1313
{
1414
private Stack<Action> _disposables = new Stack<Action>();
15-
private string TestSecretsId = Guid.NewGuid().ToString();
15+
internal string TestSecretsId = Guid.NewGuid().ToString();
1616

1717
private const string ProjectTemplate = @"<Project Sdk=""Microsoft.NET.Sdk"">
1818
<PropertyGroup>
@@ -26,6 +26,14 @@ public class UserJwtsTestFixture : IDisposable
2626
private const string LaunchSettingsTemplate = @"
2727
{
2828
""profiles"": {
29+
""iisSettings"": {
30+
""windowsAuthentication"": false,
31+
""anonymousAuthentication"": true,
32+
""iisExpress"": {
33+
""applicationUrl"": ""http://localhost:23528"",
34+
""sslPort"": 44395
35+
}
36+
},
2937
""HttpApiSampleApp"": {
3038
""commandName"": ""Project"",
3139
""dotnetRunMessages"": true,
@@ -71,7 +79,7 @@ public string CreateProject(bool hasSecret = true)
7179
catch { }
7280
});
7381
}
74-
82+
7583
_disposables.Push(() => TryDelete(projectPath.FullName));
7684

7785
return projectPath.FullName;

0 commit comments

Comments
 (0)