Skip to content

Commit 4e2e41d

Browse files
Handle JSON comments and trailing commas in user-jwts (#54826)
* Handle JSON comments and trailing commas Handle the presence of comments and trailing commas in JSON files processed by user-jwts. Also fixes up some of the IDE suggestions found while making the change. Resolves #54814. * Use one JsonSerializerOptions Use a shared static customised `JsonSerializerOptions` instance. * Rename to JwtSerializerOptions Address feedback.
1 parent dc1acba commit 4e2e41d

File tree

9 files changed

+76
-49
lines changed

9 files changed

+76
-49
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ private static int Execute(
254254
reporter.Output(jwt.Token);
255255
break;
256256
case "json":
257-
reporter.Output(JsonSerializer.Serialize(jwt, new JsonSerializerOptions { WriteIndented = true }));
257+
reporter.Output(JsonSerializer.Serialize(jwt, JwtSerializerOptions.Default));
258258
break;
259259
default:
260260
reporter.Output(Resources.FormatCreateCommand_Confirmed(jwtToken.Id));

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Text.Json;
45
using Microsoft.Extensions.CommandLineUtils;
56
using Microsoft.Extensions.Tools.Internal;
6-
using System.Text.Json;
77

88
namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
99

@@ -54,11 +54,11 @@ private static void PrintJwtsJson(IReporter reporter, JwtStore jwtStore)
5454
{
5555
if (jwtStore.Jwts is { Count: > 0 } jwts)
5656
{
57-
reporter.Output(JsonSerializer.Serialize(jwts, new JsonSerializerOptions { WriteIndented = true }));
57+
reporter.Output(JsonSerializer.Serialize(jwts, JwtSerializerOptions.Default));
5858
}
5959
else
6060
{
61-
reporter.Output(JsonSerializer.Serialize(Array.Empty<Jwt>(), new JsonSerializerOptions { WriteIndented = true }));
61+
reporter.Output(JsonSerializer.Serialize(Array.Empty<Jwt>(), JwtSerializerOptions.Default));
6262
}
6363
}
6464

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
using System.IdentityModel.Tokens.Jwt;
55
using System.Linq;
66
using System.Text.Json;
7-
using System.Text.Json.Nodes;
8-
using Microsoft.Extensions.Configuration;
9-
using Microsoft.Extensions.Configuration.UserSecrets;
107
using Microsoft.Extensions.Tools.Internal;
118
using Microsoft.IdentityModel.Tokens;
129

@@ -144,7 +141,7 @@ public static void PrintJwt(IReporter reporter, Jwt jwt, bool showAll, string ou
144141

145142
static void PrintJwtJson(IReporter reporter, Jwt jwt, bool showAll, JwtSecurityToken fullToken)
146143
{
147-
reporter.Output(JsonSerializer.Serialize(jwt, new JsonSerializerOptions { WriteIndented = true }));
144+
reporter.Output(JsonSerializer.Serialize(jwt, JwtSerializerOptions.Default));
148145
}
149146

150147
static void PrintJwtDefault(IReporter reporter, Jwt jwt, bool showAll, JwtSecurityToken fullToken)

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

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,10 @@ internal sealed record JwtAuthenticationSchemeSettings(string SchemeName, List<s
1313
private const string AuthenticationKey = "Authentication";
1414
private const string SchemesKey = "Schemes";
1515

16-
private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions
17-
{
18-
WriteIndented = true,
19-
};
20-
2116
public void Save(string filePath)
2217
{
2318
using var reader = new FileStream(filePath, FileMode.Open, FileAccess.Read);
24-
var config = JsonSerializer.Deserialize<JsonObject>(reader, _jsonSerializerOptions);
19+
var config = JsonSerializer.Deserialize<JsonObject>(reader, JwtSerializerOptions.Default);
2520
reader.Close();
2621

2722
var settingsObject = new JsonObject
@@ -63,13 +58,13 @@ public void Save(string filePath)
6358
streamOptions.UnixCreateMode = UnixFileMode.UserRead | UnixFileMode.UserWrite;
6459
}
6560
using var writer = new FileStream(filePath, streamOptions);
66-
JsonSerializer.Serialize(writer, config, _jsonSerializerOptions);
61+
JsonSerializer.Serialize(writer, config, JwtSerializerOptions.Default);
6762
}
6863

6964
public static void RemoveScheme(string filePath, string name)
7065
{
7166
using var reader = new FileStream(filePath, FileMode.Open, FileAccess.Read);
72-
var config = JsonSerializer.Deserialize<JsonObject>(reader);
67+
var config = JsonSerializer.Deserialize<JsonObject>(reader, JwtSerializerOptions.Default);
7368
reader.Close();
7469

7570
if (config[AuthenticationKey] is JsonObject authentication &&
@@ -79,6 +74,6 @@ public static void RemoveScheme(string filePath, string name)
7974
}
8075

8176
using var writer = new FileStream(filePath, FileMode.Create, FileAccess.Write);
82-
JsonSerializer.Serialize(writer, config, _jsonSerializerOptions);
77+
JsonSerializer.Serialize(writer, config, JwtSerializerOptions.Default);
8378
}
8479
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Text.Json;
5+
6+
namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
7+
8+
internal static class JwtSerializerOptions
9+
{
10+
public static JsonSerializerOptions Default { get; } = new()
11+
{
12+
AllowTrailingCommas = true,
13+
ReadCommentHandling = JsonCommentHandling.Skip,
14+
WriteIndented = true,
15+
};
16+
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
1010
public class JwtStore
1111
{
1212
private const string FileName = "user-jwts.json";
13-
private readonly string _userSecretsId;
1413
private readonly string _filePath;
1514

1615
public JwtStore(string userSecretsId, Program program = null)
1716
{
18-
_userSecretsId = userSecretsId;
1917
_filePath = Path.Combine(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(userSecretsId)), FileName);
2018
Load();
2119

@@ -35,7 +33,7 @@ public void Load()
3533
using var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
3634
if (fileStream.Length > 0)
3735
{
38-
Jwts = JsonSerializer.Deserialize<IDictionary<string, Jwt>>(fileStream) ?? new Dictionary<string, Jwt>();
36+
Jwts = JsonSerializer.Deserialize<IDictionary<string, Jwt>>(fileStream, JwtSerializerOptions.Default) ?? new Dictionary<string, Jwt>();
3937
}
4038
}
4139
}

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using Microsoft.Extensions.Configuration.UserSecrets;
5-
using Microsoft.Extensions.Configuration;
6-
using System.Text.Json.Nodes;
7-
using System.Text.Json;
84
using System.Linq;
9-
using System.Globalization;
5+
using System.Text.Json;
6+
using System.Text.Json.Nodes;
7+
using Microsoft.Extensions.Configuration;
8+
using Microsoft.Extensions.Configuration.UserSecrets;
109

1110
namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools;
1211

@@ -69,7 +68,7 @@ public static byte[] CreateSigningKeyMaterial(string userSecretsId, string schem
6968
using var secretsFileStream = new FileStream(secretsFilePath, FileMode.Open, FileAccess.Read);
7069
if (secretsFileStream.Length > 0)
7170
{
72-
secrets = JsonSerializer.Deserialize<JsonObject>(secretsFileStream);
71+
secrets = JsonSerializer.Deserialize<JsonObject>(secretsFileStream, JwtSerializerOptions.Default);
7372
}
7473
}
7574

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

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System;
5-
using System.Collections.Generic;
64
using System.Globalization;
7-
using System.IO;
85
using Microsoft.Extensions.Configuration.UserSecrets;
96

107
namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests;
118

12-
public class UserJwtsTestFixture : IDisposable
9+
public sealed class UserJwtsTestFixture : IDisposable
1310
{
14-
private Stack<Action> _disposables = new Stack<Action>();
15-
internal string TestSecretsId;
11+
private readonly Stack<Action> _disposables = new();
12+
internal string TestSecretsId { get; private set; }
1613

1714
private const string ProjectTemplate = @"<Project Sdk=""Microsoft.NET.Sdk"">
1815
<PropertyGroup>

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

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools.Tests;
1414

1515
public class UserJwtsTests(UserJwtsTestFixture fixture, ITestOutputHelper output) : IClassFixture<UserJwtsTestFixture>
1616
{
17-
private readonly TestConsole _console = new TestConsole(output);
17+
private readonly TestConsole _console = new(output);
1818

1919
[Fact]
2020
public void List_NoTokensForNewProject()
@@ -71,12 +71,10 @@ public void Create_CanModifyExistingScheme()
7171

7272
app.Run(new[] { "create", "--project", project });
7373
Assert.Contains("New JWT saved", _console.GetOutput());
74-
var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'");
75-
var id = matches.SingleOrDefault().Groups[1].Value;
7674

7775
var appSettings = JsonSerializer.Deserialize<JsonObject>(File.ReadAllText(appsettings));
7876
Assert.Equal("dotnet-user-jwts", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue<string>());
79-
app.Run(new[] { "create", "--project", project, "--issuer", "new-issuer" });
77+
app.Run(["create", "--project", project, "--issuer", "new-issuer"]);
8078
appSettings = JsonSerializer.Deserialize<JsonObject>(File.ReadAllText(appsettings));
8179
Assert.Equal("new-issuer", appSettings["Authentication"]["Schemes"]["Bearer"]["ValidIssuer"].GetValue<string>());
8280
}
@@ -208,7 +206,7 @@ await File.WriteAllTextAsync(secretsFilePath,
208206
app.Run(new[] { "key", "--reset", "--force", "--project", project });
209207
Assert.Contains("New signing key created:", _console.GetOutput());
210208

211-
using FileStream openStream = File.OpenRead(secretsFilePath);
209+
using var openStream = File.OpenRead(secretsFilePath);
212210
var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
213211
Assert.NotNull(secretsJson);
214212
Assert.True(secretsJson.ContainsKey(SigningKeysHandler.GetSigningKeyPropertyName(DevJwtsDefaults.Scheme)));
@@ -423,11 +421,39 @@ await File.WriteAllTextAsync(secretsFilePath,
423421
}
424422
}");
425423
var app = new Program(_console);
426-
app.Run(new[] { "create", "--project", project});
424+
app.Run(new[] { "create", "--project", project });
425+
var output = _console.GetOutput();
426+
427+
Assert.Contains("New JWT saved", output);
428+
using var openStream = File.OpenRead(secretsFilePath);
429+
var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
430+
Assert.NotNull(secretsJson);
431+
var signingKey = Assert.Single(secretsJson[SigningKeysHandler.GetSigningKeyPropertyName(DevJwtsDefaults.Scheme)].AsArray());
432+
Assert.Equal(32, signingKey["Length"].GetValue<int>());
433+
Assert.True(Convert.TryFromBase64String(signingKey["Value"].GetValue<string>(), new byte[32], out var _));
434+
Assert.True(secretsJson.TryGetPropertyValue("Foo", out var fooField));
435+
Assert.Equal("baz", fooField["Bar"].GetValue<string>());
436+
}
437+
438+
[Fact]
439+
public async Task Create_GracefullyHandles_PrepopulatedSecrets_WithCommasAndComments()
440+
{
441+
var projectPath = fixture.CreateProject();
442+
var project = Path.Combine(projectPath, "TestProject.csproj");
443+
var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(fixture.TestSecretsId);
444+
await File.WriteAllTextAsync(secretsFilePath,
445+
@"{
446+
""Foo"": {
447+
""Bar"": ""baz"",
448+
//""Bar"": ""baz"",
449+
}
450+
}");
451+
var app = new Program(_console);
452+
app.Run(["create", "--project", project]);
427453
var output = _console.GetOutput();
428454

429455
Assert.Contains("New JWT saved", output);
430-
using FileStream openStream = File.OpenRead(secretsFilePath);
456+
using var openStream = File.OpenRead(secretsFilePath);
431457
var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
432458
Assert.NotNull(secretsJson);
433459
var signingKey = Assert.Single(secretsJson[SigningKeysHandler.GetSigningKeyPropertyName(DevJwtsDefaults.Scheme)].AsArray());
@@ -444,7 +470,7 @@ public void Create_GetsAudiencesFromAllIISAndKestrel()
444470
var project = Path.Combine(projectPath, "TestProject.csproj");
445471

446472
var app = new Program(_console);
447-
app.Run(new[] { "create", "--project", project});
473+
app.Run(new[] { "create", "--project", project });
448474
var matches = Regex.Matches(_console.GetOutput(), "New JWT saved with ID '(.*?)'");
449475
var id = matches.SingleOrDefault().Groups[1].Value;
450476
app.Run(new[] { "print", id, "--project", project, "--show-all" });
@@ -466,7 +492,7 @@ public async Task Create_SupportsSettingACustomIssuerAndScheme()
466492

467493
Assert.Contains("New JWT saved", _console.GetOutput());
468494

469-
using FileStream openStream = File.OpenRead(secretsFilePath);
495+
using var openStream = File.OpenRead(secretsFilePath);
470496
var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
471497
Assert.True(secretsJson.ContainsKey(SigningKeysHandler.GetSigningKeyPropertyName("test-scheme")));
472498
var signingKey = Assert.Single(secretsJson[SigningKeysHandler.GetSigningKeyPropertyName("test-scheme")].AsArray());
@@ -488,7 +514,7 @@ public async Task Create_SupportsSettingMutlipleIssuersAndSingleScheme()
488514

489515
Assert.Contains("New JWT saved", _console.GetOutput());
490516

491-
using FileStream openStream = File.OpenRead(secretsFilePath);
517+
using var openStream = File.OpenRead(secretsFilePath);
492518
var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
493519
Assert.True(secretsJson.ContainsKey(SigningKeysHandler.GetSigningKeyPropertyName("test-scheme")));
494520
var signingKeys = secretsJson[SigningKeysHandler.GetSigningKeyPropertyName("test-scheme")].AsArray();
@@ -510,7 +536,7 @@ public async Task Create_SupportsSettingSingleIssuerAndMultipleSchemes()
510536

511537
Assert.Contains("New JWT saved", _console.GetOutput());
512538

513-
using FileStream openStream = File.OpenRead(secretsFilePath);
539+
using var openStream = File.OpenRead(secretsFilePath);
514540
var secretsJson = await JsonSerializer.DeserializeAsync<JsonObject>(openStream);
515541
var signingKey1 = Assert.Single(secretsJson[SigningKeysHandler.GetSigningKeyPropertyName("test-scheme")].AsArray());
516542
Assert.Equal("test-issuer", signingKey1["Issuer"].GetValue<string>());
@@ -580,7 +606,7 @@ public void Create_CanHandleNoProjectOptionProvided()
580606
Directory.SetCurrentDirectory(projectPath);
581607

582608
var app = new Program(_console);
583-
app.Run(new[] { "create" });
609+
app.Run(["create"]);
584610

585611
Assert.DoesNotContain("No project found at `-p|--project` path or current directory.", _console.GetOutput());
586612
Assert.Contains("New JWT saved", _console.GetOutput());
@@ -593,7 +619,7 @@ public void Create_CanHandleNoProjectOptionProvided_WithNoProjects()
593619
Directory.SetCurrentDirectory(path.FullName);
594620

595621
var app = new Program(_console);
596-
app.Run(new[] { "create" });
622+
app.Run(["create"]);
597623

598624
Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput());
599625
Assert.DoesNotContain(Resources.CreateCommand_NoAudience_Error, _console.GetOutput());
@@ -606,7 +632,7 @@ public void Delete_CanHandleNoProjectOptionProvided_WithNoProjects()
606632
Directory.SetCurrentDirectory(path.FullName);
607633

608634
var app = new Program(_console);
609-
app.Run(new[] { "remove", "some-id" });
635+
app.Run(["remove", "some-id"]);
610636

611637
Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput());
612638
}
@@ -618,7 +644,7 @@ public void Clear_CanHandleNoProjectOptionProvided_WithNoProjects()
618644
Directory.SetCurrentDirectory(path.FullName);
619645

620646
var app = new Program(_console);
621-
app.Run(new[] { "clear" });
647+
app.Run(["clear"]);
622648

623649
Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput());
624650
}
@@ -630,7 +656,7 @@ public void List_CanHandleNoProjectOptionProvided_WithNoProjects()
630656
Directory.SetCurrentDirectory(path.FullName);
631657

632658
var app = new Program(_console);
633-
app.Run(new[] { "list" });
659+
app.Run(["list"]);
634660

635661
Assert.Contains($"Could not find a MSBuild project file in '{Directory.GetCurrentDirectory()}'. Specify which project to use with the --project option.", _console.GetOutput());
636662
}
@@ -669,7 +695,6 @@ public void Create_CanHandleRelativePathAsOption()
669695
var projectPath = fixture.CreateProject();
670696
var tempPath = Path.GetTempPath();
671697
var targetPath = Path.GetRelativePath(tempPath, projectPath);
672-
var project = Path.Combine(projectPath, "TestProject.csproj");
673698
Directory.SetCurrentDirectory(tempPath);
674699

675700
var app = new Program(_console);

0 commit comments

Comments
 (0)