/* * 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 System.Diagnostics; using System.Security.Claims; using Microsoft.Extensions.Logging; namespace OpenIddict.Server; public static partial class OpenIddictServerHandlers { public static class Revocation { public static ImmutableArray DefaultHandlers { get; } = [ /* * Revocation request top-level processing: */ ExtractRevocationRequest.Descriptor, ValidateRevocationRequest.Descriptor, HandleRevocationRequest.Descriptor, ApplyRevocationResponse.Descriptor, ApplyRevocationResponse.Descriptor, /* * Revocation request validation: */ ValidateTokenParameter.Descriptor, ValidateClientCredentialsParameters.Descriptor, ValidateAuthentication.Descriptor, ValidateEndpointPermissions.Descriptor, ValidateTokenType.Descriptor, ValidateAuthorizedParty.Descriptor, /* * Revocation request handling: */ AttachPrincipal.Descriptor, RevokeToken.Descriptor, /* * Revocation response handling: */ NormalizeErrorResponse.Descriptor ]; /// /// Contains the logic responsible for extracting revocation requests and invoking the corresponding event handlers. /// public sealed class ExtractRevocationRequest : IOpenIddictServerHandler { private readonly IOpenIddictServerDispatcher _dispatcher; public ExtractRevocationRequest(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() .AddFilter() .UseScopedHandler() .SetOrder(100_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(ProcessRequestContext context) { ArgumentNullException.ThrowIfNull(context); var notification = new ExtractRevocationRequestContext(context.Transaction); await _dispatcher.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 is null) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0048)); } context.Logger.LogInformation(6109, SR.GetResourceString(SR.ID6109), notification.Request); } } /// /// Contains the logic responsible for validating revocation requests and invoking the corresponding event handlers. /// public sealed class ValidateRevocationRequest : IOpenIddictServerHandler { private readonly IOpenIddictServerDispatcher _dispatcher; public ValidateRevocationRequest(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() .AddFilter() .UseScopedHandler() .SetOrder(ExtractRevocationRequest.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(ProcessRequestContext context) { ArgumentNullException.ThrowIfNull(context); var notification = new ValidateRevocationRequestContext(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 principal without triggering a new validation process. context.Transaction.SetProperty(typeof(ValidateRevocationRequestContext).FullName!, 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.Logger.LogInformation(6110, SR.GetResourceString(SR.ID6110)); } } /// /// Contains the logic responsible for handling revocation requests and invoking the corresponding event handlers. /// public sealed class HandleRevocationRequest : IOpenIddictServerHandler { private readonly IOpenIddictServerDispatcher _dispatcher; public HandleRevocationRequest(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() .AddFilter() .UseScopedHandler() .SetOrder(ValidateRevocationRequest.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(ProcessRequestContext context) { ArgumentNullException.ThrowIfNull(context); var notification = new HandleRevocationRequestContext(context.Transaction); await _dispatcher.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.Transaction.Response = new OpenIddictResponse(); } } /// /// Contains the logic responsible for processing sign-in responses and invoking the corresponding event handlers. /// public sealed class ApplyRevocationResponse : IOpenIddictServerHandler where TContext : BaseRequestContext { private readonly IOpenIddictServerDispatcher _dispatcher; public ApplyRevocationResponse(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() .AddFilter() .UseScopedHandler>() .SetOrder(500_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(TContext context) { ArgumentNullException.ThrowIfNull(context); var notification = new ApplyRevocationResponseContext(context.Transaction); await _dispatcher.DispatchAsync(notification); if (notification.IsRequestHandled) { context.HandleRequest(); return; } else if (notification.IsRequestSkipped) { context.SkipRequest(); return; } throw new InvalidOperationException(SR.GetResourceString(SR.ID0049)); } } /// /// Contains the logic responsible for rejecting revocation requests that don't specify a token. /// public sealed 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) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ValidateRevocationRequestContext context) { ArgumentNullException.ThrowIfNull(context); // Reject revocation requests missing the mandatory token parameter. if (string.IsNullOrEmpty(context.Request.Token)) { context.Logger.LogInformation(6111, SR.GetResourceString(SR.ID6111), Parameters.Token); context.Reject( error: Errors.InvalidRequest, description: SR.FormatID2029(Parameters.Token), uri: SR.FormatID8000(SR.ID2029)); return ValueTask.CompletedTask; } return ValueTask.CompletedTask; } } /// /// Contains the logic responsible for rejecting revocation requests that specify invalid client credentials parameters. /// public sealed class ValidateClientCredentialsParameters : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(ValidateTokenParameter.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ValidateRevocationRequestContext context) { ArgumentNullException.ThrowIfNull(context); // Ensure a client_assertion_type is specified when a client_assertion was attached. if (!string.IsNullOrEmpty(context.Request.ClientAssertion) && string.IsNullOrEmpty(context.Request.ClientAssertionType)) { context.Reject( error: Errors.InvalidRequest, description: SR.FormatID2037(Parameters.ClientAssertionType, Parameters.ClientAssertion), uri: SR.FormatID8000(SR.ID2037)); return ValueTask.CompletedTask; } // Ensure a client_assertion is specified when a client_assertion_type was attached. if (string.IsNullOrEmpty(context.Request.ClientAssertion) && !string.IsNullOrEmpty(context.Request.ClientAssertionType)) { context.Reject( error: Errors.InvalidRequest, description: SR.FormatID2037(Parameters.ClientAssertion, Parameters.ClientAssertionType), uri: SR.FormatID8000(SR.ID2037)); return ValueTask.CompletedTask; } // Reject requests that use multiple client authentication methods. // // See https://tools.ietf.org/html/rfc6749#section-2.3 for more information. if (!string.IsNullOrEmpty(context.Request.ClientAssertion) && !string.IsNullOrEmpty(context.Request.ClientSecret)) { context.Logger.LogInformation(6140, SR.GetResourceString(SR.ID6140)); context.Reject( error: Errors.InvalidRequest, description: SR.GetResourceString(SR.ID2087), uri: SR.FormatID8000(SR.ID2087)); return ValueTask.CompletedTask; } // Ensure the specified client_assertion_type is supported. if (!string.IsNullOrEmpty(context.Request.ClientAssertionType) && !context.Options.ClientAssertionTypes.Contains(context.Request.ClientAssertionType)) { context.Reject( error: Errors.InvalidClient, description: SR.FormatID2032(Parameters.ClientAssertionType), uri: SR.FormatID8000(SR.ID2032)); return ValueTask.CompletedTask; } return ValueTask.CompletedTask; } } /// /// Contains the logic responsible for applying the authentication logic to revocation requests. /// public sealed class ValidateAuthentication : IOpenIddictServerHandler { private readonly IOpenIddictServerDispatcher _dispatcher; public ValidateAuthentication(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() .SetOrder(ValidateClientCredentialsParameters.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(ValidateRevocationRequestContext context) { ArgumentNullException.ThrowIfNull(context); var notification = new ProcessAuthenticationContext(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 authentication result without triggering a new authentication flow. context.Transaction.SetProperty(typeof(ProcessAuthenticationContext).FullName!, 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; } // Attach the security principal extracted from the token to the validation context. context.GenericTokenPrincipal = notification.GenericTokenPrincipal; } } /// /// Contains the logic responsible for 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 sealed class ValidateEndpointPermissions : IOpenIddictServerHandler { private readonly IOpenIddictApplicationManager _applicationManager; public ValidateEndpointPermissions() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); public ValidateEndpointPermissions(IOpenIddictApplicationManager applicationManager) => _applicationManager = applicationManager ?? throw new ArgumentNullException(nameof(applicationManager)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() .AddFilter() .UseScopedHandler() .SetOrder(ValidateAuthentication.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(ValidateRevocationRequestContext context) { ArgumentNullException.ThrowIfNull(context); Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); var application = await _applicationManager.FindByClientIdAsync(context.ClientId) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0032)); // Reject the request if the application is not allowed to use the revocation endpoint. if (!await _applicationManager.HasPermissionAsync(application, Permissions.Endpoints.Revocation)) { context.Logger.LogInformation(6116, SR.GetResourceString(SR.ID6116), context.ClientId); context.Reject( error: Errors.UnauthorizedClient, description: SR.GetResourceString(SR.ID2078), uri: SR.FormatID8000(SR.ID2078)); return; } } } /// /// Contains the logic responsible for rejecting revocation requests that specify an unsupported token. /// public sealed class ValidateTokenType : IOpenIddictServerHandler { /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .UseSingletonHandler() .SetOrder(ValidateEndpointPermissions.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ValidateRevocationRequestContext context) { ArgumentNullException.ThrowIfNull(context); Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); if (!context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken) && !context.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.RefreshToken)) { context.Logger.LogInformation(6117, SR.GetResourceString(SR.ID6117)); context.Reject( error: Errors.UnsupportedTokenType, description: SR.GetResourceString(SR.ID2079), uri: SR.FormatID8000(SR.ID2079)); return ValueTask.CompletedTask; } return ValueTask.CompletedTask; } } /// /// Contains the logic responsible for rejecting revocation requests that specify a token /// that cannot be revoked by the client application sending the revocation request. /// public sealed 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(ValidateTokenType.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ValidateRevocationRequestContext context) { ArgumentNullException.ThrowIfNull(context); Debug.Assert(!string.IsNullOrEmpty(context.ClientId), SR.FormatID4000(Parameters.ClientId)); Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); // 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.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.AccessToken) && context.GenericTokenPrincipal.HasClaim(Claims.Private.Audience) && !context.GenericTokenPrincipal.HasAudience(context.ClientId) && context.GenericTokenPrincipal.HasClaim(Claims.Private.Presenter) && !context.GenericTokenPrincipal.HasPresenter(context.ClientId)) { context.Logger.LogWarning(6119, SR.GetResourceString(SR.ID6119)); context.Reject( error: Errors.InvalidToken, description: SR.GetResourceString(SR.ID2080), uri: SR.FormatID8000(SR.ID2080)); return ValueTask.CompletedTask; } // 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.GenericTokenPrincipal.HasTokenType(TokenTypeIdentifiers.RefreshToken) && context.GenericTokenPrincipal.HasClaim(Claims.Private.Presenter) && !context.GenericTokenPrincipal.HasPresenter(context.ClientId)) { context.Logger.LogWarning(6121, SR.GetResourceString(SR.ID6121)); context.Reject( error: Errors.InvalidToken, description: SR.GetResourceString(SR.ID2080), uri: SR.FormatID8000(SR.ID2080)); return ValueTask.CompletedTask; } return ValueTask.CompletedTask; } } /// /// Contains the logic responsible for attaching the principal /// extracted from the revoked token to the event context. /// public sealed 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) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(HandleRevocationRequestContext context) { ArgumentNullException.ThrowIfNull(context); var notification = context.Transaction.GetProperty( typeof(ValidateRevocationRequestContext).FullName!) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0007)); Debug.Assert(notification.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); context.GenericTokenPrincipal ??= notification.GenericTokenPrincipal; return ValueTask.CompletedTask; } } /// /// Contains the logic responsible for revoking the token sent by the client application. /// Note: this handler is not used when the degraded mode is enabled. /// public sealed class RevokeToken : IOpenIddictServerHandler { private readonly IOpenIddictTokenManager _tokenManager; public RevokeToken() => throw new InvalidOperationException(SR.GetResourceString(SR.ID0016)); public RevokeToken(IOpenIddictTokenManager tokenManager) => _tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager)); /// /// Gets the default descriptor definition assigned to this handler. /// public static OpenIddictServerHandlerDescriptor Descriptor { get; } = OpenIddictServerHandlerDescriptor.CreateBuilder() .AddFilter() .UseScopedHandler() .SetOrder(AttachPrincipal.Descriptor.Order + 1_000) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public async ValueTask HandleAsync(HandleRevocationRequestContext context) { ArgumentNullException.ThrowIfNull(context); Debug.Assert(context.GenericTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); // Extract the token identifier from the authentication principal. var identifier = context.GenericTokenPrincipal.GetTokenId(); if (string.IsNullOrEmpty(identifier)) { context.Logger.LogInformation(6122, SR.GetResourceString(SR.ID6122)); context.Reject( error: Errors.UnsupportedTokenType, description: SR.GetResourceString(SR.ID2079), uri: SR.FormatID8000(SR.ID2079)); return; } var token = await _tokenManager.FindByIdAsync(identifier); if (token is null) { context.Logger.LogInformation(6123, SR.GetResourceString(SR.ID6123), identifier); context.Reject( error: Errors.InvalidToken, description: SR.GetResourceString(SR.ID2004), uri: SR.FormatID8000(SR.ID2004)); return; } // Try to revoke the token. If an error occurs, return an error. if (!await _tokenManager.TryRevokeAsync(token)) { context.Reject( error: Errors.UnsupportedTokenType, description: SR.GetResourceString(SR.ID2079), uri: SR.FormatID8000(SR.ID2079)); return; } } } /// /// Contains the logic responsible for converting revocation errors to standard empty responses. /// public sealed 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) .SetType(OpenIddictServerHandlerType.BuiltIn) .Build(); /// public ValueTask HandleAsync(ApplyRevocationResponseContext context) { ArgumentNullException.ThrowIfNull(context); if (string.IsNullOrEmpty(context.Error)) { return ValueTask.CompletedTask; } // 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 ValueTask.CompletedTask; } } } }