diff --git a/shared/OpenIddict.Extensions/OpenIddictHelpers.cs b/shared/OpenIddict.Extensions/OpenIddictHelpers.cs
index 49469fb3..a23c2048 100644
--- a/shared/OpenIddict.Extensions/OpenIddictHelpers.cs
+++ b/shared/OpenIddict.Extensions/OpenIddictHelpers.cs
@@ -6,6 +6,7 @@ using System.Globalization;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
+using System.Text.Json;
using Microsoft.Extensions.Primitives;
namespace OpenIddict.Extensions;
@@ -1032,4 +1033,32 @@ internal static class OpenIddictHelpers
return false;
}
#endif
+
+ ///
+ /// Determines whether the items contained in
+ /// are of the specified .
+ ///
+ /// The .
+ /// The expected .
+ ///
+ /// if the array doesn't contain any value or if all the items
+ /// are of the specified , otherwise.
+ ///
+ public static bool ValidateArrayElements(JsonElement element, JsonValueKind kind)
+ {
+ if (element.ValueKind is not JsonValueKind.Array)
+ {
+ throw new ArgumentOutOfRangeException(nameof(element));
+ }
+
+ foreach (var item in element.EnumerateArray())
+ {
+ if (item.ValueKind != kind)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
}
diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
index c81d09bb..cc42c279 100644
--- a/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
+++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictExtensions.cs
@@ -1579,7 +1579,37 @@ public static class OpenIddictExtensions
throw new ArgumentException(SR.GetResourceString(SR.ID0184), nameof(type));
}
- return identity.FindAll(type).Select(claim => claim.Value).Distinct(StringComparer.Ordinal).ToImmutableArray();
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var claim in identity.FindAll(type))
+ {
+ // If the claim uses the special JSON_ARRAY claim value type, parse it to extract its individual
+ // values. When the individual values are not strings, their string representation is returned.
+ if (claim.ValueType is "JSON_ARRAY")
+ {
+ var element = JsonSerializer.Deserialize(claim.Value);
+ if (element.ValueKind is not JsonValueKind.Array)
+ {
+ continue;
+ }
+
+ foreach (var item in element.EnumerateArray())
+ {
+ var value = item.ToString();
+ if (!builder.Contains(value))
+ {
+ builder.Add(value);
+ }
+ }
+ }
+
+ else if (!builder.Contains(claim.Value))
+ {
+ builder.Add(claim.Value);
+ }
+ }
+
+ return builder.ToImmutable();
}
///
@@ -1600,7 +1630,37 @@ public static class OpenIddictExtensions
throw new ArgumentException(SR.GetResourceString(SR.ID0184), nameof(type));
}
- return principal.FindAll(type).Select(claim => claim.Value).Distinct(StringComparer.Ordinal).ToImmutableArray();
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var claim in principal.FindAll(type))
+ {
+ // If the claim uses the special JSON_ARRAY claim value type, parse it to extract its individual
+ // values. When the individual values are not strings, their string representation is returned.
+ if (claim.ValueType is "JSON_ARRAY")
+ {
+ var element = JsonSerializer.Deserialize(claim.Value);
+ if (element.ValueKind is not JsonValueKind.Array)
+ {
+ continue;
+ }
+
+ foreach (var item in element.EnumerateArray())
+ {
+ var value = item.ToString();
+ if (!builder.Contains(value))
+ {
+ builder.Add(value);
+ }
+ }
+ }
+
+ else if (!builder.Contains(claim.Value))
+ {
+ builder.Add(claim.Value);
+ }
+ }
+
+ return builder.ToImmutable();
}
///
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
index 7bb0fbff..17532908 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs
@@ -120,7 +120,8 @@ public static partial class OpenIddictClientHandlers
Metadata.ScopesSupported or
Metadata.TokenEndpointAuthMethodsSupported
=> ((JsonElement) value) is JsonElement element &&
- element.ValueKind is JsonValueKind.Array && ValidateStringArray(element),
+ element.ValueKind is JsonValueKind.Array &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String),
// The following parameters MUST be formatted as booleans:
Metadata.AuthorizationResponseIssParameterSupported
@@ -129,19 +130,6 @@ public static partial class OpenIddictClientHandlers
// Parameters that are not in the well-known list can be of any type.
_ => true
};
-
- static bool ValidateStringArray(JsonElement element)
- {
- foreach (var item in element.EnumerateArray())
- {
- if (item.ValueKind is not JsonValueKind.String)
- {
- return false;
- }
- }
-
- return true;
- }
}
}
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs
index 7030313f..76482844 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Introspection.cs
@@ -10,6 +10,7 @@ using System.Globalization;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
+using OpenIddict.Extensions;
namespace OpenIddict.Client;
@@ -94,7 +95,8 @@ public static partial class OpenIddictClientHandlers
// Note: empty arrays and arrays that contain a single value are also considered valid.
Claims.Audience => ((JsonElement) value) is JsonElement element &&
element.ValueKind is JsonValueKind.String ||
- (element.ValueKind is JsonValueKind.Array && ValidateStringArray(element)),
+ (element.ValueKind is JsonValueKind.Array &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
// The following claims MUST be formatted as numeric dates:
Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
@@ -104,19 +106,6 @@ public static partial class OpenIddictClientHandlers
// Claims that are not in the well-known list can be of any type.
_ => true
};
-
- static bool ValidateStringArray(JsonElement element)
- {
- foreach (var item in element.EnumerateArray())
- {
- if (item.ValueKind is not JsonValueKind.String)
- {
- return false;
- }
- }
-
- return true;
- }
}
}
diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
index 22a6ee05..998f43dc 100644
--- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs
+++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs
@@ -13,6 +13,7 @@ using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
+using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
using static OpenIddict.Abstractions.OpenIddictExceptions;
@@ -1734,7 +1735,12 @@ public static partial class OpenIddictClientHandlers
// The following claims MUST be represented as unique strings or array of strings.
Claims.Audience or Claims.AuthenticationMethodReference
- => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String) ||
+ // Note: a unique claim using the special JSON_ARRAY claim value type is allowed
+ // if the individual elements of the parsed JSON array are all string values.
+ (values is [{ ValueType: JsonClaimValueTypes.JsonArray, Value: string value }] &&
+ JsonSerializer.Deserialize(value) is { ValueKind: JsonValueKind.Array } element &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
// The following claims MUST be represented as unique numeric dates.
Claims.AuthenticationTime or Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
@@ -3079,7 +3085,12 @@ public static partial class OpenIddictClientHandlers
// The following claims MUST be represented as unique strings or array of strings.
Claims.Audience or Claims.AuthenticationMethodReference
- => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String) ||
+ // Note: a unique claim using the special JSON_ARRAY claim value type is allowed
+ // if the individual elements of the parsed JSON array are all string values.
+ (values is [{ ValueType: JsonClaimValueTypes.JsonArray, Value: string value }] &&
+ JsonSerializer.Deserialize(value) is { ValueKind: JsonValueKind.Array } element &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
// The following claims MUST be represented as unique numeric dates.
Claims.AuthenticationTime or Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
@@ -4271,7 +4282,12 @@ public static partial class OpenIddictClientHandlers
{
// The following claims MUST be represented as unique strings or array of strings.
Claims.Private.Audience or Claims.Private.Resource or Claims.Private.Presenter
- => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String) ||
+ // Note: a unique claim using the special JSON_ARRAY claim value type is allowed
+ // if the individual elements of the parsed JSON array are all string values.
+ (values is [{ ValueType: JsonClaimValueTypes.JsonArray, Value: string value }] &&
+ JsonSerializer.Deserialize(value) is { ValueKind: JsonValueKind.Array } element &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
// The following claims MUST be represented as unique integers.
Claims.Private.StateTokenLifetime
@@ -7094,7 +7110,12 @@ public static partial class OpenIddictClientHandlers
{
// The following claims MUST be represented as unique strings or array of strings.
Claims.Private.Audience or Claims.Private.Resource or Claims.Private.Presenter
- => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String) ||
+ // Note: a unique claim using the special JSON_ARRAY claim value type is allowed
+ // if the individual elements of the parsed JSON array are all string values.
+ (values is [{ ValueType: JsonClaimValueTypes.JsonArray, Value: string value }] &&
+ JsonSerializer.Deserialize(value) is { ValueKind: JsonValueKind.Array } element &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
// The following claims MUST be represented as unique integers.
Claims.Private.StateTokenLifetime
diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
index 6e939bc5..9df9fd88 100644
--- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs
+++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs
@@ -14,6 +14,7 @@ using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
@@ -698,7 +699,13 @@ public static partial class OpenIddictServerHandlers
=> values is [{ ValueType: ClaimValueTypes.String }],
// The following claims MUST be represented as unique strings or array of strings.
- Claims.Audience => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ Claims.Audience
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String) ||
+ // Note: a unique claim using the special JSON_ARRAY claim value type is allowed
+ // if the individual elements of the parsed JSON array are all string values.
+ (values is [{ ValueType: JsonClaimValueTypes.JsonArray, Value: string value }] &&
+ JsonSerializer.Deserialize(value) is { ValueKind: JsonValueKind.Array } element &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
// The following claims MUST be represented as unique numeric dates.
Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
@@ -2210,7 +2217,12 @@ public static partial class OpenIddictServerHandlers
// The following claims MUST be represented as unique strings or array of strings.
Claims.AuthenticationMethodReference or Claims.Private.Audience or
Claims.Private.Presenter or Claims.Private.Resource
- => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String),
+ => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String) ||
+ // Note: a unique claim using the special JSON_ARRAY claim value type is allowed
+ // if the individual elements of the parsed JSON array are all string values.
+ (values is [{ ValueType: JsonClaimValueTypes.JsonArray, Value: string value }] &&
+ JsonSerializer.Deserialize(value) is { ValueKind: JsonValueKind.Array } element &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
// The following claims MUST be represented as unique integers.
Claims.Private.AccessTokenLifetime or Claims.Private.AuthorizationCodeLifetime or
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs
index 5754e4f7..d6b678ea 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs
@@ -94,24 +94,12 @@ public static partial class OpenIddictValidationHandlers
// The following parameters MUST be formatted as arrays of strings:
Metadata.IntrospectionEndpointAuthMethodsSupported
=> ((JsonElement) value) is JsonElement element &&
- element.ValueKind is JsonValueKind.Array && ValidateStringArray(element),
+ element.ValueKind is JsonValueKind.Array &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String),
// Parameters that are not in the well-known list can be of any type.
_ => true
};
-
- static bool ValidateStringArray(JsonElement element)
- {
- foreach (var item in element.EnumerateArray())
- {
- if (item.ValueKind is not JsonValueKind.String)
- {
- return false;
- }
- }
-
- return true;
- }
}
}
diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
index 2120c188..6c2b4c53 100644
--- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
+++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs
@@ -10,6 +10,7 @@ using System.Globalization;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.Logging;
+using OpenIddict.Extensions;
namespace OpenIddict.Validation;
@@ -94,7 +95,8 @@ public static partial class OpenIddictValidationHandlers
// Note: empty arrays and arrays that contain a single value are also considered valid.
Claims.Audience => ((JsonElement) value) is JsonElement element &&
element.ValueKind is JsonValueKind.String ||
- (element.ValueKind is JsonValueKind.Array && ValidateStringArray(element)),
+ (element.ValueKind is JsonValueKind.Array &&
+ OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)),
// The following claims MUST be formatted as numeric dates:
Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore
@@ -104,19 +106,6 @@ public static partial class OpenIddictValidationHandlers
// Claims that are not in the well-known list can be of any type.
_ => true
};
-
- static bool ValidateStringArray(JsonElement element)
- {
- foreach (var item in element.EnumerateArray())
- {
- if (item.ValueKind is not JsonValueKind.String)
- {
- return false;
- }
- }
-
- return true;
- }
}
}
diff --git a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs
index 3785aaaa..c4a7c25d 100644
--- a/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs
+++ b/test/OpenIddict.Abstractions.Tests/Primitives/OpenIddictExtensionsTests.cs
@@ -2592,6 +2592,44 @@ public class OpenIddictExtensionsTests
Assert.Equal([Scopes.OpenId, Scopes.Profile], principal.GetClaims(Claims.Scope), StringComparer.Ordinal);
}
+ [Fact]
+ public void ClaimsIdentity_GetClaims_ReturnsExpectedResultForJsonArrayClaims()
+ {
+ // Arrange
+ var identity = new ClaimsIdentity();
+ identity.AddClaim(new Claim("array_claim",
+ """["value1", "value2", 42, ["value1", "value2"], {"property": "value"}]""", "JSON_ARRAY"));
+
+ // Act and assert
+ var claims = identity.GetClaims("array_claim");
+ Assert.Equal(5, claims.Length);
+ Assert.Equal("value1", claims[0]);
+ Assert.Equal("value2", claims[1]);
+ Assert.Equal("42", claims[2]);
+ Assert.Equal("""["value1", "value2"]""", claims[3]);
+ Assert.Equal("""{"property": "value"}""", claims[4]);
+ }
+
+ [Fact]
+ public void ClaimsPrincipal_GetClaims_ReturnsExpectedResultForJsonArrayClaims()
+ {
+ // Arrange
+ var identity = new ClaimsIdentity();
+ identity.AddClaim(new Claim("array_claim",
+ """["value1", "value2", 42, ["value1", "value2"], {"property": "value"}]""", "JSON_ARRAY"));
+
+ var principal = new ClaimsPrincipal(identity);
+
+ // Act and assert
+ var claims = principal.GetClaims("array_claim");
+ Assert.Equal(5, claims.Length);
+ Assert.Equal("value1", claims[0]);
+ Assert.Equal("value2", claims[1]);
+ Assert.Equal("42", claims[2]);
+ Assert.Equal("""["value1", "value2"]""", claims[3]);
+ Assert.Equal("""{"property": "value"}""", claims[4]);
+ }
+
[Fact]
public void ClaimsIdentity_HasClaim_ThrowsAnExceptionForNullIdentity()
{
diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
index d2747fc8..3664c3d0 100644
--- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
+++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.cs
@@ -12,6 +12,7 @@ using System.Security.Claims;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.JsonWebTokens;
using Moq;
using OpenIddict.Core;
using Xunit;
@@ -1483,6 +1484,46 @@ public abstract partial class OpenIddictServerIntegrationTests
Assert.Equal(SR.FormatID0424(Claims.Subject), exception.Message);
}
+ [Fact]
+ public async Task ProcessSignIn_ValidClaimValueTypeDoesNotCauseAnException()
+ {
+ // Arrange
+ await using var server = await CreateServerAsync(options =>
+ {
+ options.EnableDegradedMode();
+
+ options.AddEventHandler(builder =>
+ builder.UseInlineHandler(context =>
+ {
+ var identity = new ClaimsIdentity("Bearer")
+ .SetTokenType(TokenTypeHints.AuthorizationCode)
+ .SetPresenters("Fabrikam")
+ .SetClaim(Claims.Subject, "Bob le Bricoleur");
+
+ identity.AddClaim(new Claim(Claims.AuthenticationMethodReference,
+ """["value"]""", JsonClaimValueTypes.JsonArray));
+
+ context.Principal = new ClaimsPrincipal(identity);
+
+ return default;
+ }));
+ });
+
+ await using var client = await server.CreateClientAsync();
+
+ // Act
+ var response = await client.PostAsync("/connect/token", new OpenIddictRequest
+ {
+ GrantType = GrantTypes.Password,
+ Username = "johndoe",
+ Password = "A3ddj3w",
+ Scope = Scopes.OpenId
+ });
+
+ // Assert
+ Assert.NotNull(response.AccessToken);
+ }
+
[Fact]
public async Task ProcessSignIn_ScopeDefaultsToOpenId()
{