diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs new file mode 100644 index 00000000..17969a9a --- /dev/null +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.Revocation.cs @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.AspNetCore +{ + public static partial class OpenIddictServerAspNetCoreHandlers + { + public static class Revocation + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Revocation request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Revocation response processing: + */ + ProcessJsonResponse.Descriptor); + } + } +} diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs index 2988e8ab..4a0c5f67 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandlers.cs @@ -38,6 +38,7 @@ namespace OpenIddict.Server.AspNetCore .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Introspection.DefaultHandlers) + .AddRange(Revocation.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs new file mode 100644 index 00000000..a2af42df --- /dev/null +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.Revocation.cs @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Collections.Immutable; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server.Owin +{ + public static partial class OpenIddictServerOwinHandlers + { + public static class Revocation + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Revocation request extraction: + */ + ExtractGetOrPostRequest.Descriptor, + + /* + * Revocation response processing: + */ + ProcessJsonResponse.Descriptor); + } + } +} diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs index f9539399..21f44a74 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandlers.cs @@ -37,6 +37,7 @@ namespace OpenIddict.Server.Owin .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Introspection.DefaultHandlers) + .AddRange(Revocation.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers); diff --git a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs index a744936a..d1978c2b 100644 --- a/src/OpenIddict.Server/OpenIddictServerConfiguration.cs +++ b/src/OpenIddict.Server/OpenIddictServerConfiguration.cs @@ -138,6 +138,17 @@ namespace OpenIddict.Server .ToString()); } + if (options.RevocationEndpointUris.Count != 0 && !options.CustomHandlers.Any( + descriptor => descriptor.ContextType == typeof(ValidateRevocationRequestContext) && + descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) + { + throw new InvalidOperationException(new StringBuilder() + .Append("No custom revocation request validation handler was found. When enabling the degraded mode, ") + .Append("a custom 'IOpenIddictServerHandler' must be implemented ") + .Append("to validate revocation requests (e.g to ensure the client_id and client_secret are valid).") + .ToString()); + } + if (options.TokenEndpointUris.Count != 0 && !options.CustomHandlers.Any( descriptor => descriptor.ContextType == typeof(ValidateTokenRequestContext) && descriptor.FilterTypes.All(type => !typeof(RequireDegradedModeDisabled).IsAssignableFrom(type)))) diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs index 0dc7af6f..3d03670e 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.Revocation.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; using JetBrains.Annotations; namespace OpenIddict.Server @@ -46,6 +47,11 @@ namespace OpenIddict.Server /// revocation request, or null if it cannot be found. /// public string TokenTypeHint => Request.TokenTypeHint; + + /// + /// Gets or sets the security principal extracted from the revoked token, if available. + /// + public ClaimsPrincipal Principal { get; set; } } /// @@ -62,6 +68,11 @@ namespace OpenIddict.Server { } + /// + /// Gets or sets the security principal extracted from the revoked token. + /// + public ClaimsPrincipal Principal { get; set; } + /// /// Gets the authentication ticket. /// diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs new file mode 100644 index 00000000..c7d68df1 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.Revocation.cs @@ -0,0 +1,956 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Immutable; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using OpenIddict.Abstractions; +using static OpenIddict.Abstractions.OpenIddictConstants; +using static OpenIddict.Server.OpenIddictServerEvents; +using static OpenIddict.Server.OpenIddictServerHandlerFilters; +using Properties = OpenIddict.Server.OpenIddictServerConstants.Properties; + +namespace OpenIddict.Server +{ + public static partial class OpenIddictServerHandlers + { + public static class Revocation + { + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Revocation request top-level processing: + */ + ExtractRevocationRequest.Descriptor, + ValidateRevocationRequest.Descriptor, + HandleRevocationRequest.Descriptor, + ApplyRevocationResponse.Descriptor, + ApplyRevocationResponse.Descriptor, + + /* + * Revocation request validation: + */ + ValidateTokenParameter.Descriptor, + ValidateClientIdParameter.Descriptor, + ValidateClientId.Descriptor, + ValidateClientSecret.Descriptor, + ValidateEndpointPermissions.Descriptor, + ValidateToken.Descriptor, + ValidateAuthorizedParty.Descriptor, + + /* + * Revocation request handling: + */ + AttachPrincipal.Descriptor, + + /* + * Revocation response handling: + */ + NormalizeErrorResponse.Descriptor); + + /// + /// Contains the logic responsible of extracting revocation requests and invoking the corresponding event handlers. + /// + public class ExtractRevocationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ExtractRevocationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Revocation) + { + return; + } + + var notification = new ExtractRevocationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + if (notification.Request == null) + { + throw new InvalidOperationException(new StringBuilder() + .Append("The revocation request was not correctly extracted. To extract revocation requests, ") + .Append("create a class implementing 'IOpenIddictServerHandler' ") + .AppendLine("and register it using 'services.AddOpenIddict().AddServer().AddEventHandler()'.") + .ToString()); + } + + context.Logger.LogInformation("The revocation request was successfully extracted: {Request}.", notification.Request); + } + } + + /// + /// Contains the logic responsible of validating revocation requests and invoking the corresponding event handlers. + /// + public class ValidateRevocationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateRevocationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ExtractRevocationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Revocation) + { + return; + } + + var notification = new ValidateRevocationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + // Store the security principal extracted from the revoked token as an environment property. + context.Transaction.Properties[Properties.AmbientPrincipal] = notification.Principal; + + context.Logger.LogInformation("The revocation request was successfully validated."); + } + } + + /// + /// Contains the logic responsible of handling revocation requests and invoking the corresponding event handlers. + /// + public class HandleRevocationRequest : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public HandleRevocationRequest([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + .SetOrder(ValidateRevocationRequest.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ProcessRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Revocation) + { + return; + } + + var notification = new HandleRevocationRequestContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + + context.Response = new OpenIddictResponse(); + } + } + + /// + /// Contains the logic responsible of processing sign-in responses and invoking the corresponding event handlers. + /// + public class ApplyRevocationResponse : IOpenIddictServerHandler where TContext : BaseRequestContext + { + private readonly IOpenIddictServerProvider _provider; + + public ApplyRevocationResponse([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler>() + .SetOrder(int.MaxValue - 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] TContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.EndpointType != OpenIddictServerEndpointType.Revocation) + { + return; + } + + var notification = new ApplyRevocationResponseContext(context.Transaction); + await _provider.DispatchAsync(notification); + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting revocation requests that don't specify a token. + /// + public class ValidateTokenParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ValidateRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Reject revocation requests missing the mandatory token parameter. + if (string.IsNullOrEmpty(context.Request.Token)) + { + context.Logger.LogError("The revocation request was rejected because the token was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'token' parameter is missing."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting revocation requests that don't specify a client identifier. + /// + public class ValidateClientIdParameter : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ValidateRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // At this stage, reject the revocation request unless the client identification requirement was disabled. + if (!context.Options.AcceptAnonymousClients && string.IsNullOrEmpty(context.ClientId)) + { + context.Logger.LogError("The revocation request was rejected because the mandatory 'client_id' was missing."); + + context.Reject( + error: Errors.InvalidRequest, + description: "The mandatory 'client_id' parameter is missing."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of rejecting revocation requests that use an invalid client_id. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientId : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientId() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientId([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientIdParameter.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Retrieve the application details corresponding to the requested client_id. + // If no entity can be found, this likely indicates that the client_id is invalid. + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + context.Logger.LogError("The revocation request was rejected because the client " + + "application was not found: '{ClientId}'.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The specified 'client_id' parameter is invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting revocation requests specifying an invalid client secret. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateClientSecret : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateClientSecret() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateClientSecret([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientId.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // If the application is not a public client, validate the client secret. + if (!await _applicationManager.IsPublicAsync(application) && + !await _applicationManager.ValidateClientSecretAsync(application, context.ClientSecret)) + { + context.Logger.LogError("The revocation request was rejected because the confidential or hybrid application " + + "'{ClientId}' didn't specify valid client credentials.", context.ClientId); + + context.Reject( + error: Errors.InvalidClient, + description: "The specified client credentials are invalid."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting revocation requests made by + /// applications that haven't been granted the revocation endpoint permission. + /// Note: this handler is not used when the degraded mode is enabled. + /// + public class ValidateEndpointPermissions : IOpenIddictServerHandler + { + private readonly IOpenIddictApplicationManager _applicationManager; + + public ValidateEndpointPermissions() => throw new InvalidOperationException(new StringBuilder() + .AppendLine("The core services must be registered when enabling the OpenIddict server feature.") + .Append("To register the OpenIddict core services, reference the 'OpenIddict.Core' package ") + .AppendLine("and call 'services.AddOpenIddict().AddCore()' from 'ConfigureServices'.") + .Append("Alternatively, you can disable the built-in database-based server features by enabling ") + .Append("the degraded mode with 'services.AddOpenIddict().AddServer().EnableDegradedMode()'.") + .ToString()); + + public ValidateEndpointPermissions([NotNull] IOpenIddictApplicationManager applicationManager) + => _applicationManager = applicationManager; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .AddFilter() + .UseScopedHandler() + .SetOrder(ValidateClientSecret.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var application = await _applicationManager.FindByClientIdAsync(context.ClientId); + if (application == null) + { + throw new InvalidOperationException("The client application details cannot be found in the database."); + } + + // Reject the request if the application is not allowed to use the revocation endpoint. + if (!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Revocation)) + { + context.Logger.LogError("The revocation request was rejected because the application '{ClientId}' " + + "was not allowed to use the revocation endpoint.", context.ClientId); + + context.Reject( + error: Errors.UnauthorizedClient, + description: "This client application is not allowed to use the revocation endpoint."); + + return; + } + } + } + + /// + /// Contains the logic responsible of rejecting revocation requests that specify an invalid token. + /// + public class ValidateToken : IOpenIddictServerHandler + { + private readonly IOpenIddictServerProvider _provider; + + public ValidateToken([NotNull] IOpenIddictServerProvider provider) + => _provider = provider; + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseScopedHandler() + // This handler is deliberately registered with a high order to ensure it runs + // after custom handlers registered with the default order and prevent the token + // endpoint from disclosing whether the revoked token is valid before + // the caller's identity can first be fully verified by the other handlers. + .SetOrder(100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public async ValueTask HandleAsync([NotNull] ValidateRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Note: use the "token_type_hint" parameter specified by the client application + // to try to determine the type of the token sent by the client application. + // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. + var principal = context.Request.TokenTypeHint switch + { + TokenTypeHints.AccessToken => await DeserializeAccessTokenAsync(), + TokenTypeHints.AuthorizationCode => await DeserializeAuthorizationCodeAsync(), + TokenTypeHints.IdToken => await DeserializeIdentityTokenAsync(), + TokenTypeHints.RefreshToken => await DeserializeRefreshTokenAsync(), + + _ => null + }; + + // Note: if the revoked token can't be found using "token_type_hint", + // the search must be extended to all supported token types. + // See https://tools.ietf.org/html/rfc7662#section-2.1 for more information. + // To avoid calling the same deserialization methods twice, an additional check + // is made to exclude the corresponding call when a token_type_hint was specified. + principal ??= context.Request.TokenTypeHint switch + { + TokenTypeHints.AccessToken => await DeserializeAuthorizationCodeAsync() ?? + await DeserializeIdentityTokenAsync() ?? + await DeserializeRefreshTokenAsync(), + + TokenTypeHints.AuthorizationCode => await DeserializeAccessTokenAsync() ?? + await DeserializeIdentityTokenAsync() ?? + await DeserializeRefreshTokenAsync(), + + TokenTypeHints.IdToken => await DeserializeAccessTokenAsync() ?? + await DeserializeAuthorizationCodeAsync() ?? + await DeserializeRefreshTokenAsync(), + + TokenTypeHints.RefreshToken => await DeserializeAccessTokenAsync() ?? + await DeserializeAuthorizationCodeAsync() ?? + await DeserializeIdentityTokenAsync(), + + _ => await DeserializeAccessTokenAsync() ?? + await DeserializeAuthorizationCodeAsync() ?? + await DeserializeIdentityTokenAsync() ?? + await DeserializeRefreshTokenAsync() + }; + + if (principal == null) + { + context.Logger.LogError("The revocation request was rejected because the token was invalid."); + + context.Reject( + error: Errors.InvalidToken, + description: "The specified token is invalid."); + + return; + } + + var date = principal.GetExpirationDate(); + if (date.HasValue && date.Value < DateTimeOffset.UtcNow) + { + context.Logger.LogError("The revocation request was rejected because the token was expired."); + + context.Reject( + error: Errors.InvalidToken, + description: "The specified token is no longer valid."); + + return; + } + + // Attach the principal extracted from the token to the parent event context. + context.Principal = principal; + + async ValueTask DeserializeAccessTokenAsync() + { + var notification = new DeserializeAccessTokenContext(context.Transaction) + { + Token = context.Request.Token + }; + + await _provider.DispatchAsync(notification); + return notification.Principal; + } + + async ValueTask DeserializeAuthorizationCodeAsync() + { + var notification = new DeserializeAuthorizationCodeContext(context.Transaction) + { + Token = context.Request.Token + }; + + await _provider.DispatchAsync(notification); + return notification.Principal; + } + + async ValueTask DeserializeIdentityTokenAsync() + { + var notification = new DeserializeIdentityTokenContext(context.Transaction) + { + Token = context.Request.Token + }; + + await _provider.DispatchAsync(notification); + return notification.Principal; + } + + async ValueTask DeserializeRefreshTokenAsync() + { + var notification = new DeserializeRefreshTokenContext(context.Transaction) + { + Token = context.Request.Token + }; + + await _provider.DispatchAsync(notification); + return notification.Principal; + } + } + } + + /// + /// Contains the logic responsible of rejecting revocation requests that specify a token + /// that cannot be revoked by the client application sending the revocation requests. + /// + public class ValidateAuthorizedParty : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + // Note: when client identification is not enforced, this handler cannot validate + // the audiences/presenters if the client_id of the calling application is not known. + // In this case, the risk is quite limited as claims are never returned by this endpoint. + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateToken.Descriptor.Order + 1_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ValidateRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // When the revoked token is an authorization code, the caller must be + // listed as a presenter (i.e the party the authorization code was issued to). + if (context.Principal.IsAuthorizationCode()) + { + if (!context.Principal.HasPresenter()) + { + throw new InvalidOperationException("The presenters list cannot be extracted from the authorization code."); + } + + if (!context.Principal.HasPresenter(context.ClientId)) + { + context.Logger.LogError("The revocation request was rejected because the " + + "authorization code was issued to a different client."); + + context.Reject( + error: Errors.InvalidToken, + description: "The client application is not allowed to revoke the specified token."); + + return default; + } + + return default; + } + + // When the revoked token is an access token, the caller must be listed either as a presenter + // (i.e the party the token was issued to) or as an audience (i.e a resource server/API). + // If the access token doesn't contain any explicit presenter/audience, the token is assumed + // to be not specific to any resource server/client application and the check is bypassed. + if (context.Principal.IsAccessToken() && + context.Principal.HasAudience() && !context.Principal.HasAudience(context.ClientId) && + context.Principal.HasPresenter() && !context.Principal.HasPresenter(context.ClientId)) + { + context.Logger.LogError("The revocation request was rejected because the access token " + + "was issued to a different client or for another resource server."); + + context.Reject( + error: Errors.InvalidToken, + description: "The client application is not allowed to revoke the specified token."); + + return default; + } + + // When the revoked token is an identity token, the caller must be listed as an audience + // (i.e the client application the identity token was initially issued to). + // If the identity token doesn't contain any explicit audience, the token is + // assumed to be not specific to any client application and the check is bypassed. + if (context.Principal.IsIdentityToken() && context.Principal.HasAudience() && + !context.Principal.HasAudience(context.ClientId)) + { + context.Logger.LogError("The revocation request was rejected because the " + + "identity token was issued to a different client."); + + context.Reject( + error: Errors.InvalidToken, + description: "The client application is not allowed to revoke the specified token."); + + return default; + } + + // When the revoked token is a refresh token, the caller must be + // listed as a presenter (i.e the party the token was issued to). + // If the refresh token doesn't contain any explicit presenter, the token is + // assumed to be not specific to any client application and the check is bypassed. + if (context.Principal.IsRefreshToken() && context.Principal.HasPresenter() && + !context.Principal.HasPresenter(context.ClientId)) + { + context.Logger.LogError("The revocation request was rejected because the " + + "refresh token was issued to a different client."); + + context.Reject( + error: Errors.InvalidToken, + description: "The client application is not allowed to revoke the specified token."); + + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible of attaching the principal + /// extracted from the revoked token to the event context. + /// + public class AttachPrincipal : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] HandleRevocationRequestContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (context.Transaction.Properties.TryGetValue(Properties.AmbientPrincipal, out var principal)) + { + context.Principal ??= (ClaimsPrincipal) principal; + } + + return default; + } + } + + /// + /// Contains the logic responsible of converting revocation errors to standard empty responses. + /// + public class NormalizeErrorResponse : IOpenIddictServerHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictServerHandlerDescriptor Descriptor { get; } + = OpenIddictServerHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(int.MinValue + 100_000) + .Build(); + + /// + /// Processes the event. + /// + /// The context associated with the event to process. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public ValueTask HandleAsync([NotNull] ApplyRevocationResponseContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Error)) + { + return default; + } + + // If the error indicates an invalid token, remove the error details, as required by the revocation + // specification. Visit https://tools.ietf.org/html/rfc7009#section-2.2 for more information. + // While this prevent the resource server from determining the root cause of the revocation failure, + // this is required to keep OpenIddict fully standard and compatible with all revocation clients. + + if (string.Equals(context.Error, Errors.InvalidToken, StringComparison.Ordinal)) + { + context.Response.Error = null; + context.Response.ErrorDescription = null; + context.Response.ErrorUri = null; + } + + return default; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerHandlers.cs b/src/OpenIddict.Server/OpenIddictServerHandlers.cs index 3114cde6..a66120b8 100644 --- a/src/OpenIddict.Server/OpenIddictServerHandlers.cs +++ b/src/OpenIddict.Server/OpenIddictServerHandlers.cs @@ -53,6 +53,7 @@ namespace OpenIddict.Server .AddRange(Discovery.DefaultHandlers) .AddRange(Exchange.DefaultHandlers) .AddRange(Introspection.DefaultHandlers) + .AddRange(Revocation.DefaultHandlers) .AddRange(Serialization.DefaultHandlers) .AddRange(Session.DefaultHandlers) .AddRange(Userinfo.DefaultHandlers);