diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index b4f4ae4b..826d194a 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1524,7 +1524,7 @@ Reference the 'OpenIddict.Client.SystemNetHttp' package and call 'services.AddOp The token was rejected by the remote authentication server. - The '{0}' claim is malformed or isn't of the expected type. + The '{0}' parameter is malformed or isn't of the expected type. An introspection response containing a malformed issuer was returned. diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs index 5ca81d9d..59693226 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs @@ -5,6 +5,7 @@ */ using System.Collections.Immutable; +using System.Text.Json; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Client; @@ -18,6 +19,7 @@ public static partial class OpenIddictClientHandlers * Configuration response handling: */ HandleErrorResponse.Descriptor, + ValidateWellKnownConfigurationParameters.Descriptor, ValidateIssuer.Descriptor, ExtractAuthorizationEndpoint.Descriptor, ExtractCryptographyEndpoint.Descriptor, @@ -35,8 +37,92 @@ public static partial class OpenIddictClientHandlers * Cryptography response handling: */ HandleErrorResponse.Descriptor, + ValidateWellKnownCryptographyParameters.Descriptor, ExtractSigningKeys.Descriptor); + /// + /// Contains the logic responsible for validating the well-known parameters contained in the configuration response. + /// + public class ValidateWellKnownConfigurationParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context!!) + { + foreach (var parameter in context.Response.GetParameters()) + { + if (!ValidateParameterType(parameter.Key, parameter.Value)) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2107(parameter.Key), + uri: SR.FormatID8000(SR.ID2107)); + + return default; + } + } + + return default; + + // Note: in the typical case, the response parameters should be deserialized from a + // JSON response and thus natively stored as System.Text.Json.JsonElement instances. + // + // In the rare cases where the underlying value wouldn't be a JsonElement instance + // (e.g when custom parameters are manually added to the response), the static + // conversion operator would take care of converting the underlying value to a + // JsonElement instance using the same value type as the original parameter value. + static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch + { + // The following parameters MUST be formatted as unique strings: + Metadata.AuthorizationEndpoint or + Metadata.Issuer or + Metadata.JwksUri or + Metadata.TokenEndpoint or + Metadata.UserinfoEndpoint + => ((JsonElement) value).ValueKind is JsonValueKind.String, + + // The following parameters MUST be formatted as arrays of strings: + Metadata.CodeChallengeMethodsSupported or + Metadata.GrantTypesSupported or + Metadata.ResponseModesSupported or + Metadata.ResponseTypesSupported or + Metadata.ScopesSupported or + Metadata.TokenEndpointAuthMethodsSupported + => ((JsonElement) value) is JsonElement element && + element.ValueKind is JsonValueKind.Array && ValidateStringArray(element), + + // The following parameters MUST be formatted as booleans: + Metadata.AuthorizationResponseIssParameterSupported + => ((JsonElement) value).ValueKind is JsonValueKind.True or JsonValueKind.False, + + // 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; + } + } + } + /// /// Contains the logic responsible for extracting the issuer from the discovery document. /// @@ -48,7 +134,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(ValidateWellKnownConfigurationParameters.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -503,6 +589,71 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for validating the well-known parameters contained in the JWKS response. + /// + public class ValidateWellKnownCryptographyParameters : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleCryptographyResponseContext context!!) + { + foreach (var parameter in context.Response.GetParameters()) + { + if (!ValidateParameterType(parameter.Key, parameter.Value)) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2107(parameter.Key), + uri: SR.FormatID8000(SR.ID2107)); + + return default; + } + } + + return default; + + // Note: in the typical case, the response parameters should be deserialized from a + // JSON response and thus natively stored as System.Text.Json.JsonElement instances. + // + // In the rare cases where the underlying value wouldn't be a JsonElement instance + // (e.g when custom parameters are manually added to the response), the static + // conversion operator would take care of converting the underlying value to a + // JsonElement instance using the same value type as the original parameter value. + static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch + { + // The following parameters MUST be formatted as arrays of objects: + JsonWebKeySetParameterNames.Keys => ((JsonElement) value) is JsonElement element && + element.ValueKind is JsonValueKind.Array && ValidateObjectArray(element), + + // Parameters that are not in the well-known list can be of any type. + _ => true + }; + + static bool ValidateObjectArray(JsonElement element) + { + foreach (var item in element.EnumerateArray()) + { + if (item.ValueKind is not JsonValueKind.Object) + { + return false; + } + } + + return true; + } + } + } + /// /// Contains the logic responsible for extracting the signing keys from the JWKS document. /// @@ -514,7 +665,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(ValidateWellKnownCryptographyParameters.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs index dddceb7d..4393df4c 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs @@ -40,17 +40,15 @@ public static partial class OpenIddictClientHandlers { foreach (var parameter in context.Response.GetParameters()) { - if (ValidateParameterType(parameter.Key, parameter.Value)) + if (!ValidateParameterType(parameter.Key, parameter.Value)) { - continue; - } - - context.Reject( - error: Errors.ServerError, - description: SR.FormatID2107(parameter.Key), - uri: SR.FormatID8000(SR.ID2107)); + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2107(parameter.Key), + uri: SR.FormatID8000(SR.ID2107)); - return default; + return default; + } } return default; @@ -64,11 +62,11 @@ public static partial class OpenIddictClientHandlers // JsonElement instance using the same value type as the original parameter value. static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch { - // The 'access_token', 'id_token' and 'refresh_token' parameters MUST be formatted as unique strings. + // The following parameters MUST be formatted as unique strings: Parameters.AccessToken or Parameters.IdToken or Parameters.RefreshToken => ((JsonElement) value).ValueKind is JsonValueKind.String, - // The 'expires_in' parameter MUST be formatted as a numeric date value. + // The following parameters MUST be formatted as numeric dates: Parameters.ExpiresIn => ((JsonElement) value).ValueKind is JsonValueKind.Number, // Parameters that are not in the well-known list can be of any type. diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs index 19f8e9cc..fb5005ce 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs @@ -49,17 +49,15 @@ public static partial class OpenIddictClientHandlers foreach (var parameter in context.Response.GetParameters()) { - if (ValidateClaimType(parameter.Key, parameter.Value)) + if (!ValidateParameterType(parameter.Key, parameter.Value)) { - continue; - } - - context.Reject( - error: Errors.ServerError, - description: SR.FormatID2107(parameter.Key), - uri: SR.FormatID8000(SR.ID2107)); + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2107(parameter.Key), + uri: SR.FormatID8000(SR.ID2107)); - return default; + return default; + } } return default; @@ -71,9 +69,9 @@ public static partial class OpenIddictClientHandlers // (e.g when custom parameters are manually added to the response), the static // conversion operator would take care of converting the underlying value to a // JsonElement instance using the same value type as the original parameter value. - static bool ValidateClaimType(string name, OpenIddictParameter value) => name switch + static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch { - // The 'sub' parameter MUST be formatted as a unique string value. + // The following parameters MUST be formatted as unique strings: Claims.Subject => ((JsonElement) value).ValueKind is JsonValueKind.String, // Parameters that are not in the well-known list can be of any type. diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs index df8f2940..e291a0ec 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs @@ -5,6 +5,7 @@ */ using System.Collections.Immutable; +using System.Text.Json; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Validation; @@ -18,6 +19,7 @@ public static partial class OpenIddictValidationHandlers * Configuration response handling: */ HandleErrorResponse.Descriptor, + ValidateWellKnownConfigurationParameters.Descriptor, ValidateIssuer.Descriptor, ExtractCryptographyEndpoint.Descriptor, ExtractIntrospectionEndpoint.Descriptor, @@ -27,8 +29,80 @@ public static partial class OpenIddictValidationHandlers * Cryptography response handling: */ HandleErrorResponse.Descriptor, + ValidateWellKnownCryptographyParameters.Descriptor, ExtractSigningKeys.Descriptor); + /// + /// Contains the logic responsible for validating the well-known parameters contained in the configuration response. + /// + public class ValidateWellKnownConfigurationParameters : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context!!) + { + foreach (var parameter in context.Response.GetParameters()) + { + if (!ValidateParameterType(parameter.Key, parameter.Value)) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2107(parameter.Key), + uri: SR.FormatID8000(SR.ID2107)); + + return default; + } + } + + return default; + + // Note: in the typical case, the response parameters should be deserialized from a + // JSON response and thus natively stored as System.Text.Json.JsonElement instances. + // + // In the rare cases where the underlying value wouldn't be a JsonElement instance + // (e.g when custom parameters are manually added to the response), the static + // conversion operator would take care of converting the underlying value to a + // JsonElement instance using the same value type as the original parameter value. + static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch + { + // The following parameters MUST be formatted as unique strings: + Metadata.IntrospectionEndpoint or + Metadata.Issuer + => ((JsonElement) value).ValueKind is JsonValueKind.String, + + // The following parameters MUST be formatted as arrays of strings: + Metadata.IntrospectionEndpointAuthMethodsSupported + => ((JsonElement) value) is JsonElement element && + element.ValueKind is JsonValueKind.Array && ValidateStringArray(element), + + // 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; + } + } + } + /// /// Contains the logic responsible for extracting the issuer from the discovery document. /// @@ -40,7 +114,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(ValidateWellKnownConfigurationParameters.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -199,6 +273,71 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible for validating the well-known parameters contained in the JWKS response. + /// + public class ValidateWellKnownCryptographyParameters : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleCryptographyResponseContext context!!) + { + foreach (var parameter in context.Response.GetParameters()) + { + if (!ValidateParameterType(parameter.Key, parameter.Value)) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2107(parameter.Key), + uri: SR.FormatID8000(SR.ID2107)); + + return default; + } + } + + return default; + + // Note: in the typical case, the response parameters should be deserialized from a + // JSON response and thus natively stored as System.Text.Json.JsonElement instances. + // + // In the rare cases where the underlying value wouldn't be a JsonElement instance + // (e.g when custom parameters are manually added to the response), the static + // conversion operator would take care of converting the underlying value to a + // JsonElement instance using the same value type as the original parameter value. + static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch + { + // The following parameters MUST be formatted as arrays of objects: + JsonWebKeySetParameterNames.Keys => ((JsonElement) value) is JsonElement element && + element.ValueKind is JsonValueKind.Array && ValidateObjectArray(element), + + // Parameters that are not in the well-known list can be of any type. + _ => true + }; + + static bool ValidateObjectArray(JsonElement element) + { + foreach (var item in element.EnumerateArray()) + { + if (item.ValueKind is not JsonValueKind.Object) + { + return false; + } + } + + return true; + } + } + } + /// /// Contains the logic responsible for extracting the signing keys from the JWKS document. /// @@ -210,7 +349,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(ValidateWellKnownCryptographyParameters.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs index 4fcdd479..e49de34e 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs @@ -26,8 +26,8 @@ public static partial class OpenIddictValidationHandlers * Introspection response handling: */ HandleErrorResponse.Descriptor, + ValidateWellKnownParameters.Descriptor, HandleInactiveResponse.Descriptor, - ValidateWellKnownClaims.Descriptor, ValidateIssuer.Descriptor, ValidateTokenUsage.Descriptor, PopulateClaims.Descriptor); @@ -83,85 +83,34 @@ public static partial class OpenIddictValidationHandlers } /// - /// Contains the logic responsible for extracting the active: false marker from the response. + /// Contains the logic responsible for validating the well-known parameters contained in the introspection response. /// - public class HandleInactiveResponse : IOpenIddictValidationHandler + public class ValidateWellKnownParameters : IOpenIddictValidationHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); - /// - public ValueTask HandleAsync(HandleIntrospectionResponseContext context!!) - { - // Note: the introspection specification requires that server return "active: false" instead of a proper - // OAuth 2.0 error when the token is invalid, expired, revoked or invalid for any other reason. - // While OpenIddict's server can be tweaked to return a proper error (by removing NormalizeErrorResponse) - // from the enabled handlers, supporting "active: false" is required to ensure total compatibility. - - if (!context.Response.TryGetParameter(Parameters.Active, out var parameter)) - { - context.Reject( - error: Errors.ServerError, - description: SR.FormatID2105(Parameters.Active), - uri: SR.FormatID8000(SR.ID2105)); - - return default; - } - - // Note: if the parameter cannot be converted to a boolean instance, the default value - // (false) is returned by the static operator, which is appropriate for this check. - if (!(bool) parameter) - { - context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2106), - uri: SR.FormatID8000(SR.ID2106)); - - return default; - } - - return default; - } - } - - /// - /// Contains the logic responsible for validating the well-known claims contained in the introspection response. - /// - public class ValidateWellKnownClaims : IOpenIddictValidationHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() - .SetOrder(HandleInactiveResponse.Descriptor.Order + 1_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - /// public ValueTask HandleAsync(HandleIntrospectionResponseContext context!!) { foreach (var parameter in context.Response.GetParameters()) { - if (ValidateClaimType(parameter.Key, parameter.Value)) + if (!ValidateParameterType(parameter.Key, parameter.Value)) { - continue; - } - - context.Reject( - error: Errors.ServerError, - description: SR.FormatID2107(parameter.Key), - uri: SR.FormatID8000(SR.ID2107)); + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2107(parameter.Key), + uri: SR.FormatID8000(SR.ID2107)); - return default; + return default; + } } return default; @@ -173,24 +122,27 @@ public static partial class OpenIddictValidationHandlers // (e.g when custom parameters are manually added to the response), the static // conversion operator would take care of converting the underlying value to a // JsonElement instance using the same value type as the original parameter value. - static bool ValidateClaimType(string name, OpenIddictParameter value) => name switch + static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch { - // The 'jti', 'iss', 'scope' and 'token_usage' claims MUST be formatted as a unique string. + // The following parameters MUST be formatted as booleans: + Claims.Active => ((JsonElement) value).ValueKind is JsonValueKind.True or JsonValueKind.False, + + // The following parameters MUST be formatted as unique strings: Claims.JwtId or Claims.Issuer or Claims.Scope or Claims.TokenUsage => ((JsonElement) value).ValueKind is JsonValueKind.String, - // The 'aud' claim MUST be represented either as a unique string or as an array of strings. + // The following parameters MUST be formatted as strings or arrays of strings: // // 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)), - // The 'exp', 'iat' and 'nbf' claims MUST be formatted as numeric date values. + // The following parameters MUST be formatted as numeric dates: Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore => ((JsonElement) value).ValueKind is JsonValueKind.Number, - // Claims that are not in the well-known list can be of any type. + // Parameters that are not in the well-known list can be of any type. _ => true }; @@ -209,6 +161,54 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible for extracting the active: false marker from the response. + /// + public class HandleInactiveResponse : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleIntrospectionResponseContext context!!) + { + // Note: the introspection specification requires that server return "active: false" instead of a proper + // OAuth 2.0 error when the token is invalid, expired, revoked or invalid for any other reason. + // While OpenIddict's server can be tweaked to return a proper error (by removing NormalizeErrorResponse) + // from the enabled handlers, supporting "active: false" is required to ensure total compatibility. + + var active = (bool?) context.Response[Parameters.Active]; + if (active is null) + { + context.Reject( + error: Errors.ServerError, + description: SR.FormatID2105(Parameters.Active), + uri: SR.FormatID8000(SR.ID2105)); + + return default; + } + + if (active is not true) + { + context.Reject( + error: Errors.InvalidToken, + description: SR.GetResourceString(SR.ID2106), + uri: SR.FormatID8000(SR.ID2106)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting the issuer from the introspection response. /// @@ -220,7 +220,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateWellKnownClaims.Descriptor.Order + 1_000) + .SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build();