mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Do not escape '+' character in x5c of jwks
This commit is contained in:
parent
27d0d4a9ad
commit
2e704d9386
4 changed files with 97 additions and 1 deletions
|
|
@ -52,6 +52,8 @@ internal class JsonWebKeysHttpWriter : IHttpResponseWriter<JsonWebKeysResult>
|
|||
context.Response.SetCache(result.MaxAge.Value, "Origin");
|
||||
}
|
||||
|
||||
return context.Response.WriteJsonAsync(new { keys = result.WebKeys }, "application/json; charset=UTF-8");
|
||||
var json = ObjectSerializer.ToUnescapedString(new { keys = result.WebKeys });
|
||||
|
||||
return context.Response.WriteJsonAsync(json, "application/json; charset=UTF-8");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
// See LICENSE in the project root for license information.
|
||||
|
||||
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
|
|
@ -14,7 +15,28 @@ internal static class ObjectSerializer
|
|||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions OptionsWithoutEscaping = new()
|
||||
{
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
// Use UnsafeRelaxedJsonEscaping to avoid escaping '+' as '\u002B' in base64-encoded
|
||||
// values like x5c certificates. The '+' character is valid in JSON strings and does
|
||||
// not need to be escaped. The default encoder escapes it for HTML safety, but our
|
||||
// JSON responses are served with application/json content type.
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an object to a JSON string using default encoding, which escapes
|
||||
/// certain characters (such as '+') for HTML safety.
|
||||
/// </summary>
|
||||
public static string ToString(object o) => JsonSerializer.Serialize(o, Options);
|
||||
|
||||
/// <summary>
|
||||
/// Serializes an object to a JSON string using relaxed encoding that does not
|
||||
/// escape characters like '+'. This is useful for producing JSON where
|
||||
/// base64-encoded values (e.g., x5c certificates) should remain unescaped.
|
||||
/// </summary>
|
||||
public static string ToUnescapedString(object o) => JsonSerializer.Serialize(o, OptionsWithoutEscaping);
|
||||
|
||||
public static T FromString<T>(string value) => JsonSerializer.Deserialize<T>(value, Options);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -286,6 +286,62 @@ public class DiscoveryEndpointTests
|
|||
jwks.Keys.ShouldContain(x => x.KeyId == rsaKey.KeyId && x.Alg == "RS256");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task Jwks_x5c_should_not_escape_plus_character()
|
||||
{
|
||||
var cert = TestCert.Load();
|
||||
|
||||
var pipeline = new IdentityServerPipeline();
|
||||
pipeline.OnPostConfigureServices += services =>
|
||||
{
|
||||
services.AddIdentityServerBuilder()
|
||||
.AddSigningCredential(cert);
|
||||
};
|
||||
pipeline.Initialize();
|
||||
|
||||
var result = await pipeline.BackChannelClient.GetAsync("https://server/.well-known/openid-configuration/jwks");
|
||||
var json = await result.Content.ReadAsStringAsync();
|
||||
|
||||
// The x5c property contains base64-encoded certificate data which commonly has '+' characters.
|
||||
// These should not be escaped as \u002B in the JSON response.
|
||||
json.ShouldNotContain("\\u002B");
|
||||
json.ShouldContain('+');
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task Jwks_x5t_should_not_escape_base64url_encoded_characters()
|
||||
{
|
||||
var cert = TestCert.Load();
|
||||
|
||||
var pipeline = new IdentityServerPipeline();
|
||||
pipeline.OnPostConfigureServices += services =>
|
||||
{
|
||||
services.AddIdentityServerBuilder()
|
||||
.AddSigningCredential(cert);
|
||||
};
|
||||
pipeline.Initialize();
|
||||
|
||||
var result = await pipeline.BackChannelClient.GetAsync("https://server/.well-known/openid-configuration/jwks");
|
||||
var json = await result.Content.ReadAsStringAsync();
|
||||
var data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json);
|
||||
|
||||
var keys = data["keys"].EnumerateArray().ToList();
|
||||
var keyWithX5t = keys.First(k => k.TryGetProperty("x5t", out _));
|
||||
var x5t = keyWithX5t.GetProperty("x5t").GetString();
|
||||
|
||||
// The x5t property is a base64url-encoded SHA-1 thumbprint (per RFC 7517).
|
||||
// Base64url encoding uses '-' and '_' instead of '+' and '/', so '+' and '/' must not appear.
|
||||
x5t.ShouldNotContain("+");
|
||||
x5t.ShouldNotContain("/");
|
||||
x5t.ShouldContain("_"); // The cert we are using happens to contain '_' but not '-' in its thumbprint
|
||||
|
||||
// Verify the value matches the expected base64url-encoded thumbprint
|
||||
var expectedThumbprint = Base64UrlEncoder.Encode(cert.GetCertHash());
|
||||
x5t.ShouldBe(expectedThumbprint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", Category)]
|
||||
public async Task Unicode_values_in_url_should_be_processed_correctly()
|
||||
|
|
|
|||
|
|
@ -37,4 +37,20 @@ public class ObjectSerializerTests
|
|||
|
||||
result.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Can_serialize_jwk_with_plus_character_in_x5c()
|
||||
{
|
||||
var jwk = new Dictionary<string, object>
|
||||
{
|
||||
{ "kty", "RSA" },
|
||||
{ "x5c", new List<string> { "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA+test+value+with+plus" } }
|
||||
};
|
||||
|
||||
var json = Duende.IdentityServer.ObjectSerializer.ToUnescapedString(jwk);
|
||||
|
||||
// The '+' character should not be escaped as \u002B
|
||||
json.ShouldNotContain("\\u002B");
|
||||
json.ShouldContain("+");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue