diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs index 74201830..a8f601b5 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs @@ -565,6 +565,70 @@ public static partial class OpenIddictClientEvents /// public bool ValidateUserinfoToken { get; set; } + /// + /// Gets or sets a boolean indicating whether an invalid authorization code + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectAuthorizationCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid backchannel access token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectBackchannelAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid backchannel identity token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectBackchannelIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid frontchannel access token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectFrontchannelAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid frontchannel identity token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectFrontchannelIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid refresh token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectRefreshToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid state token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectStateToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid userinfo token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectUserinfoToken { get; set; } + /// /// Gets or sets the authorization code to validate, if applicable. /// diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index 1b58bb2d..b35e68d5 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -401,23 +401,24 @@ public static partial class OpenIddictClientHandlers (context.ExtractStateToken, context.RequireStateToken, - context.ValidateStateToken) = context.EndpointType switch + context.ValidateStateToken, + context.RejectStateToken) = context.EndpointType switch { // While the OAuth 2.0/2.1 and OpenID Connect specifications don't require sending a // state as part of authorization requests, the identity provider MUST return the state // if one was initially specified. Since OpenIddict always sends a state (used as a way // to mitigate CSRF attacks and store per-authorization values like the identity of the // chosen authorization server), the state is always considered required at this point. - OpenIddictClientEndpointType.Redirection => (true, true, true), + OpenIddictClientEndpointType.Redirection => (true, true, true, true), // While the OpenID Connect RP-initiated logout specification doesn't require sending // a state as part of logout requests, the identity provider MUST return the state // if one was initially specified. Since OpenIddict always sends a state (used as a // way to mitigate CSRF attacks and store per-logout values like the identity of the // chosen authorization server), the state is always considered required at this point. - OpenIddictClientEndpointType.PostLogoutRedirection => (true, true, true), + OpenIddictClientEndpointType.PostLogoutRedirection => (true, true, true, true), - _ => (false, false, false) + _ => (false, false, false, false) }; return default; @@ -555,10 +556,15 @@ public static partial class OpenIddictClientHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectStateToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -1204,7 +1210,8 @@ public static partial class OpenIddictClientHandlers (context.ExtractAuthorizationCode, context.RequireAuthorizationCode, - context.ValidateAuthorizationCode) = context.GrantType switch + context.ValidateAuthorizationCode, + context.RejectAuthorizationCode) = context.GrantType switch { // An authorization code is returned for the authorization code and implicit grants when // the response type contains the "code" value, which includes the authorization code @@ -1217,14 +1224,15 @@ public static partial class OpenIddictClientHandlers GrantTypes.AuthorizationCode or GrantTypes.Implicit when context.ResponseType?.Split(Separators.Space) is IList types && types.Contains(ResponseTypes.Code) - => (true, true, false), + => (true, true, false, false), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractFrontchannelAccessToken, context.RequireFrontchannelAccessToken, - context.ValidateFrontchannelAccessToken) = context.GrantType switch + context.ValidateFrontchannelAccessToken, + context.RejectFrontchannelAccessToken) = context.GrantType switch { // An access token is returned for the authorization code and implicit grants when // the response type contains the "token" value, which includes some variations of @@ -1237,14 +1245,15 @@ public static partial class OpenIddictClientHandlers GrantTypes.AuthorizationCode or GrantTypes.Implicit when context.ResponseType?.Split(Separators.Space) is IList types && types.Contains(ResponseTypes.Token) - => (true, true, false), + => (true, true, false, false), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractFrontchannelIdentityToken, context.RequireFrontchannelIdentityToken, - context.ValidateFrontchannelIdentityToken) = context.GrantType switch + context.ValidateFrontchannelIdentityToken, + context.RejectFrontchannelIdentityToken) = context.GrantType switch { // An identity token is returned for the authorization code and implicit grants when // the response type contains the "id_token" value, which includes some variations @@ -1256,9 +1265,9 @@ public static partial class OpenIddictClientHandlers GrantTypes.AuthorizationCode or GrantTypes.Implicit when context.ResponseType?.Split(Separators.Space) is IList types && types.Contains(ResponseTypes.IdToken) - => (true, true, true), + => (true, true, true, true), - _ => (false, false, false) + _ => (false, false, false, false) }; return default; @@ -1414,10 +1423,15 @@ public static partial class OpenIddictClientHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectFrontchannelIdentityToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -1932,10 +1946,15 @@ public static partial class OpenIddictClientHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectFrontchannelAccessToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -2002,10 +2021,15 @@ public static partial class OpenIddictClientHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectAuthorizationCode) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -2489,7 +2513,8 @@ public static partial class OpenIddictClientHandlers (context.ExtractBackchannelAccessToken, context.RequireBackchannelAccessToken, - context.ValidateBackchannelAccessToken) = context.GrantType switch + context.ValidateBackchannelAccessToken, + context.RejectBackchannelAccessToken) = context.GrantType switch { // An access token is always returned as part of token responses, independently of // the negotiated response types or whether the server supports OpenID Connect or not. @@ -2501,19 +2526,20 @@ public static partial class OpenIddictClientHandlers GrantTypes.AuthorizationCode or GrantTypes.Implicit when context.ResponseType?.Split(Separators.Space) is IList types && types.Contains(ResponseTypes.Code) - => (true, true, false), + => (true, true, false, false), // An access token is always returned as part of client credentials, // resource owner password credentials and refresh token responses. GrantTypes.ClientCredentials or GrantTypes.Password or GrantTypes.RefreshToken - => (true, true, false), + => (true, true, false, false), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractBackchannelIdentityToken, context.RequireBackchannelIdentityToken, - context.ValidateBackchannelIdentityToken) = context.GrantType switch + context.ValidateBackchannelIdentityToken, + context.RejectBackchannelIdentityToken) = context.GrantType switch { // An identity token is always returned as part of token responses for the code and // hybrid flows when the authorization server supports OpenID Connect. As such, @@ -2523,7 +2549,7 @@ public static partial class OpenIddictClientHandlers context.ResponseType?.Split(Separators.Space) is IList types && types.Contains(ResponseTypes.Code) && context.StateTokenPrincipal is ClaimsPrincipal principal && - principal.HasScope(Scopes.OpenId) => (true, true, true), + principal.HasScope(Scopes.OpenId) => (true, true, true, true), // The client credentials and resource owner password credentials grants don't have // an equivalent in OpenID Connect so an identity token is typically never returned @@ -2531,20 +2557,21 @@ public static partial class OpenIddictClientHandlers // allow returning it as a non-standard artifact. As such, the identity token // is not considered required but will always be validated using the same routine // (except nonce validation) if it is present in the token response. - GrantTypes.ClientCredentials or GrantTypes.Password => (true, false, true), + GrantTypes.ClientCredentials or GrantTypes.Password => (true, false, true, false), // An identity token may or may not be returned as part of refresh token responses // depending on the policy adopted by the remote authorization server. As such, // the identity token is not considered required but will always be validated using // the same routine (except nonce validation) if it is present in the token response. - GrantTypes.RefreshToken => (true, false, true), + GrantTypes.RefreshToken => (true, false, true, false), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractRefreshToken, context.RequireRefreshToken, - context.ValidateRefreshToken) = context.GrantType switch + context.ValidateRefreshToken, + context.RejectRefreshToken) = context.GrantType switch { // A refresh token may be returned as part of token responses, depending on the // policy enforced by the remote authorization server (e.g the "offline_access" @@ -2557,16 +2584,16 @@ public static partial class OpenIddictClientHandlers GrantTypes.AuthorizationCode or GrantTypes.Implicit when context.ResponseType?.Split(Separators.Space) is IList types && types.Contains(ResponseTypes.Code) - => (true, false, false), + => (true, false, false, false), // A refresh token may or may not be returned as part of client credentials, // resource owner password credentials and refresh token responses depending // on the policy adopted by the remote authorization server. As such, a // refresh token is never considered required for such token responses. GrantTypes.ClientCredentials or GrantTypes.Password or GrantTypes.RefreshToken - => (true, false, false), + => (true, false, false, false), - _ => (false, false, false) + _ => (false, false, false, false) }; return default; @@ -2719,10 +2746,15 @@ public static partial class OpenIddictClientHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectBackchannelIdentityToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -3201,10 +3233,15 @@ public static partial class OpenIddictClientHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectBackchannelAccessToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -3271,10 +3308,15 @@ public static partial class OpenIddictClientHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectRefreshToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -3482,7 +3524,8 @@ public static partial class OpenIddictClientHandlers // or responses will be extracted and validated when a userinfo request was sent. (context.ExtractUserinfoToken, context.RequireUserinfoToken, - context.ValidateUserinfoToken) = (true, false, true); + context.ValidateUserinfoToken, + context.RejectUserinfoToken) = (true, false, true, true); return default; } @@ -3584,10 +3627,15 @@ public static partial class OpenIddictClientHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectUserinfoToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs index 605f20b4..f32b270f 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Device.cs @@ -192,7 +192,7 @@ public static partial class OpenIddictServerEvents } /// - /// Gets or sets the security principal extracted from the user code. + /// Gets or sets the security principal extracted from the user code, if applicable. /// public ClaimsPrincipal? Principal { get; set; } } @@ -220,6 +220,11 @@ public static partial class OpenIddictServerEvents set => Transaction.Request = value; } + /// + /// Gets or sets the security principal extracted from the user code, if applicable. + /// + public ClaimsPrincipal? UserCodePrincipal { get; set; } + /// /// Gets the additional parameters returned to the caller. /// diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index 9e964b6f..ba91329d 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -481,6 +481,62 @@ public static partial class OpenIddictServerEvents /// public bool ValidateUserCode { get; set; } + /// + /// Gets or sets a boolean indicating whether an invalid access token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid authorization code + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectAuthorizationCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid device code + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectDeviceCode { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid generic token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectGenericToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid identity token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectIdentityToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid refresh token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectRefreshToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid user code + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectUserCode { get; set; } + /// /// Gets or sets the access token to validate, if applicable. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs index e3790530..51f035da 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Authentication.cs @@ -1636,8 +1636,7 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for rejecting authorization - /// requests that don't specify a valid id_token_hint. + /// Contains the logic responsible for validating the token(s) present in the request. /// public sealed class ValidateToken : IOpenIddictServerHandler { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs index 50ea3383..d3d108fe 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Device.cs @@ -54,6 +54,11 @@ public static partial class OpenIddictServerHandlers ApplyVerificationResponse.Descriptor, ApplyVerificationResponse.Descriptor, + /* + * Verification request validation: + */ + ValidateToken.Descriptor, + /* * Verification request handling: */ @@ -951,6 +956,10 @@ public static partial class OpenIddictServerHandlers var notification = new ValidateVerificationRequestContext(context.Transaction); await _dispatcher.DispatchAsync(notification); + // Store the context object in the transaction so it can be later retrieved by handlers + // that want to access the context without triggering a new validation process. + context.Transaction.SetProperty(typeof(ValidateVerificationRequestContext).FullName!, notification); + if (notification.IsRequestHandled) { context.HandleRequest(); @@ -1122,42 +1131,33 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for attaching the claims principal resolved from the user code. + /// Contains the logic responsible for validating the token(s) present in the request. /// - public sealed class AttachUserCodePrincipal : IOpenIddictServerHandler + public sealed class ValidateToken : IOpenIddictServerHandler { private readonly IOpenIddictServerDispatcher _dispatcher; - public AttachUserCodePrincipal(IOpenIddictServerDispatcher dispatcher) + public ValidateToken(IOpenIddictServerDispatcher dispatcher) => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } - = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseScopedHandler() + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() .SetOrder(int.MinValue + 100_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// - public async ValueTask HandleAsync(HandleVerificationRequestContext context) + public async ValueTask HandleAsync(ValidateVerificationRequestContext context) { if (context is null) { throw new ArgumentNullException(nameof(context)); } - // Note: the user_code may not be present (e.g when the user typed - // the verification_uri manually without the user code appended). - // In this case, ignore the missing token so that a view can be - // rendered by the application to ask the user to enter the code. - if (string.IsNullOrEmpty(context.Request.UserCode)) - { - return; - } - var notification = new ProcessAuthenticationContext(context.Transaction); await _dispatcher.DispatchAsync(notification); @@ -1179,7 +1179,10 @@ public static partial class OpenIddictServerHandlers else if (notification.IsRejected) { - // Note: authentication errors are deliberately not flowed up to the parent context. + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); return; } @@ -1187,5 +1190,38 @@ public static partial class OpenIddictServerHandlers context.Principal = notification.UserCodePrincipal; } } + + /// + /// Contains the logic responsible for attaching the principal extracted from the user code to the event context. + /// + public sealed class AttachUserCodePrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictServerHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(HandleVerificationRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + var notification = context.Transaction.GetProperty( + typeof(ValidateVerificationRequestContext).FullName!) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0007)); + + context.UserCodePrincipal ??= notification.Principal; + + return default; + } + } } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs index 4f0f8a6d..77480861 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Exchange.cs @@ -1286,8 +1286,7 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for rejecting token requests that don't - /// specify a valid authorization code, device code or refresh token. + /// Contains the logic responsible for validating the token(s) present in the request. /// public sealed class ValidateToken : IOpenIddictServerHandler { @@ -1351,8 +1350,8 @@ public static partial class OpenIddictServerHandlers // Attach the security principal extracted from the token to the validation context. context.Principal = context.Request.IsAuthorizationCodeGrantType() ? notification.AuthorizationCodePrincipal : - context.Request.IsDeviceCodeGrantType() ? notification.DeviceCodePrincipal : - context.Request.IsRefreshTokenGrantType() ? notification.RefreshTokenPrincipal : null; + context.Request.IsDeviceCodeGrantType() ? notification.DeviceCodePrincipal : + context.Request.IsRefreshTokenGrantType() ? notification.RefreshTokenPrincipal : null; } } diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs index 553a9538..e29f87c3 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Introspection.cs @@ -650,7 +650,7 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for rejecting introspection requests that don't specify a valid token. + /// Contains the logic responsible for validating the token(s) present in the request. /// public sealed class ValidateToken : IOpenIddictServerHandler { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs index 2ba37168..f09205a2 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs @@ -597,7 +597,7 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for rejecting revocation requests that don't specify a valid token. + /// Contains the logic responsible for validating the token(s) present in the request. /// public sealed class ValidateToken : IOpenIddictServerHandler { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs index 4d9f423a..57ef3eb7 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Session.cs @@ -595,7 +595,7 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for rejecting logout requests that don't specify a valid id_token_hint. + /// Contains the logic responsible for validating the token(s) present in the request. /// public sealed class ValidateToken : IOpenIddictServerHandler { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs index 275f5b75..b1948199 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Userinfo.cs @@ -342,7 +342,7 @@ public static partial class OpenIddictServerHandlers } /// - /// Contains the logic responsible for rejecting userinfo requests that don't specify a valid token. + /// Contains the logic responsible for validating the token(s) present in the request. /// public sealed class ValidateToken : IOpenIddictServerHandler { diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 492b381c..19f2ba12 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -267,80 +267,90 @@ public static partial class OpenIddictServerHandlers (context.ExtractAccessToken, context.RequireAccessToken, - context.ValidateAccessToken) = context.EndpointType switch + context.ValidateAccessToken, + context.RejectAccessToken) = context.EndpointType switch { // The userinfo endpoint requires sending a valid access token. - OpenIddictServerEndpointType.Userinfo => (true, true, true), + OpenIddictServerEndpointType.Userinfo => (true, true, true, true), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractAuthorizationCode, context.RequireAuthorizationCode, - context.ValidateAuthorizationCode) = context.EndpointType switch + context.ValidateAuthorizationCode, + context.RejectAuthorizationCode) = context.EndpointType switch { // The authorization code grant requires sending a valid authorization code. OpenIddictServerEndpointType.Token when context.Request.IsAuthorizationCodeGrantType() - => (true, true, true), + => (true, true, true, true), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractDeviceCode, context.RequireDeviceCode, - context.ValidateDeviceCode) = context.EndpointType switch + context.ValidateDeviceCode, + context.RejectDeviceCode) = context.EndpointType switch { // The device code grant requires sending a valid device code. OpenIddictServerEndpointType.Token when context.Request.IsDeviceCodeGrantType() - => (true, true, true), + => (true, true, true, true), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractGenericToken, context.RequireGenericToken, - context.ValidateGenericToken) = context.EndpointType switch + context.ValidateGenericToken, + context.RejectGenericToken) = context.EndpointType switch { // Tokens received by the introspection and revocation endpoints can be of any type. // Additional token type filtering is made by the endpoint themselves, if needed. OpenIddictServerEndpointType.Introspection or OpenIddictServerEndpointType.Revocation - => (true, true, true), + => (true, true, true, true), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractIdentityToken, context.RequireIdentityToken, - context.ValidateIdentityToken) = context.EndpointType switch + context.ValidateIdentityToken, + context.RejectIdentityToken) = context.EndpointType switch { // The identity token received by the authorization and logout // endpoints are not required and serve as optional hints. + // + // As such, identity token hints are extracted and validated, but + // the authentication demand is not rejected if they are not valid. OpenIddictServerEndpointType.Authorization or OpenIddictServerEndpointType.Logout - => (true, false, true), + => (true, false, true, false), - _ => (false, false, true) + _ => (false, false, false, false) }; (context.ExtractRefreshToken, context.RequireRefreshToken, - context.ValidateRefreshToken) = context.EndpointType switch + context.ValidateRefreshToken, + context.RejectRefreshToken) = context.EndpointType switch { // The refresh token grant requires sending a valid refresh token. OpenIddictServerEndpointType.Token when context.Request.IsRefreshTokenGrantType() - => (true, true, true), + => (true, true, true, true), - _ => (false, false, false) + _ => (false, false, false, false) }; (context.ExtractUserCode, context.RequireUserCode, - context.ValidateUserCode) = context.EndpointType switch + context.ValidateUserCode, + context.RejectUserCode) = context.EndpointType switch { // Note: the verification endpoint can be accessed without specifying a // user code (that can be later set by the user using a form, for instance). - OpenIddictServerEndpointType.Verification => (true, false, true), + OpenIddictServerEndpointType.Verification => (true, false, true, false), - _ => (false, false, false) + _ => (false, false, false, false) }; return default; @@ -442,10 +452,10 @@ public static partial class OpenIddictServerHandlers /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() - .UseSingletonHandler() + .UseSingletonHandler() // Note: this handler is registered with a high gap to allow handlers // that do token extraction to be executed before this handler runs. - .SetOrder(EvaluateValidatedTokens.Descriptor.Order + 50_000) + .SetOrder(ResolveValidatedTokens.Descriptor.Order + 50_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); @@ -533,10 +543,15 @@ public static partial class OpenIddictServerHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectAccessToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -600,10 +615,15 @@ public static partial class OpenIddictServerHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectAuthorizationCode) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -667,10 +687,15 @@ public static partial class OpenIddictServerHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectDeviceCode) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -741,10 +766,15 @@ public static partial class OpenIddictServerHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectGenericToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -811,10 +841,15 @@ public static partial class OpenIddictServerHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectIdentityToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -878,10 +913,15 @@ public static partial class OpenIddictServerHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectRefreshToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } @@ -945,10 +985,15 @@ public static partial class OpenIddictServerHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectUserCode) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index addab223..4d4e14c9 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -321,6 +321,14 @@ public static partial class OpenIddictValidationEvents /// recommended, except when dealing with non-standard clients. /// public bool ValidateAccessToken { get; set; } + + /// + /// Gets or sets a boolean indicating whether an invalid access token + /// will cause the authentication demand to be rejected or will be ignored. + /// Note: overriding the value of this property is generally not + /// recommended, except when dealing with non-standard clients. + /// + public bool RejectAccessToken { get; set; } } /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs index 285eead7..40ffd897 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationHandlers.cs @@ -90,13 +90,14 @@ public static partial class OpenIddictValidationHandlers (context.ExtractAccessToken, context.RequireAccessToken, - context.ValidateAccessToken) = context.EndpointType switch + context.ValidateAccessToken, + context.RejectAccessToken) = context.EndpointType switch { // The validation handler is responsible for validating access tokens for endpoints // it doesn't manage (typically, API endpoints using token authentication). - OpenIddictValidationEndpointType.Unknown => (true, true, true), + OpenIddictValidationEndpointType.Unknown => (true, true, true, true), - _ => (false, false, false) + _ => (false, false, false, false) }; // Note: unlike the equivalent event in the server stack, authentication can be triggered for @@ -203,10 +204,15 @@ public static partial class OpenIddictValidationHandlers else if (notification.IsRejected) { - context.Reject( - error: notification.Error ?? Errors.InvalidRequest, - description: notification.ErrorDescription, - uri: notification.ErrorUri); + if (context.RejectAccessToken) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + return; } diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs index 05f33f57..94efc146 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Authentication.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers; using static OpenIddict.Server.OpenIddictServerHandlers.Protection; namespace OpenIddict.Server.IntegrationTests; @@ -1859,10 +1860,62 @@ public abstract partial class OpenIddictServerIntegrationTests } [Fact] - public async Task ValidateAuthorizationRequest_InvalidIdentityTokenHintCausesAnError() + public async Task ValidateAuthorizationRequest_InvalidIdentityTokenHintDoesNotCauseAnError() { // Arrange - await using var server = await CreateServerAsync(options => options.EnableDegradedMode()); + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + Assert.Null(context.IdentityTokenHintPrincipal); + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity("Bearer")) + .SetClaim(Claims.Subject, "Bob le Magnifique"); + + return default; + })); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/authorize", new OpenIddictRequest + { + ClientId = "Fabrikam", + IdTokenHint = "id_token", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = ResponseTypes.Token + }); + + // Assert + Assert.Null(response.Code); + Assert.NotNull(response.AccessToken); + } + + [Fact] + public async Task ValidateAuthorizationRequest_InvalidIdentityTokenHintCausesAnErrorWhenRejectionIsEnabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + context.RejectIdentityToken = true; + + return default; + }); + + builder.SetOrder(EvaluateValidatedTokens.Descriptor.Order + 500); + }); + }); + await using var client = await server.CreateClientAsync(); // Act diff --git a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs index 83c2feef..7466436e 100644 --- a/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs +++ b/test/OpenIddict.Server.IntegrationTests/OpenIddictServerIntegrationTests.Session.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.DependencyInjection; using Moq; using Xunit; using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlers; using static OpenIddict.Server.OpenIddictServerHandlers.Protection; namespace OpenIddict.Server.IntegrationTests; @@ -443,10 +444,60 @@ public abstract partial class OpenIddictServerIntegrationTests } [Fact] - public async Task ValidateLogoutRequest_InvalidIdentityTokenHintCausesAnError() + public async Task ValidateLogoutRequest_InvalidIdentityTokenHintDoesNotCauseAnError() { // Arrange - await using var server = await CreateServerAsync(options => options.EnableDegradedMode()); + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + builder.UseInlineHandler(context => + { + Assert.Null(context.IdentityTokenHintPrincipal); + + context.SignOut(); + + return default; + })); + }); + + await using var client = await server.CreateClientAsync(); + + // Act + var response = await client.PostAsync("/connect/logout", new OpenIddictRequest + { + ClientId = "Fabrikam", + IdTokenHint = "id_token", + PostLogoutRedirectUri = "http://www.fabrikam.com/path", + State = "af0ifjsldkj" + }); + + // Assert + Assert.Equal("af0ifjsldkj", response.State); + } + + [Fact] + public async Task ValidateLogoutRequest_InvalidIdentityTokenHintCausesAnErrorWhenRejectionIsEnabled() + { + // Arrange + await using var server = await CreateServerAsync(options => + { + options.EnableDegradedMode(); + + options.AddEventHandler(builder => + { + builder.UseInlineHandler(context => + { + context.RejectIdentityToken = true; + + return default; + }); + + builder.SetOrder(EvaluateValidatedTokens.Descriptor.Order + 500); + }); + }); + await using var client = await server.CreateClientAsync(); // Act