diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index bf0fd2e4..61147ea0 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1775,6 +1775,21 @@ Alternatively, you can disable the token storage feature by calling 'services.Ad An unsupported content encoding was returned by the remote server. + + The configuration request was rejected by the remote server. + + + The cryptography request was rejected by the remote server. + + + The introspection request was rejected by the remote server. + + + The token request was rejected by the remote server. + + + The userinfo request was rejected by the remote server. + The '{0}' parameter shouldn't be null or empty at this point. @@ -2392,6 +2407,21 @@ This may indicate that the hashed entry is corrupted or malformed. Client validation failed because '{PostLogoutRedirectUri}' was not a valid post_logout_redirect_uri for {Client}. + + The configuration request was rejected by the remote authorization server: {Response}. + + + The cryptography request was rejected by the remote authorization server: {Response}. + + + The introspection request was rejected by the remote authorization server: {Response}. + + + The token request was rejected by the remote authorization server: {Response}. + + + The userinfo request was rejected by the remote authorization server: {Response}. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs index 27db3d28..8a5cb767 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Discovery.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Text.Json; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Client; @@ -18,8 +19,8 @@ public static partial class OpenIddictClientHandlers /* * Configuration response handling: */ - HandleErrorResponse.Descriptor, ValidateWellKnownConfigurationParameters.Descriptor, + HandleConfigurationErrorResponse.Descriptor, ValidateIssuer.Descriptor, ExtractAuthorizationEndpoint.Descriptor, ExtractCryptographyEndpoint.Descriptor, @@ -37,8 +38,8 @@ public static partial class OpenIddictClientHandlers /* * Cryptography response handling: */ - HandleErrorResponse.Descriptor, ValidateWellKnownCryptographyParameters.Descriptor, + HandleCryptographyErrorResponse.Descriptor, ExtractSigningKeys.Descriptor); /// @@ -52,7 +53,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -88,6 +89,10 @@ 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 { + // Error parameters MUST be formatted as unique strings: + Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri + => ((JsonElement) value).ValueKind is JsonValueKind.String, + // The following parameters MUST be formatted as unique strings: Metadata.AuthorizationEndpoint or Metadata.EndSessionEndpoint or @@ -130,6 +135,49 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for surfacing potential errors from the configuration response. + /// + public class HandleConfigurationErrorResponse : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateWellKnownConfigurationParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the specification doesn't define a standard way to return an error other than + // returning a 4xx status code. That said, some implementations are known to return + // JSON payloads similar to standard errored token responses. For more information, see + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse. + if (!string.IsNullOrEmpty(context.Response.Error)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6203), context.Response); + + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2144), + uri: SR.FormatID8000(SR.ID2144)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting the issuer from the discovery document. /// @@ -141,7 +189,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateWellKnownConfigurationParameters.Descriptor.Order + 1_000) + .SetOrder(HandleConfigurationErrorResponse.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -721,7 +769,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -757,6 +805,10 @@ 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 { + // Error parameters MUST be formatted as unique strings: + Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri + => ((JsonElement) value).ValueKind is JsonValueKind.String, + // The following parameters MUST be formatted as arrays of objects: JsonWebKeySetParameterNames.Keys => ((JsonElement) value) is JsonElement element && element.ValueKind is JsonValueKind.Array && ValidateObjectArray(element), @@ -780,6 +832,49 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for surfacing potential errors from the cryptography response. + /// + public class HandleCryptographyErrorResponse : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateWellKnownCryptographyParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleCryptographyResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the specification doesn't define a standard way to return an error other than + // returning a 4xx status code. That said, some implementations are known to return + // JSON payloads similar to standard errored token responses. For more information, see + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse. + if (!string.IsNullOrEmpty(context.Response.Error)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6204), context.Response); + + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2145), + uri: SR.FormatID8000(SR.ID2145)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting the signing keys from the JWKS document. /// @@ -791,7 +886,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateWellKnownCryptographyParameters.Descriptor.Order + 1_000) + .SetOrder(HandleCryptographyErrorResponse.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 ac9869e8..8e9bb383 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Exchange.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Text.Json; +using Microsoft.Extensions.Logging; namespace OpenIddict.Client; @@ -17,8 +18,8 @@ public static partial class OpenIddictClientHandlers /* * Token response handling: */ - HandleErrorResponse.Descriptor, - ValidateWellKnownParameters.Descriptor); + ValidateWellKnownParameters.Descriptor, + HandleErrorResponse.Descriptor); /// /// Contains the logic responsible for validating the well-known parameters contained in the token response. @@ -67,6 +68,10 @@ 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 { + // Error parameters MUST be formatted as unique strings: + Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri + => ((JsonElement) value).ValueKind is JsonValueKind.String, + // The following parameters MUST be formatted as unique strings: Parameters.AccessToken or Parameters.IdToken or Parameters.RefreshToken => ((JsonElement) value).ValueKind is JsonValueKind.String, @@ -79,5 +84,54 @@ public static partial class OpenIddictClientHandlers }; } } + + /// + /// Contains the logic responsible for surfacing potential errors from the token response. + /// + public class HandleErrorResponse : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleTokenResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // For more information, see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2. + if (!string.IsNullOrEmpty(context.Response.Error)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6206), context.Response); + + context.Reject( + error: context.Response.Error switch + { + Errors.InvalidClient => Errors.InvalidRequest, + Errors.InvalidGrant => Errors.InvalidGrant, + Errors.InvalidScope => Errors.InvalidScope, + Errors.InvalidRequest => Errors.InvalidRequest, + Errors.UnauthorizedClient => Errors.UnauthorizedClient, + Errors.UnsupportedGrantType => Errors.UnsupportedGrantType, + _ => Errors.ServerError + }, + description: SR.GetResourceString(SR.ID2147), + uri: SR.FormatID8000(SR.ID2147)); + + return default; + } + + return default; + } + } } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs index a44eda6d..766c6f56 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Userinfo.cs @@ -7,6 +7,7 @@ using System.Collections.Immutable; using System.Security.Claims; using System.Text.Json; +using Microsoft.Extensions.Logging; namespace OpenIddict.Client; @@ -18,21 +19,21 @@ public static partial class OpenIddictClientHandlers /* * Userinfo response handling: */ - HandleErrorResponse.Descriptor, - ValidateWellKnownClaims.Descriptor, + ValidateWellKnownParameters.Descriptor, + HandleErrorResponse.Descriptor, PopulateClaims.Descriptor); /// /// Contains the logic responsible for validating the well-known parameters contained in the userinfo response. /// - public class ValidateWellKnownClaims : IOpenIddictClientHandler + public class ValidateWellKnownParameters : IOpenIddictClientHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -75,6 +76,10 @@ 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 { + // Error parameters MUST be formatted as unique strings: + Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri + => ((JsonElement) value).ValueKind is JsonValueKind.String, + // The following parameters MUST be formatted as unique strings: Claims.Subject => ((JsonElement) value).ValueKind is JsonValueKind.String, @@ -84,6 +89,52 @@ public static partial class OpenIddictClientHandlers } } + /// + /// Contains the logic responsible for surfacing potential errors from the userinfo response. + /// + public class HandleErrorResponse : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleUserinfoResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // For more information, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoError. + if (!string.IsNullOrEmpty(context.Response.Error)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6207), context.Response); + + context.Reject( + error: context.Response.Error switch + { + Errors.InsufficientScope => Errors.InsufficientScope, + Errors.InvalidRequest => Errors.InvalidToken, + Errors.InvalidToken => Errors.InvalidToken, + _ => Errors.ServerError + }, + description: SR.GetResourceString(SR.ID2148), + uri: SR.FormatID8000(SR.ID2148)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting the claims from the introspection response. /// @@ -95,7 +146,7 @@ public static partial class OpenIddictClientHandlers public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateWellKnownClaims.Descriptor.Order + 1_000) + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index ecb8ccde..a1b6d3a0 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -10,7 +10,9 @@ using System.Diagnostics; using System.Security.Claims; using System.Security.Cryptography; using System.Text; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using static OpenIddict.Abstractions.OpenIddictExceptions; #if !SUPPORTS_TIME_CONSTANT_COMPARISONS using Org.BouncyCastle.Utilities; @@ -60,7 +62,6 @@ public static partial class OpenIddictClientHandlers GenerateClientAssertionToken.Descriptor, AttachTokenRequestClientCredentials.Descriptor, SendTokenRequest.Descriptor, - ValidateTokenErrorParameters.Descriptor, EvaluateValidatedBackchannelTokens.Descriptor, ResolveValidatedBackchannelTokens.Descriptor, @@ -80,7 +81,6 @@ public static partial class OpenIddictClientHandlers EvaluateUserinfoRequest.Descriptor, AttachUserinfoRequestParameters.Descriptor, SendUserinfoRequest.Descriptor, - ValidateUserinfoErrorParameters.Descriptor, EvaluateValidatedUserinfoToken.Descriptor, ValidateRequiredUserinfoToken.Descriptor, ValidateUserinfoToken.Descriptor, @@ -2017,47 +2017,21 @@ public static partial class OpenIddictClientHandlers throw new InvalidOperationException(SR.FormatID0301(Metadata.TokenEndpoint)); } - context.TokenResponse = await _service.SendTokenRequestAsync( - context.Registration, context.TokenEndpoint, context.TokenRequest); - } - } - - /// - /// Contains the logic responsible for rejecting errored token responses. - /// - public class ValidateTokenErrorParameters : IOpenIddictClientHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictClientHandlerDescriptor Descriptor { get; } - = OpenIddictClientHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(SendTokenRequest.Descriptor.Order + 1_000) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) + try { - throw new ArgumentNullException(nameof(context)); + context.TokenResponse = await _service.SendTokenRequestAsync( + context.Registration, context.TokenEndpoint, context.TokenRequest); } - Debug.Assert(context.TokenResponse is not null, SR.GetResourceString(SR.ID4007)); - - if (!string.IsNullOrEmpty(context.TokenResponse.Error)) + catch (ProtocolException exception) { context.Reject( - error: context.TokenResponse.Error, - description: context.TokenResponse.ErrorDescription, - uri: context.TokenResponse.ErrorUri); + error: exception.Error, + description: exception.ErrorDescription, + uri: exception.ErrorUri); - return default; + return; } - - return default; } } @@ -2073,7 +2047,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(ValidateTokenErrorParameters.Descriptor.Order + 1_000) + .SetOrder(SendTokenRequest.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -3002,47 +2976,21 @@ public static partial class OpenIddictClientHandlers // - application/json responses containing a JSON object listing the user claims as-is. // - application/jwt responses containing a signed/encrypted JSON Web Token containing the user claims. - (context.UserinfoResponse, (context.UserinfoTokenPrincipal, context.UserinfoToken)) = - await _service.SendUserinfoRequestAsync(context.Registration, context.UserinfoEndpoint, context.UserinfoRequest); - } - } - - /// - /// Contains the logic responsible for rejecting errored userinfo responses. - /// - public class ValidateUserinfoErrorParameters : IOpenIddictClientHandler - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictClientHandlerDescriptor Descriptor { get; } - = OpenIddictClientHandlerDescriptor.CreateBuilder() - .AddFilter() - .UseSingletonHandler() - .SetOrder(SendUserinfoRequest.Descriptor.Order + 1_000) - .Build(); - - /// - public ValueTask HandleAsync(ProcessAuthenticationContext context) - { - if (context is null) + try { - throw new ArgumentNullException(nameof(context)); + (context.UserinfoResponse, (context.UserinfoTokenPrincipal, context.UserinfoToken)) = + await _service.SendUserinfoRequestAsync(context.Registration, context.UserinfoEndpoint, context.UserinfoRequest); } - Debug.Assert(context.UserinfoResponse is not null, SR.GetResourceString(SR.ID4007)); - - if (!string.IsNullOrEmpty(context.UserinfoResponse.Error)) + catch (ProtocolException exception) { context.Reject( - error: context.UserinfoResponse.Error, - description: context.UserinfoResponse.ErrorDescription, - uri: context.UserinfoResponse.ErrorUri); + error: exception.Error, + description: exception.ErrorDescription, + uri: exception.ErrorUri); - return default; + return; } - - return default; } } @@ -3058,7 +3006,7 @@ public static partial class OpenIddictClientHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(ValidateUserinfoErrorParameters.Descriptor.Order + 1_000) + .SetOrder(SendUserinfoRequest.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -4979,43 +4927,6 @@ public static partial class OpenIddictClientHandlers } } - /// - /// Contains the logic responsible for extracting potential errors from the response. - /// - public class HandleErrorResponse : IOpenIddictClientHandler where TContext : BaseValidatingContext - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictClientHandlerDescriptor Descriptor { get; } - = OpenIddictClientHandlerDescriptor.CreateBuilder() - .UseSingletonHandler>() - .SetOrder(int.MinValue + 100_000) - .SetType(OpenIddictClientHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(TContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!string.IsNullOrEmpty(context.Transaction.Response?.Error)) - { - context.Reject( - error: context.Transaction.Response.Error, - description: context.Transaction.Response.ErrorDescription, - uri: context.Transaction.Response.ErrorUri); - - return default; - } - - return default; - } - } - /// /// Contains the logic responsible for attaching the appropriate parameters to the error response. /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs index df8d2e3f..f53886a3 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Discovery.cs @@ -6,6 +6,7 @@ using System.Collections.Immutable; using System.Text.Json; +using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; namespace OpenIddict.Validation; @@ -18,8 +19,8 @@ public static partial class OpenIddictValidationHandlers /* * Configuration response handling: */ - HandleErrorResponse.Descriptor, ValidateWellKnownConfigurationParameters.Descriptor, + HandleConfigurationErrorResponse.Descriptor, ValidateIssuer.Descriptor, ExtractCryptographyEndpoint.Descriptor, ExtractIntrospectionEndpoint.Descriptor, @@ -28,8 +29,8 @@ public static partial class OpenIddictValidationHandlers /* * Cryptography response handling: */ - HandleErrorResponse.Descriptor, ValidateWellKnownCryptographyParameters.Descriptor, + HandleCryptographyErrorResponse.Descriptor, ExtractSigningKeys.Descriptor); /// @@ -43,7 +44,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -79,6 +80,10 @@ public static partial class OpenIddictValidationHandlers // JsonElement instance using the same value type as the original parameter value. static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch { + // Error parameters MUST be formatted as unique strings: + Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri + => ((JsonElement) value).ValueKind is JsonValueKind.String, + // The following parameters MUST be formatted as unique strings: Metadata.IntrospectionEndpoint or Metadata.Issuer @@ -108,6 +113,49 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible for surfacing potential errors from the configuration response. + /// + public class HandleConfigurationErrorResponse : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateWellKnownConfigurationParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleConfigurationResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the specification doesn't define a standard way to return an error other than + // returning a 4xx status code. That said, some implementations are known to return + // JSON payloads similar to standard errored token responses. For more information, see + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse. + if (!string.IsNullOrEmpty(context.Response.Error)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6203), context.Response); + + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2144), + uri: SR.FormatID8000(SR.ID2144)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting the issuer from the discovery document. /// @@ -119,7 +167,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateWellKnownConfigurationParameters.Descriptor.Order + 1_000) + .SetOrder(HandleConfigurationErrorResponse.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -320,7 +368,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -356,6 +404,10 @@ public static partial class OpenIddictValidationHandlers // JsonElement instance using the same value type as the original parameter value. static bool ValidateParameterType(string name, OpenIddictParameter value) => name switch { + // Error parameters MUST be formatted as unique strings: + Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri + => ((JsonElement) value).ValueKind is JsonValueKind.String, + // The following parameters MUST be formatted as arrays of objects: JsonWebKeySetParameterNames.Keys => ((JsonElement) value) is JsonElement element && element.ValueKind is JsonValueKind.Array && ValidateObjectArray(element), @@ -379,6 +431,49 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible for surfacing potential errors from the cryptography response. + /// + public class HandleCryptographyErrorResponse : IOpenIddictValidationHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictValidationHandlerDescriptor Descriptor { get; } + = OpenIddictValidationHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateWellKnownCryptographyParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictValidationHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleCryptographyResponseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the specification doesn't define a standard way to return an error other than + // returning a 4xx status code. That said, some implementations are known to return + // JSON payloads similar to standard errored token responses. For more information, see + // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse. + if (!string.IsNullOrEmpty(context.Response.Error)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6204), context.Response); + + context.Reject( + error: Errors.ServerError, + description: SR.GetResourceString(SR.ID2145), + uri: SR.FormatID8000(SR.ID2145)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting the signing keys from the JWKS document. /// @@ -390,7 +485,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateWellKnownCryptographyParameters.Descriptor.Order + 1_000) + .SetOrder(HandleCryptographyErrorResponse.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 ce51a2f5..71a66f85 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Introspection.cs @@ -7,6 +7,7 @@ using System.Collections.Immutable; using System.Security.Claims; using System.Text.Json; +using Microsoft.Extensions.Logging; namespace OpenIddict.Validation; @@ -24,8 +25,8 @@ public static partial class OpenIddictValidationHandlers /* * Introspection response handling: */ - HandleErrorResponse.Descriptor, ValidateWellKnownParameters.Descriptor, + HandleErrorResponse.Descriptor, HandleInactiveResponse.Descriptor, ValidateIssuer.Descriptor, ValidateTokenUsage.Descriptor, @@ -102,7 +103,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) + .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); @@ -138,25 +139,29 @@ public static partial class OpenIddictValidationHandlers // 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 booleans: + // Error parameters MUST be formatted as unique strings: + Parameters.Error or Parameters.ErrorDescription or Parameters.ErrorUri + => ((JsonElement) value).ValueKind is JsonValueKind.String, + + // The following claims 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: + // The following claims MUST be formatted as unique strings: Claims.JwtId or Claims.Issuer or Claims.Scope or Claims.TokenUsage => ((JsonElement) value).ValueKind is JsonValueKind.String, - // The following parameters MUST be formatted as strings or arrays of strings: + // The following claims 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 following parameters MUST be formatted as numeric dates: + // The following claims MUST be formatted as numeric dates: Claims.ExpiresAt or Claims.IssuedAt or Claims.NotBefore => ((JsonElement) value).ValueKind is JsonValueKind.Number, - // Parameters that are not in the well-known list can be of any type. + // Claims that are not in the well-known list can be of any type. _ => true }; @@ -175,6 +180,52 @@ public static partial class OpenIddictValidationHandlers } } + /// + /// Contains the logic responsible for surfacing potential errors from the introspection response. + /// + public class HandleErrorResponse : 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) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: the specification requires returning most errors (e.g invalid token errors) + // as "active: false" responses instead of as proper OAuth 2.0 error responses. + // For more information, see https://datatracker.ietf.org/doc/html/rfc7662#section-2.3. + if (!string.IsNullOrEmpty(context.Response.Error)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6205), context.Response); + + context.Reject( + error: context.Response.Error switch + { + Errors.UnauthorizedClient => Errors.UnauthorizedClient, + _ => Errors.ServerError + }, + description: SR.GetResourceString(SR.ID2146), + uri: SR.FormatID8000(SR.ID2146)); + + return default; + } + + return default; + } + } + /// /// Contains the logic responsible for extracting the active: false marker from the response. /// @@ -186,7 +237,7 @@ public static partial class OpenIddictValidationHandlers public static OpenIddictValidationHandlerDescriptor Descriptor { get; } = OpenIddictValidationHandlerDescriptor.CreateBuilder() .UseSingletonHandler() - .SetOrder(ValidateWellKnownParameters.Descriptor.Order + 1_000) + .SetOrder(HandleErrorResponse.Descriptor.Order + 1_000) .SetType(OpenIddictValidationHandlerType.BuiltIn) .Build(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs index d037ff05..f8e80aef 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.Protection.cs @@ -10,6 +10,7 @@ using System.Globalization; using System.Security.Claims; using Microsoft.Extensions.Logging; using Microsoft.IdentityModel.Tokens; +using static OpenIddict.Abstractions.OpenIddictExceptions; namespace OpenIddict.Validation; @@ -336,14 +337,14 @@ public static partial class OpenIddictValidationHandlers }) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0141)); } - catch (Exception exception) + catch (ProtocolException exception) { context.Logger.LogDebug(exception, SR.GetResourceString(SR.ID6155)); context.Reject( - error: Errors.InvalidToken, - description: SR.GetResourceString(SR.ID2004), - uri: SR.FormatID8000(SR.ID2004)); + error: exception.Error, + description: exception.ErrorDescription, + uri: exception.ErrorUri); return; } diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 6e8154aa..4bdcb4fd 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -365,41 +365,4 @@ public static partial class OpenIddictValidationHandlers return default; } } - - /// - /// Contains the logic responsible for extracting potential errors from the response. - /// - public class HandleErrorResponse : IOpenIddictValidationHandler where TContext : BaseValidatingContext - { - /// - /// Gets the default descriptor definition assigned to this handler. - /// - public static OpenIddictValidationHandlerDescriptor Descriptor { get; } - = OpenIddictValidationHandlerDescriptor.CreateBuilder() - .UseSingletonHandler>() - .SetOrder(int.MinValue + 100_000) - .SetType(OpenIddictValidationHandlerType.BuiltIn) - .Build(); - - /// - public ValueTask HandleAsync(TContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (!string.IsNullOrEmpty(context.Transaction.Response?.Error)) - { - context.Reject( - error: context.Transaction.Response.Error, - description: context.Transaction.Response.ErrorDescription, - uri: context.Transaction.Response.ErrorUri); - - return default; - } - - return default; - } - } }