diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs index 6b51b320..9781984b 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs @@ -826,6 +826,14 @@ public static partial class OpenIddictClientEvents /// Note: overriding the value of this property is generally not recommended. /// public bool DisableFrontchannelIdentityTokenNonceValidation { get; set; } + + /// + /// Gets or sets a boolean indicating whether userinfo validation should be disabled. + /// + /// + /// Note: overriding the value of this property is generally not recommended. + /// + public bool DisableUserinfoValidation { get; set; } } /// diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs index 7fa124fe..dcd87f55 100644 --- a/src/OpenIddict.Client/OpenIddictClientExtensions.cs +++ b/src/OpenIddict.Client/OpenIddictClientExtensions.cs @@ -63,6 +63,7 @@ public static class OpenIddictClientExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Register the built-in client event handlers used by the OpenIddict client components. // Note: the order used here is not important, as the actual order is set in the options. diff --git a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs index 5279e909..f4cc0d8c 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlerFilters.cs @@ -475,4 +475,21 @@ public static class OpenIddictClientHandlerFilters return new(context.UserinfoTokenPrincipal is not null); } } + + /// + /// Represents a filter that excludes the associated handlers if userinfo validation was disabled. + /// + public sealed class RequireUserinfoValidationEnabled : IOpenIddictClientHandlerFilter + { + /// + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!context.DisableUserinfoValidation); + } + } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 39f5ccaf..0be38508 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -3404,6 +3404,12 @@ public static partial class OpenIddictClientHandlers _ => false }; + // The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints + // but must also support non-standard implementations, that are common with OAuth 2.0-only servers. + // + // As such, protocol requirements are only enforced if the server supports OpenID Connect. + context.DisableUserinfoValidation = !context.Configuration.ScopesSupported.Contains(Scopes.OpenId); + return default; } } @@ -3659,6 +3665,7 @@ public static partial class OpenIddictClientHandlers /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateUserinfoToken.Descriptor.Order + 1_000) @@ -3675,28 +3682,21 @@ public static partial class OpenIddictClientHandlers Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - // The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints - // but must also support non-standard implementations, that are common with OAuth 2.0-only servers. - // - // As such, protocol requirements are only enforced if the server supports OpenID Connect. - if (context.Configuration.ScopesSupported.Contains(Scopes.OpenId)) + foreach (var group in context.UserinfoTokenPrincipal.Claims + .GroupBy(claim => claim.Type) + .ToDictionary(group => group.Key, group => group.ToList())) { - foreach (var group in context.UserinfoTokenPrincipal.Claims - .GroupBy(claim => claim.Type) - .ToDictionary(group => group.Key, group => group.ToList())) + if (ValidateClaimGroup(group)) { - if (ValidateClaimGroup(group)) - { - continue; - } + continue; + } - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2131(group.Key), - uri: SR.FormatID8000(SR.ID2131)); + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2131(group.Key), + uri: SR.FormatID8000(SR.ID2131)); - return default; - } + return default; } return default; @@ -3725,6 +3725,7 @@ public static partial class OpenIddictClientHandlers /// public static OpenIddictClientHandlerDescriptor Descriptor { get; } = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() .AddFilter() .UseSingletonHandler() .SetOrder(ValidateUserinfoTokenWellknownClaims.Descriptor.Order + 1_000) @@ -3741,53 +3742,46 @@ public static partial class OpenIddictClientHandlers Debug.Assert(context.UserinfoTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - // The OpenIddict client is expected to be used with standard OpenID Connect userinfo endpoints - // but must also support non-standard implementations, that are common with OAuth 2.0-only servers. - // - // As such, protocol requirements are only enforced if the server supports OpenID Connect. - if (context.Configuration.ScopesSupported.Contains(Scopes.OpenId)) + // Standard OpenID Connect userinfo responses/tokens MUST contain a "sub" claim. For more + // information, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse. + if (!context.UserinfoTokenPrincipal.HasClaim(Claims.Subject)) { - // Standard OpenID Connect userinfo responses/tokens MUST contain a "sub" claim. For more - // information, see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse. - if (!context.UserinfoTokenPrincipal.HasClaim(Claims.Subject)) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2132(Claims.Subject), - uri: SR.FormatID8000(SR.ID2132)); + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2132(Claims.Subject), + uri: SR.FormatID8000(SR.ID2132)); - return default; - } + return default; + } - // The "sub" claim returned as part of the userinfo response/token MUST exactly match the value - // returned in the frontchannel identity token, if one was returned. For more information, - // see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse. - if (context.FrontchannelIdentityTokenPrincipal is not null && !string.Equals( - context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.Subject), - context.UserinfoTokenPrincipal.GetClaim(Claims.Subject), StringComparison.Ordinal)) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2133(Claims.Subject), - uri: SR.FormatID8000(SR.ID2133)); + // The "sub" claim returned as part of the userinfo response/token MUST exactly match the value + // returned in the frontchannel identity token, if one was returned. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse. + if (context.FrontchannelIdentityTokenPrincipal is not null && !string.Equals( + context.FrontchannelIdentityTokenPrincipal.GetClaim(Claims.Subject), + context.UserinfoTokenPrincipal.GetClaim(Claims.Subject), StringComparison.Ordinal)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2133(Claims.Subject), + uri: SR.FormatID8000(SR.ID2133)); - return default; - } + return default; + } - // The "sub" claim returned as part of the userinfo response/token MUST exactly match the value - // returned in the frontchannel identity token, if one was returned. For more information, - // see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse. - if (context.BackchannelIdentityTokenPrincipal is not null && !string.Equals( - context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.Subject), - context.UserinfoTokenPrincipal.GetClaim(Claims.Subject), StringComparison.Ordinal)) - { - context.Reject( - error: Errors.InvalidRequest, - description: SR.FormatID2133(Claims.Subject), - uri: SR.FormatID8000(SR.ID2133)); + // The "sub" claim returned as part of the userinfo response/token MUST exactly match the value + // returned in the frontchannel identity token, if one was returned. For more information, + // see https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse. + if (context.BackchannelIdentityTokenPrincipal is not null && !string.Equals( + context.BackchannelIdentityTokenPrincipal.GetClaim(Claims.Subject), + context.UserinfoTokenPrincipal.GetClaim(Claims.Subject), StringComparison.Ordinal)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.FormatID2133(Claims.Subject), + uri: SR.FormatID8000(SR.ID2133)); - return default; - } + return default; } return default;