diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs index 4431087e..c28299c0 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Protection.cs @@ -1384,6 +1384,8 @@ public static partial class OpenIddictServerHandlers Claims.Private.Scope when context.TokenType is TokenTypeHints.AccessToken => false, + Claims.AuthenticationMethodReference when context.TokenType is TokenTypeHints.IdToken => false, + _ => true }); @@ -1398,10 +1400,27 @@ public static partial class OpenIddictServerHandlers var audiences = context.Principal.GetAudiences(); if (audiences.Any()) { - claims.Add(Claims.Audience, audiences.Length switch + claims.Add(Claims.Audience, audiences switch + { + [string audience] => audience, + _ => audiences.ToArray() + }); + } + } + + // Note: unlike other claims (e.g "aud"), the "amr" claim MUST be represented as a unique + // claim representing a JSON array, even if a single authentication method reference is + // present in the collection. To ensure an array is always returned, the "amr" claim is + // filtered out from the clone principal and manually added as a "string[]" claim value. + if (context.TokenType is TokenTypeHints.IdToken) + { + var methods = context.Principal.GetClaims(Claims.AuthenticationMethodReference); + if (methods.Any()) + { + claims.Add(Claims.AuthenticationMethodReference, methods switch { - 1 => audiences.ElementAt(0), - _ => audiences + [string method] => [method], + _ => methods.ToArray() }); } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index e7047ba3..27fecf6e 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -2216,8 +2216,7 @@ public static partial class OpenIddictServerHandlers => values is [{ ValueType: ClaimValueTypes.String }], // 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 + Claims.Private.Audience or Claims.Private.Presenter or Claims.Private.Resource => 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. @@ -2225,6 +2224,17 @@ public static partial class OpenIddictServerHandlers JsonSerializer.Deserialize(value) is { ValueKind: JsonValueKind.Array } element && OpenIddictHelpers.ValidateArrayElements(element, JsonValueKind.String)), + // Note: unlike other claims (e.g "aud"), the "amr" claim MUST be represented as a unique + // claim representing a JSON array, even if a single authentication method reference + // is present in the collection. To avoid forcing users to use the special JSON_ARRAY + // value type, string values are also allowed here and normalized to JSON arrays + // by OpenIddict when generating an identity token based on the specified principal. + Claims.AuthenticationMethodReference + => values.TrueForAll(static value => value.ValueType is ClaimValueTypes.String) || + (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 Claims.Private.DeviceCodeLifetime or Claims.Private.IdentityTokenLifetime or