From 32b5845f10707d6015c590c8357be6c2d4cbccaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn-Vegard=20Thoresen?= Date: Wed, 21 Jan 2026 14:07:36 +0100 Subject: [PATCH 1/3] unescapes json pasted into the terminal --- .../Extensions/CommandHandlerExtensions.cs | 20 ++++++++++++++++++- .../ReadClientSecretExpirationTests.cs | 6 ++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs b/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs index 508b35a..d3660b7 100644 --- a/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs +++ b/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs @@ -25,7 +25,7 @@ public static string ResolveKeyValuePathOrString( if (!string.IsNullOrWhiteSpace(directValue)) { logger.LogInformation("{keyLabel} provided directly.", keyLabel); - return directValue; + return UnescapeJsonIfNeeded(directValue.Trim()); } if (!string.IsNullOrWhiteSpace(filePath)) @@ -37,5 +37,23 @@ public static string ResolveKeyValuePathOrString( logger.LogWarning("{keyLabel} not provided.", keyLabel); return string.Empty; } + + /// + /// Unescapes terminal/shell-escaped JSON where the entire structure has escaped quotes. + /// Handles input like: {\"kid\":\"test\"} -> {"kid":"test"} + /// Does NOT unescape valid JSON that contains escaped chars within values, + /// e.g., {"d":"quote-\"here"} stays unchanged. + /// + private static string UnescapeJsonIfNeeded(string input) + { + // Check for {\" pattern (shell-escaped opening brace+quote) + if (input.StartsWith("{\\\"")) + { + return input + .Replace("\\\"", "\"") + .Replace("\\\\", "\\"); + } + return input; + } } } \ No newline at end of file diff --git a/Fhi.HelseId/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs b/Fhi.HelseId/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs index 7de7093..d519c85 100644 --- a/Fhi.HelseId/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs +++ b/Fhi.HelseId/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs @@ -14,7 +14,9 @@ public class ReadClientSecretExpirationTests [TestCase("{\n \"kid\": \"test-kid\",\n \"kty\": \"RSA\",\n \"d\": \"test-d-value\",\n \"n\": \"test-n-value\",\n \"e\": \"AQAB\"\n}")] [TestCase("{\"d\":\"test-kid\",\"e\":\"AQAB\",\"kid\":\"test-kid\",\"kty\":\"RSA\",\"n\":\"test-n-value\"}")] [TestCase("{ \"kid\": \"test-kid\", \"kty\": \"RSA\", \"d\": \"test-data\", \"n\": \"test-modulus\", \"e\": \"AQAB\" }")] - // [TestCase(@"{""kid"":""test-with-special-chars-!@#$%^&*()"",""d"":""data-with-quotes-\""and\""-backslashes-\\"",""n"":""modulus"",""e"":""AQAB""}")] + [TestCase(@"{""kid"":""test-kid"",""kty"":""RSA"",""d"":""test-with-special-chars-!@#$%^&*()"",""n"":""test-n-value"",""e"":""AQAB""}")] + [TestCase(@"{""kid"":""test-kid"",""kty"":""RSA"",""d"":""data-with-quotes-\""and\""-backslashes-\\"",""n"":""test-n-value"",""e"":""AQAB""}")] + [TestCase("{\\\"kid\\\":\\\"test-kid\\\",\\\"kty\\\":\\\"RSA\\\",\\\"d\\\":\\\"test-d-value\\\",\\\"n\\\":\\\"test-n-value\\\",\\\"e\\\":\\\"AQAB\\\"}")] public async Task ReadClientSecretExpiration_ValidDirectJwkArgument_ExitCode0(string jwk) { var fakeLogProvider = new FakeLoggerProvider(); @@ -40,7 +42,7 @@ public async Task ReadClientSecretExpiration_ValidDirectJwkArgument_ExitCode0(st using (Assert.EnterMultipleScope()) { - Assert.That(exitCode, Is.EqualTo(0)); + Assert.That(exitCode, Is.Zero); Assert.That(fakeLogProvider.Collector?.LatestRecord.Message, Does.Contain(((DateTimeOffset)clientSecrets.FirstOrDefault()!.Expiration!).ToUnixTimeSeconds().ToString())); var logs = fakeLogProvider.Collector?.GetSnapshot().Select(x => x.Message).ToList(); Assert.That(logs!, Does.Contain("Kid: test-kid")); From a1d4dd257e876b475477dc7f6671198d7e79f93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn-Vegard=20Thoresen?= Date: Mon, 26 Jan 2026 09:22:55 +0100 Subject: [PATCH 2/3] renames method to UnescapeJson --- .../Commands/Extensions/CommandHandlerExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs b/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs index d3660b7..73d04ee 100644 --- a/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs +++ b/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs @@ -25,7 +25,7 @@ public static string ResolveKeyValuePathOrString( if (!string.IsNullOrWhiteSpace(directValue)) { logger.LogInformation("{keyLabel} provided directly.", keyLabel); - return UnescapeJsonIfNeeded(directValue.Trim()); + return UnescapeJson(directValue.Trim()); } if (!string.IsNullOrWhiteSpace(filePath)) @@ -44,7 +44,7 @@ public static string ResolveKeyValuePathOrString( /// Does NOT unescape valid JSON that contains escaped chars within values, /// e.g., {"d":"quote-\"here"} stays unchanged. /// - private static string UnescapeJsonIfNeeded(string input) + private static string UnescapeJson(string input) { // Check for {\" pattern (shell-escaped opening brace+quote) if (input.StartsWith("{\\\"")) From 83b3d86524e607a396385618f7d96f5ddfa8fa9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn-Vegard=20Thoresen?= Date: Thu, 5 Feb 2026 08:18:24 +0100 Subject: [PATCH 3/3] =?UTF-8?q?h=C3=A5ndterer=20json=20escaping=20med=20al?= =?UTF-8?q?le=20kjente=20enkodinger?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Extensions/CommandHandlerExtensions.cs | 19 +++++++++---------- .../ReadClientSecretExpirationTests.cs | 7 +++++++ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs b/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs index 73d04ee..5439bf0 100644 --- a/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs +++ b/Fhi.HelseId/src/Fhi.HelseIdSelvbetjening.CLI/Commands/Extensions/CommandHandlerExtensions.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using Fhi.HelseIdSelvbetjening.CLI.Services; using Microsoft.Extensions.Logging; @@ -39,21 +40,19 @@ public static string ResolveKeyValuePathOrString( } /// - /// Unescapes terminal/shell-escaped JSON where the entire structure has escaped quotes. - /// Handles input like: {\"kid\":\"test\"} -> {"kid":"test"} - /// Does NOT unescape valid JSON that contains escaped chars within values, - /// e.g., {"d":"quote-\"here"} stays unchanged. + /// Unescapes JSON string escape sequences by deserializing the input as a JSON string value. + /// /// private static string UnescapeJson(string input) { - // Check for {\" pattern (shell-escaped opening brace+quote) - if (input.StartsWith("{\\\"")) + try { - return input - .Replace("\\\"", "\"") - .Replace("\\\\", "\\"); + return JsonSerializer.Deserialize($"\"{input}\"")!; + } + catch (JsonException) + { + return input; } - return input; } } } \ No newline at end of file diff --git a/Fhi.HelseId/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs b/Fhi.HelseId/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs index d519c85..de7c6c0 100644 --- a/Fhi.HelseId/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs +++ b/Fhi.HelseId/tests/Fhi.HelseIdSelvbetjening.CLI.Tests/IntegrationTests/ReadClientSecretExpirationTests.cs @@ -11,12 +11,19 @@ namespace Fhi.HelseIdSelvbetjening.CLI.IntegrationTests { public class ReadClientSecretExpirationTests { + /// + /// Unicode escape sequence for the double quote character ("). + /// The first JWKs from HelseId contained unicode escaped quotes. + /// + private const string UnicodeEscapedQuote = "\\u0022"; + [TestCase("{\n \"kid\": \"test-kid\",\n \"kty\": \"RSA\",\n \"d\": \"test-d-value\",\n \"n\": \"test-n-value\",\n \"e\": \"AQAB\"\n}")] [TestCase("{\"d\":\"test-kid\",\"e\":\"AQAB\",\"kid\":\"test-kid\",\"kty\":\"RSA\",\"n\":\"test-n-value\"}")] [TestCase("{ \"kid\": \"test-kid\", \"kty\": \"RSA\", \"d\": \"test-data\", \"n\": \"test-modulus\", \"e\": \"AQAB\" }")] [TestCase(@"{""kid"":""test-kid"",""kty"":""RSA"",""d"":""test-with-special-chars-!@#$%^&*()"",""n"":""test-n-value"",""e"":""AQAB""}")] [TestCase(@"{""kid"":""test-kid"",""kty"":""RSA"",""d"":""data-with-quotes-\""and\""-backslashes-\\"",""n"":""test-n-value"",""e"":""AQAB""}")] [TestCase("{\\\"kid\\\":\\\"test-kid\\\",\\\"kty\\\":\\\"RSA\\\",\\\"d\\\":\\\"test-d-value\\\",\\\"n\\\":\\\"test-n-value\\\",\\\"e\\\":\\\"AQAB\\\"}")] + [TestCase("{" + UnicodeEscapedQuote + "kid" + UnicodeEscapedQuote + ":" + UnicodeEscapedQuote + "test-kid" + UnicodeEscapedQuote + "," + UnicodeEscapedQuote + "kty" + UnicodeEscapedQuote + ":" + UnicodeEscapedQuote + "RSA" + UnicodeEscapedQuote + "," + UnicodeEscapedQuote + "d" + UnicodeEscapedQuote + ":" + UnicodeEscapedQuote + "test-d-value" + UnicodeEscapedQuote + "," + UnicodeEscapedQuote + "n" + UnicodeEscapedQuote + ":" + UnicodeEscapedQuote + "test-n-value" + UnicodeEscapedQuote + "," + UnicodeEscapedQuote + "e" + UnicodeEscapedQuote + ":" + UnicodeEscapedQuote + "AQAB" + UnicodeEscapedQuote + "}")] public async Task ReadClientSecretExpiration_ValidDirectJwkArgument_ExitCode0(string jwk) { var fakeLogProvider = new FakeLoggerProvider();