diff --git a/src/OpenIddict.Abstractions/OpenIddictBuilder.cs b/src/OpenIddict.Abstractions/OpenIddictBuilder.cs index 88acb436..ab10dd05 100644 --- a/src/OpenIddict.Abstractions/OpenIddictBuilder.cs +++ b/src/OpenIddict.Abstractions/OpenIddictBuilder.cs @@ -35,4 +35,4 @@ namespace Microsoft.Extensions.DependencyInjection [EditorBrowsable(EditorBrowsableState.Never)] public IServiceCollection Services { get; } } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs index 73fd5546..25112337 100644 --- a/src/OpenIddict.Core/OpenIddictCoreBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictCoreBuilder.cs @@ -779,4 +779,4 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.DefaultTokenType = type); } } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/IOpenIddictServerEvent.cs b/src/OpenIddict.Server/IOpenIddictServerEvent.cs new file mode 100644 index 00000000..6b371320 --- /dev/null +++ b/src/OpenIddict.Server/IOpenIddictServerEvent.cs @@ -0,0 +1,7 @@ +namespace OpenIddict.Server +{ + /// + /// Represents an OpenIddict server event. + /// + public interface IOpenIddictServerEvent { } +} diff --git a/src/OpenIddict.Server/IOpenIddictServerEventHandler.cs b/src/OpenIddict.Server/IOpenIddictServerEventHandler.cs new file mode 100644 index 00000000..d620c2ff --- /dev/null +++ b/src/OpenIddict.Server/IOpenIddictServerEventHandler.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + /// + /// Represents a handler able to process events. + /// + /// The type of the events handled by this instance. + public interface IOpenIddictServerEventHandler where TEvent : class, IOpenIddictServerEvent + { + /// + /// Processes the event. + /// + /// The event to process. + /// + /// The that can be used to abort the operation. + /// + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task HandleAsync([NotNull] TEvent notification, CancellationToken cancellationToken); + } +} diff --git a/src/OpenIddict.Server/IOpenIddictServerEventService.cs b/src/OpenIddict.Server/IOpenIddictServerEventService.cs new file mode 100644 index 00000000..0549bf1f --- /dev/null +++ b/src/OpenIddict.Server/IOpenIddictServerEventService.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + /// + /// Dispatches events by invoking the corresponding handlers. + /// + public interface IOpenIddictServerEventService + { + /// + /// Publishes a new event. + /// + /// The type of the event to publish. + /// The event to publish. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + Task PublishAsync([NotNull] TEvent notification, CancellationToken cancellationToken = default) + where TEvent : class, IOpenIddictServerEvent; + } +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerHandler.cs b/src/OpenIddict.Server/Internal/OpenIddictServerHandler.cs index d4702720..79405eec 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerHandler.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerHandler.cs @@ -1,8 +1,5 @@ -using System; -using System.ComponentModel; -using System.Text; +using System.ComponentModel; using System.Text.Encodings.Web; -using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; @@ -22,36 +19,5 @@ namespace OpenIddict.Server : base(options, logger, encoder, clock) { } - - protected override async Task InitializeEventsAsync() - { - await base.InitializeEventsAsync(); - - // If an application provider instance or type was specified, import the application provider events. - if (Options.ApplicationProvider != null || Options.ApplicationProviderType != null) - { - // Resolve the user provider from the options or from the services container. - var provider = Options.ApplicationProvider; - if (provider == null) - { - provider = Context.RequestServices.GetService(Options.ApplicationProviderType) as OpenIdConnectServerProvider; - } - - if (provider == null) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The application provider cannot be resolved from the dependency injection container. ") - .Append("Make sure it is correctly registered in 'ConfigureServices(IServiceCollection services)'.") - .ToString()); - } - - // Update the main provider to invoke the user provider's event handlers. - Provider.Import(provider); - } - } - - private new OpenIddictServerOptions Options => (OpenIddictServerOptions) base.Options; - - private OpenIddictServerProvider Provider => (OpenIddictServerProvider) base.Events; } } diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs b/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs index 1f908f0f..4556ccd9 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerInitializer.cs @@ -63,19 +63,6 @@ namespace OpenIddict.Server throw new InvalidOperationException("A random number generator must be registered."); } - if (options.ApplicationProviderType != null) - { - if (options.ApplicationProvider != null) - { - throw new InvalidOperationException("An application provider cannot be registered when a type is specified."); - } - - if (!typeof(OpenIdConnectServerProvider).IsAssignableFrom(options.ApplicationProviderType)) - { - throw new InvalidOperationException("Application providers must inherit from OpenIdConnectServerProvider."); - } - } - // When no distributed cache has been registered in the options, // try to resolve it from the dependency injection container. if (options.Cache == null) diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs index e519ffc9..fd7fcb51 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Authentication.cs @@ -107,7 +107,7 @@ namespace OpenIddict.Server } } - await base.ExtractAuthorizationRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ExtractAuthorizationRequest(context)); } public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) @@ -433,7 +433,7 @@ namespace OpenIddict.Server context.Validate(); - await base.ValidateAuthorizationRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ValidateAuthorizationRequest(context)); } public override async Task HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context) @@ -485,7 +485,7 @@ namespace OpenIddict.Server return; } - await base.HandleAuthorizationRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.HandleAuthorizationRequest(context)); } public override async Task ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context) @@ -526,7 +526,7 @@ namespace OpenIddict.Server } } - await base.ApplyAuthorizationResponse(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ApplyAuthorizationResponse(context)); } } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Discovery.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Discovery.cs index 6dba5d7f..174eb8dc 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Discovery.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Discovery.cs @@ -13,6 +13,12 @@ namespace OpenIddict.Server { public partial class OpenIddictServerProvider : OpenIdConnectServerProvider { + public override Task ExtractConfigurationRequest([NotNull] ExtractConfigurationRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ExtractConfigurationRequest(context)); + + public override Task ValidateConfigurationRequest([NotNull] ValidateConfigurationRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ValidateConfigurationRequest(context)); + public override Task HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context) { var options = (OpenIddictServerOptions) context.Options; @@ -41,7 +47,22 @@ namespace OpenIddict.Server context.Metadata[OpenIdConnectConstants.Metadata.RequestParameterSupported] = false; context.Metadata[OpenIdConnectConstants.Metadata.RequestUriParameterSupported] = false; - return base.HandleConfigurationRequest(context); + return _eventService.PublishAsync(new OpenIddictServerEvents.HandleConfigurationRequest(context)); } + + public override Task ApplyConfigurationResponse([NotNull] ApplyConfigurationResponseContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ApplyConfigurationResponse(context)); + + public override Task ExtractCryptographyRequest([NotNull] ExtractCryptographyRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ExtractCryptographyRequest(context)); + + public override Task ValidateCryptographyRequest([NotNull] ValidateCryptographyRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ValidateCryptographyRequest(context)); + + public override Task HandleCryptographyRequest([NotNull] HandleCryptographyRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.HandleCryptographyRequest(context)); + + public override Task ApplyCryptographyResponse([NotNull] ApplyCryptographyResponseContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ApplyCryptographyResponse(context)); } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs index 664fa0bb..c8d3bbe6 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Exchange.cs @@ -20,6 +20,9 @@ namespace OpenIddict.Server { public partial class OpenIddictServerProvider : OpenIdConnectServerProvider { + public override Task ExtractTokenRequest([NotNull] ExtractTokenRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ExtractTokenRequest(context)); + public override async Task ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) { var options = (OpenIddictServerOptions) context.Options; @@ -289,7 +292,7 @@ namespace OpenIddict.Server context.Validate(); - await base.ValidateTokenRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ValidateTokenRequest(context)); } public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context) @@ -308,7 +311,7 @@ namespace OpenIddict.Server // the user code to handle the token request. context.SkipHandler(); - await base.HandleTokenRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.HandleTokenRequest(context)); return; } @@ -400,7 +403,10 @@ namespace OpenIddict.Server // the user code to handle the token request. context.SkipHandler(); - await base.HandleTokenRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.HandleTokenRequest(context)); } + + public override Task ApplyTokenResponse([NotNull] ApplyTokenResponseContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ApplyTokenResponse(context)); } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs index fa2594a1..62680912 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Helpers.cs @@ -648,4 +648,4 @@ namespace OpenIddict.Server } } } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs index e33d9bf5..ee3fde91 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Introspection.cs @@ -17,6 +17,9 @@ namespace OpenIddict.Server { public partial class OpenIddictServerProvider : OpenIdConnectServerProvider { + public override Task ExtractIntrospectionRequest([NotNull] ExtractIntrospectionRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ExtractIntrospectionRequest(context)); + public override async Task ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) { var options = (OpenIddictServerOptions) context.Options; @@ -94,7 +97,7 @@ namespace OpenIddict.Server context.Validate(); - await base.ValidateIntrospectionRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ValidateIntrospectionRequest(context)); } public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) @@ -177,7 +180,10 @@ namespace OpenIddict.Server } } - await base.HandleIntrospectionRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.HandleIntrospectionRequest(context)); } + + public override Task ApplyIntrospectionResponse([NotNull] ApplyIntrospectionResponseContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ApplyIntrospectionResponse(context)); } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs index 1f69dcba..e7881fd3 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Revocation.cs @@ -18,6 +18,9 @@ namespace OpenIddict.Server { public partial class OpenIddictServerProvider : OpenIdConnectServerProvider { + public override Task ExtractRevocationRequest([NotNull] ExtractRevocationRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ExtractRevocationRequest(context)); + public override async Task ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context) { var options = (OpenIddictServerOptions) context.Options; @@ -160,7 +163,7 @@ namespace OpenIddict.Server context.Validate(); - await base.ValidateRevocationRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ValidateRevocationRequest(context)); } public override async Task HandleRevocationRequest([NotNull] HandleRevocationRequestContext context) @@ -232,7 +235,10 @@ namespace OpenIddict.Server context.Revoked = true; - await base.HandleRevocationRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.HandleRevocationRequest(context)); } + + public override Task ApplyRevocationResponse([NotNull] ApplyRevocationResponseContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ApplyRevocationResponse(context)); } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs index 838eb32f..a06dbcb0 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Serialization.cs @@ -34,7 +34,7 @@ namespace OpenIddict.Server context.HandleDeserialization(); } - await base.DeserializeAccessToken(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.DeserializeAccessToken(context)); } public override async Task DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context) @@ -53,9 +53,12 @@ namespace OpenIddict.Server // Prevent the OpenID Connect server middleware from using its default logic. context.HandleDeserialization(); - await base.DeserializeAuthorizationCode(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.DeserializeAuthorizationCode(context)); } + public override Task DeserializeIdentityToken(DeserializeIdentityTokenContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.DeserializeIdentityToken(context)); + public override async Task DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context) { var options = (OpenIddictServerOptions) context.Options; @@ -72,7 +75,7 @@ namespace OpenIddict.Server // Prevent the OpenID Connect server middleware from using its default logic. context.HandleDeserialization(); - await base.DeserializeRefreshToken(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.DeserializeRefreshToken(context)); } public override async Task SerializeAccessToken([NotNull] SerializeAccessTokenContext context) @@ -98,7 +101,7 @@ namespace OpenIddict.Server // Otherwise, let the OpenID Connect server middleware // serialize the token using its default internal logic. - await base.SerializeAccessToken(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.SerializeAccessToken(context)); } public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) @@ -126,9 +129,12 @@ namespace OpenIddict.Server // Otherwise, let the OpenID Connect server middleware // serialize the token using its default internal logic. - await base.SerializeAuthorizationCode(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.SerializeAuthorizationCode(context)); } + public override Task SerializeIdentityToken(SerializeIdentityTokenContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.SerializeIdentityToken(context)); + public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { var options = (OpenIddictServerOptions) context.Options; @@ -154,7 +160,7 @@ namespace OpenIddict.Server // Otherwise, let the OpenID Connect server middleware // serialize the token using its default internal logic. - await base.SerializeRefreshToken(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.SerializeRefreshToken(context)); } } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Session.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Session.cs index 7feaf96a..2bd4c543 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Session.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Session.cs @@ -77,7 +77,7 @@ namespace OpenIddict.Server } } - await base.ExtractLogoutRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ExtractLogoutRequest(context)); } public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context) @@ -151,7 +151,7 @@ namespace OpenIddict.Server context.Validate(); - await base.ValidateLogoutRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ValidateLogoutRequest(context)); } public override async Task HandleLogoutRequest([NotNull] HandleLogoutRequestContext context) @@ -203,7 +203,7 @@ namespace OpenIddict.Server return; } - await base.HandleLogoutRequest(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.HandleLogoutRequest(context)); } public override async Task ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context) @@ -244,7 +244,7 @@ namespace OpenIddict.Server } } - await base.ApplyLogoutResponse(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ApplyLogoutResponse(context)); } } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Userinfo.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Userinfo.cs index 577bfad0..c28196f3 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Userinfo.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.Userinfo.cs @@ -24,7 +24,16 @@ namespace OpenIddict.Server // the user code to handle the userinfo request. context.SkipHandler(); - return base.ExtractUserinfoRequest(context); + return _eventService.PublishAsync(new OpenIddictServerEvents.ExtractUserinfoRequest(context)); } + + public override Task ValidateUserinfoRequest([NotNull] ValidateUserinfoRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ValidateUserinfoRequest(context)); + + public override Task HandleUserinfoRequest([NotNull] HandleUserinfoRequestContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.HandleUserinfoRequest(context)); + + public override Task ApplyUserinfoResponse([NotNull] ApplyUserinfoResponseContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.ApplyUserinfoResponse(context)); } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs index 868dfcb9..5db8fcbf 100644 --- a/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs +++ b/src/OpenIddict.Server/Internal/OpenIddictServerProvider.cs @@ -24,26 +24,32 @@ namespace OpenIddict.Server [EditorBrowsable(EditorBrowsableState.Never)] public partial class OpenIddictServerProvider : OpenIdConnectServerProvider { - public readonly ILogger _logger; - public readonly IOpenIddictApplicationManager _applicationManager; - public readonly IOpenIddictAuthorizationManager _authorizationManager; - public readonly IOpenIddictScopeManager _scopeManager; - public readonly IOpenIddictTokenManager _tokenManager; + private readonly ILogger _logger; + private readonly IOpenIddictServerEventService _eventService; + private readonly IOpenIddictApplicationManager _applicationManager; + private readonly IOpenIddictAuthorizationManager _authorizationManager; + private readonly IOpenIddictScopeManager _scopeManager; + private readonly IOpenIddictTokenManager _tokenManager; public OpenIddictServerProvider( [NotNull] ILogger logger, + [NotNull] IOpenIddictServerEventService eventService, [NotNull] IOpenIddictApplicationManager applicationManager, [NotNull] IOpenIddictAuthorizationManager authorizationManager, [NotNull] IOpenIddictScopeManager scopeManager, [NotNull] IOpenIddictTokenManager tokenManager) { _logger = logger; + _eventService = eventService; _applicationManager = applicationManager; _authorizationManager = authorizationManager; _scopeManager = scopeManager; _tokenManager = tokenManager; } + public override Task MatchEndpoint([NotNull] MatchEndpointContext context) + => _eventService.PublishAsync(new OpenIddictServerEvents.MatchEndpoint(context)); + public override Task ProcessChallengeResponse([NotNull] ProcessChallengeResponseContext context) { Debug.Assert(context.Request.IsAuthorizationRequest() || @@ -58,7 +64,7 @@ namespace OpenIddict.Server context.Response.AddParameter(parameter, value); } - return base.ProcessChallengeResponse(context); + return _eventService.PublishAsync(new OpenIddictServerEvents.ProcessChallengeResponse(context)); } public override async Task ProcessSigninResponse([NotNull] ProcessSigninResponseContext context) @@ -188,7 +194,7 @@ namespace OpenIddict.Server context.Ticket.RemoveProperty(property); } - await base.ProcessSigninResponse(context); + await _eventService.PublishAsync(new OpenIddictServerEvents.ProcessSigninResponse(context)); } public override Task ProcessSignoutResponse([NotNull] ProcessSignoutResponseContext context) @@ -202,61 +208,7 @@ namespace OpenIddict.Server context.Response.AddParameter(parameter, value); } - return base.ProcessSignoutResponse(context); - } - - public void Import([NotNull] OpenIdConnectServerProvider provider) - { - OnMatchEndpoint = provider.MatchEndpoint; - - OnExtractAuthorizationRequest = provider.ExtractAuthorizationRequest; - OnExtractConfigurationRequest = provider.ExtractConfigurationRequest; - OnExtractCryptographyRequest = provider.ExtractCryptographyRequest; - OnExtractIntrospectionRequest = provider.ExtractIntrospectionRequest; - OnExtractLogoutRequest = provider.ExtractLogoutRequest; - OnExtractRevocationRequest = provider.ExtractRevocationRequest; - OnExtractTokenRequest = provider.ExtractTokenRequest; - OnExtractUserinfoRequest = provider.ExtractUserinfoRequest; - OnValidateAuthorizationRequest = provider.ValidateAuthorizationRequest; - OnValidateConfigurationRequest = provider.ValidateConfigurationRequest; - OnValidateCryptographyRequest = provider.ValidateCryptographyRequest; - OnValidateIntrospectionRequest = provider.ValidateIntrospectionRequest; - OnValidateLogoutRequest = provider.ValidateLogoutRequest; - OnValidateRevocationRequest = provider.ValidateRevocationRequest; - OnValidateTokenRequest = provider.ValidateTokenRequest; - OnValidateUserinfoRequest = provider.ValidateUserinfoRequest; - - OnHandleAuthorizationRequest = provider.HandleAuthorizationRequest; - OnHandleConfigurationRequest = provider.HandleConfigurationRequest; - OnHandleCryptographyRequest = provider.HandleCryptographyRequest; - OnHandleIntrospectionRequest = provider.HandleIntrospectionRequest; - OnHandleLogoutRequest = provider.HandleLogoutRequest; - OnHandleRevocationRequest = provider.HandleRevocationRequest; - OnHandleTokenRequest = provider.HandleTokenRequest; - OnHandleUserinfoRequest = provider.HandleUserinfoRequest; - - OnApplyAuthorizationResponse = provider.ApplyAuthorizationResponse; - OnApplyConfigurationResponse = provider.ApplyConfigurationResponse; - OnApplyCryptographyResponse = provider.ApplyCryptographyResponse; - OnApplyIntrospectionResponse = provider.ApplyIntrospectionResponse; - OnApplyLogoutResponse = provider.ApplyLogoutResponse; - OnApplyRevocationResponse = provider.ApplyRevocationResponse; - OnApplyTokenResponse = provider.ApplyTokenResponse; - OnApplyUserinfoResponse = provider.ApplyUserinfoResponse; - - OnProcessChallengeResponse = provider.ProcessChallengeResponse; - OnProcessSigninResponse = provider.ProcessSigninResponse; - OnProcessSignoutResponse = provider.ProcessSignoutResponse; - - OnDeserializeAccessToken = provider.DeserializeAccessToken; - OnDeserializeAuthorizationCode = provider.DeserializeAuthorizationCode; - OnDeserializeIdentityToken = provider.DeserializeIdentityToken; - OnDeserializeRefreshToken = provider.DeserializeRefreshToken; - - OnSerializeAccessToken = provider.SerializeAccessToken; - OnSerializeAuthorizationCode = provider.SerializeAuthorizationCode; - OnSerializeIdentityToken = provider.SerializeIdentityToken; - OnSerializeRefreshToken = provider.SerializeRefreshToken; + return _eventService.PublishAsync(new OpenIddictServerEvents.ProcessSignoutResponse(context)); } } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/OpenIddictServerBuilder.cs b/src/OpenIddict.Server/OpenIddictServerBuilder.cs index 2ce9161f..e2749734 100644 --- a/src/OpenIddict.Server/OpenIddictServerBuilder.cs +++ b/src/OpenIddict.Server/OpenIddictServerBuilder.cs @@ -12,12 +12,12 @@ using System.IO; using System.Linq; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.IdentityModel.Tokens; using OpenIddict.Server; @@ -48,6 +48,98 @@ namespace Microsoft.Extensions.DependencyInjection [EditorBrowsable(EditorBrowsableState.Never)] public IServiceCollection Services { get; } + /// + /// Registers an event handler for the specified event type. + /// + /// The handler added to the DI container. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictServerBuilder AddEventHandler( + [NotNull] IOpenIddictServerEventHandler handler) + where TEvent : class, IOpenIddictServerEvent + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + Services.AddSingleton(handler); + + return this; + } + + /// + /// Registers an event handler for the specified event type. + /// + /// The handler added to the DI container. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictServerBuilder AddEventHandler([NotNull] Func handler) + where TEvent : class, IOpenIddictServerEvent + => AddEventHandler((notification, cancellationToken) => handler(notification)); + + /// + /// Registers an event handler for the specified event type. + /// + /// The handler added to the DI container. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictServerBuilder AddEventHandler([NotNull] Func handler) + where TEvent : class, IOpenIddictServerEvent + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + return AddEventHandler(new OpenIddictServerEventHandler(handler)); + } + + /// + /// Registers an event handler for the specified event type. + /// + /// The type of the event. + /// The type of the handler. + /// The lifetime of the registered service. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictServerBuilder AddEventHandler( + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TEvent : class, IOpenIddictServerEvent + where THandler : IOpenIddictServerEventHandler + => AddEventHandler(typeof(THandler)); + + /// + /// Registers an event handler for the specified event type. + /// + /// The type of the handler. + /// The lifetime of the registered service. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictServerBuilder AddEventHandler( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TEvent : class, IOpenIddictServerEvent + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (lifetime == ServiceLifetime.Transient) + { + throw new ArgumentException("Handlers cannot be registered as transient services.", nameof(lifetime)); + } + + if (!typeof(IOpenIddictServerEventHandler).IsAssignableFrom(type)) + { + throw new ArgumentException("The specified type is invalid.", nameof(type)); + } + + Services.Add(new ServiceDescriptor(typeof(IOpenIddictServerEventHandler), type, lifetime)); + + return this; + } + /// /// Amends the default OpenIddict server configuration. /// @@ -184,7 +276,7 @@ namespace Microsoft.Extensions.DependencyInjection if (string.IsNullOrEmpty(password)) { - throw new ArgumentNullException(nameof(password)); + throw new ArgumentException("The password cannot be null or empty.", nameof(password)); } return Configure(options => options.SigningCredentials.AddCertificate(assembly, resource, password)); @@ -206,7 +298,7 @@ namespace Microsoft.Extensions.DependencyInjection if (string.IsNullOrEmpty(password)) { - throw new ArgumentNullException(nameof(password)); + throw new ArgumentException("The password cannot be null or empty.", nameof(password)); } return Configure(options => options.SigningCredentials.AddCertificate(stream, password)); @@ -233,7 +325,7 @@ namespace Microsoft.Extensions.DependencyInjection if (string.IsNullOrEmpty(password)) { - throw new ArgumentNullException(nameof(password)); + throw new ArgumentException("The password cannot be null or empty.", nameof(password)); } return Configure(options => options.SigningCredentials.AddCertificate(stream, password, flags)); @@ -552,60 +644,6 @@ namespace Microsoft.Extensions.DependencyInjection return Configure(options => options.Claims.UnionWith(claims)); } - /// - /// Registers an application-specific OpenID Connect server provider whose events - /// are automatically invoked for each request handled by the OpenIddict server handler. - /// Using this method is NOT recommended if you're not familiar with the OIDC events model. - /// - /// The custom service. - /// The . - [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder RegisterProvider([NotNull] OpenIdConnectServerProvider provider) - { - if (provider == null) - { - throw new ArgumentNullException(nameof(provider)); - } - - return Configure(options => options.ApplicationProvider = provider); - } - - /// - /// Registers an application-specific OpenID Connect server provider whose events - /// are automatically invoked for each request handled by the OpenIddict server handler. - /// Using this method is NOT recommended if you're not familiar with the OIDC events model. - /// - /// The type of the custom service. - /// The . - [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder RegisterProvider() where TProvider : OpenIdConnectServerProvider - => RegisterProvider(typeof(TProvider)); - - /// - /// Registers an application-specific OpenID Connect server provider whose events - /// are automatically invoked for each request handled by the OpenIddict server handler. - /// Using this method is NOT recommended if you're not familiar with the OIDC events model. - /// - /// The type of the custom service. - /// The . - [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictServerBuilder RegisterProvider([NotNull] Type type) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - if (!typeof(OpenIdConnectServerProvider).IsAssignableFrom(type)) - { - throw new ArgumentException("The specified type is invalid.", nameof(type)); - } - - Services.TryAddScoped(type); - - return Configure(options => options.ApplicationProviderType = type); - } - /// /// Registers the specified scopes as supported scopes so /// they can be returned as part of the discovery document. @@ -735,4 +773,4 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictServerBuilder UseRollingTokens() => Configure(options => options.UseRollingTokens = true); } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvent.cs b/src/OpenIddict.Server/OpenIddictServerEvent.cs new file mode 100644 index 00000000..bc6d0b34 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvent.cs @@ -0,0 +1,24 @@ +using System; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + /// + /// Represents an OpenIddict server event. + /// + /// The type of the context instance associated with the event. + public class OpenIddictServerEvent : IOpenIddictServerEvent where TContext : class + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the event. + public OpenIddictServerEvent([NotNull] TContext context) + => Context = context ?? throw new ArgumentNullException(nameof(context)); + + /// + /// Gets the context instance associated with the event. + /// + public TContext Context { get; } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEventHandler.cs b/src/OpenIddict.Server/OpenIddictServerEventHandler.cs new file mode 100644 index 00000000..727353ec --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEventHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + /// + /// Represents a handler able to process events. + /// + /// The type of the events handled by this instance. + public class OpenIddictServerEventHandler : IOpenIddictServerEventHandler + where TEvent : class, IOpenIddictServerEvent + { + private readonly Func _handler; + + /// + /// Creates a new event using the specified handler delegate. + /// + /// The event handler delegate + public OpenIddictServerEventHandler([NotNull] Func handler) + => _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + + /// + /// Processes the event. + /// + /// The event to process. + /// + /// The that can be used to abort the operation. + /// + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync(TEvent notification, CancellationToken cancellationToken) + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return _handler.Invoke(notification, cancellationToken); + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEventService.cs b/src/OpenIddict.Server/OpenIddictServerEventService.cs new file mode 100644 index 00000000..4f359301 --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEventService.cs @@ -0,0 +1,150 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Primitives; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using static OpenIddict.Server.OpenIddictServerEvents; + +namespace OpenIddict.Server +{ + /// + /// Dispatches notifications by invoking the corresponding handlers. + /// + public class OpenIddictServerEventService : IOpenIddictServerEventService + { + private readonly IServiceProvider _provider; + + public OpenIddictServerEventService([NotNull] IServiceProvider provider) + { + _provider = provider; + } + + /// + /// Publishes a new event. + /// + /// The type of the event to publish. + /// The event to publish. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public async Task PublishAsync([NotNull] TEvent notification, CancellationToken cancellationToken = default) + where TEvent : class, IOpenIddictServerEvent + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + foreach (var handler in _provider.GetServices>()) + { + cancellationToken.ThrowIfCancellationRequested(); + + await handler.HandleAsync(notification, cancellationToken); + + // Note: the following logic determines whether next handlers should be invoked + // depending on whether the underlying event context was substantially updated. + switch (notification) + { + case MatchEndpoint value when value.Context.Result != null: return; + case MatchEndpoint value when value.Context.IsAuthorizationEndpoint || + value.Context.IsConfigurationEndpoint || + value.Context.IsCryptographyEndpoint || + value.Context.IsIntrospectionEndpoint || + value.Context.IsLogoutEndpoint || + value.Context.IsRevocationEndpoint || + value.Context.IsTokenEndpoint || + value.Context.IsUserinfoEndpoint: return; + + case ExtractAuthorizationRequest value when value.Context.Result != null: return; + case ExtractConfigurationRequest value when value.Context.Result != null: return; + case ExtractCryptographyRequest value when value.Context.Result != null: return; + case ExtractIntrospectionRequest value when value.Context.Result != null: return; + case ExtractLogoutRequest value when value.Context.Result != null: return; + case ExtractRevocationRequest value when value.Context.Result != null: return; + case ExtractTokenRequest value when value.Context.Result != null: return; + case ExtractUserinfoRequest value when value.Context.Result != null: return; + + case ValidateAuthorizationRequest value when value.Context.Result != null: return; + case ValidateConfigurationRequest value when value.Context.Result != null: return; + case ValidateCryptographyRequest value when value.Context.Result != null: return; + case ValidateIntrospectionRequest value when value.Context.Result != null: return; + case ValidateLogoutRequest value when value.Context.Result != null: return; + case ValidateRevocationRequest value when value.Context.Result != null: return; + case ValidateTokenRequest value when value.Context.Result != null: return; + case ValidateUserinfoRequest value when value.Context.Result != null: return; + + case ValidateAuthorizationRequest value when value.Context.IsRejected: return; + case ValidateConfigurationRequest value when value.Context.IsRejected: return; + case ValidateCryptographyRequest value when value.Context.IsRejected: return; + case ValidateIntrospectionRequest value when value.Context.IsRejected: return; + case ValidateLogoutRequest value when value.Context.IsRejected: return; + case ValidateRevocationRequest value when value.Context.IsRejected: return; + case ValidateTokenRequest value when value.Context.IsRejected: return; + case ValidateUserinfoRequest value when value.Context.IsRejected: return; + + case ValidateIntrospectionRequest value when value.Context.IsSkipped: return; + case ValidateRevocationRequest value when value.Context.IsSkipped: return; + case ValidateTokenRequest value when value.Context.IsSkipped: return; + + case HandleAuthorizationRequest value when value.Context.Result != null: return; + case HandleConfigurationRequest value when value.Context.Result != null: return; + case HandleCryptographyRequest value when value.Context.Result != null: return; + case HandleIntrospectionRequest value when value.Context.Result != null: return; + case HandleLogoutRequest value when value.Context.Result != null: return; + case HandleRevocationRequest value when value.Context.Result != null: return; + case HandleTokenRequest value when value.Context.Result != null: return; + case HandleUserinfoRequest value when value.Context.Result != null: return; + + case HandleAuthorizationRequest value when value.Context.Ticket != null: return; + + case HandleTokenRequest value when value.Context.Ticket != null && + !value.Context.Request.IsAuthorizationCodeGrantType() && + !value.Context.Request.IsRefreshTokenGrantType(): return; + + case HandleTokenRequest value when value.Context.Ticket == null && + (value.Context.Request.IsAuthorizationCodeGrantType() || + value.Context.Request.IsRefreshTokenGrantType()): return; + + case HandleAuthorizationRequest value when value.Context.Ticket != null: return; + + case ProcessChallengeResponse value when value.Context.Result != null: return; + case ProcessSigninResponse value when value.Context.Result != null: return; + case ProcessSignoutResponse value when value.Context.Result != null: return; + + case ProcessChallengeResponse value when value.Context.IsRejected: return; + case ProcessSigninResponse value when value.Context.IsRejected: return; + case ProcessSignoutResponse value when value.Context.IsRejected: return; + + case ApplyAuthorizationResponse value when value.Context.Result != null: return; + case ApplyConfigurationResponse value when value.Context.Result != null: return; + case ApplyCryptographyResponse value when value.Context.Result != null: return; + case ApplyIntrospectionResponse value when value.Context.Result != null: return; + case ApplyLogoutResponse value when value.Context.Result != null: return; + case ApplyRevocationResponse value when value.Context.Result != null: return; + case ApplyTokenResponse value when value.Context.Result != null: return; + case ApplyUserinfoResponse value when value.Context.Result != null: return; + + case DeserializeAuthorizationCode value when value.Context.IsHandled: return; + case DeserializeAccessToken value when value.Context.IsHandled: return; + case DeserializeIdentityToken value when value.Context.IsHandled: return; + case DeserializeRefreshToken value when value.Context.IsHandled: return; + + case DeserializeAuthorizationCode value when value.Context.Ticket != null: return; + case DeserializeAccessToken value when value.Context.Ticket != null: return; + case DeserializeIdentityToken value when value.Context.Ticket != null: return; + case DeserializeRefreshToken value when value.Context.Ticket != null: return; + + case SerializeAuthorizationCode value when value.Context.IsHandled: return; + case SerializeAccessToken value when value.Context.IsHandled: return; + case SerializeIdentityToken value when value.Context.IsHandled: return; + case SerializeRefreshToken value when value.Context.IsHandled: return; + + case SerializeAuthorizationCode value when !string.IsNullOrEmpty(value.Context.AuthorizationCode): return; + case SerializeAccessToken value when !string.IsNullOrEmpty(value.Context.AccessToken): return; + case SerializeIdentityToken value when !string.IsNullOrEmpty(value.Context.IdentityToken): return; + case SerializeRefreshToken value when !string.IsNullOrEmpty(value.Context.RefreshToken): return; + } + } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs new file mode 100644 index 00000000..d4de707b --- /dev/null +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -0,0 +1,565 @@ +using AspNet.Security.OpenIdConnect.Server; +using JetBrains.Annotations; + +namespace OpenIddict.Server +{ + /// + /// Contains common events used by the OpenIddict server handler. + /// + public static class OpenIddictServerEvents + { + /// + /// Represents an event called for each HTTP request to determine if + /// it should be handled by the OpenID Connect server middleware. + /// + public sealed class MatchEndpoint : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public MatchEndpoint([NotNull] MatchEndpointContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the authorization endpoint to give the user code + /// a chance to manually extract the authorization request from the ambient HTTP context. + /// + public sealed class ExtractAuthorizationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the configuration endpoint to give the user code + /// a chance to manually extract the configuration request from the ambient HTTP context. + /// + public sealed class ExtractConfigurationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ExtractConfigurationRequest([NotNull] ExtractConfigurationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the cryptography endpoint to give the user code + /// a chance to manually extract the cryptography request from the ambient HTTP context. + /// + + public sealed class ExtractCryptographyRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ExtractCryptographyRequest([NotNull] ExtractCryptographyRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the introspection endpoint to give the user code + /// a chance to manually extract the introspection request from the ambient HTTP context. + /// + public sealed class ExtractIntrospectionRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ExtractIntrospectionRequest([NotNull] ExtractIntrospectionRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the logout endpoint to give the user code + /// a chance to manually extract the logout request from the ambient HTTP context. + /// + public sealed class ExtractLogoutRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ExtractLogoutRequest([NotNull] ExtractLogoutRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the revocation endpoint to give the user code + /// a chance to manually extract the revocation request from the ambient HTTP context. + /// + public sealed class ExtractRevocationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ExtractRevocationRequest([NotNull] ExtractRevocationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the token endpoint to give the user code + /// a chance to manually extract the token request from the ambient HTTP context. + /// + public sealed class ExtractTokenRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ExtractTokenRequest([NotNull] ExtractTokenRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the userinfo endpoint to give the user code + /// a chance to manually extract the userinfo request from the ambient HTTP context. + /// + public sealed class ExtractUserinfoRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ExtractUserinfoRequest([NotNull] ExtractUserinfoRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the authorization endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public sealed class ValidateAuthorizationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the configuration endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public sealed class ValidateConfigurationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateConfigurationRequest([NotNull] ValidateConfigurationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the cryptography endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public sealed class ValidateCryptographyRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateCryptographyRequest([NotNull] ValidateCryptographyRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the introspection endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public sealed class ValidateIntrospectionRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the logout endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public sealed class ValidateLogoutRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the revocation endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public sealed class ValidateRevocationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the token endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public sealed class ValidateTokenRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each request to the userinfo endpoint + /// to determine if the request is valid and should continue to be processed. + /// + public sealed class ValidateUserinfoRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateUserinfoRequest([NotNull] ValidateUserinfoRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each validated authorization request + /// to allow the user code to decide how the request should be handled. + /// + public sealed class HandleAuthorizationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each validated configuration request + /// to allow the user code to decide how the request should be handled. + /// + public sealed class HandleConfigurationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each validated cryptography request + /// to allow the user code to decide how the request should be handled. + /// + public sealed class HandleCryptographyRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public HandleCryptographyRequest([NotNull] HandleCryptographyRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each validated introspection request + /// to allow the user code to decide how the request should be handled. + /// + public sealed class HandleIntrospectionRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each validated logout request + /// to allow the user code to decide how the request should be handled. + /// + public sealed class HandleLogoutRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public HandleLogoutRequest([NotNull] HandleLogoutRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each validated revocation request + /// to allow the user code to decide how the request should be handled. + /// + public sealed class HandleRevocationRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public HandleRevocationRequest([NotNull] HandleRevocationRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each validated token request + /// to allow the user code to decide how the request should be handled. + /// + public sealed class HandleTokenRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public HandleTokenRequest([NotNull] HandleTokenRequestContext context) : base(context) { } + } + + /// + /// Represents an event called for each validated userinfo request + /// to allow the user code to decide how the request should be handled. + /// + public sealed class HandleUserinfoRequest : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public HandleUserinfoRequest([NotNull] HandleUserinfoRequestContext context) : base(context) { } + } + + /// + /// Represents an event called when processing a challenge response. + /// + public sealed class ProcessChallengeResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ProcessChallengeResponse([NotNull] ProcessChallengeResponseContext context) : base(context) { } + } + + /// + /// Represents an event called when processing a sign-in response. + /// + public sealed class ProcessSigninResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ProcessSigninResponse([NotNull] ProcessSigninResponseContext context) : base(context) { } + } + + /// + /// Represents an event called when processing a sign-out response. + /// + public sealed class ProcessSignoutResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ProcessSignoutResponse([NotNull] ProcessSignoutResponseContext context) : base(context) { } + } + + /// + /// Represents an event called before the authorization response is returned to the caller. + /// + public sealed class ApplyAuthorizationResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context) : base(context) { } + } + + /// + /// Represents an event called before the configuration response is returned to the caller. + /// + public sealed class ApplyConfigurationResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyConfigurationResponse([NotNull] ApplyConfigurationResponseContext context) : base(context) { } + } + + /// + /// Represents an event called before the cryptography response is returned to the caller. + /// + public sealed class ApplyCryptographyResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyCryptographyResponse([NotNull] ApplyCryptographyResponseContext context) : base(context) { } + } + + /// + /// Represents an event called before the introspection response is returned to the caller. + /// + public sealed class ApplyIntrospectionResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyIntrospectionResponse([NotNull] ApplyIntrospectionResponseContext context) : base(context) { } + } + + /// + /// Represents an event called before the logout response is returned to the caller. + /// + public sealed class ApplyLogoutResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context) : base(context) { } + } + + /// + /// Represents an event called before the revocation response is returned to the caller. + /// + public sealed class ApplyRevocationResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyRevocationResponse([NotNull] ApplyRevocationResponseContext context) : base(context) { } + } + + /// + /// Represents an event called before the token response is returned to the caller. + /// + public sealed class ApplyTokenResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyTokenResponse([NotNull] ApplyTokenResponseContext context) : base(context) { } + } + + /// + /// Represents an event called before the userinfo response is returned to the caller. + /// + public sealed class ApplyUserinfoResponse : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyUserinfoResponse([NotNull] ApplyUserinfoResponseContext context) : base(context) { } + } + + /// + /// Represents an event called when serializing an authorization code. + /// + public sealed class SerializeAuthorizationCode : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) : base(context) { } + } + + /// + /// Represents an event called when serializing an access token. + /// + public sealed class SerializeAccessToken : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public SerializeAccessToken([NotNull] SerializeAccessTokenContext context) : base(context) { } + } + + /// + /// Represents an event called when serializing an identity token. + /// + public sealed class SerializeIdentityToken : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public SerializeIdentityToken([NotNull] SerializeIdentityTokenContext context) : base(context) { } + } + + /// + /// Represents an event called when serializing a refresh token. + /// + public sealed class SerializeRefreshToken : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) : base(context) { } + } + + /// + /// Represents an event called when deserializing an authorization code. + /// + public sealed class DeserializeAuthorizationCode : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public DeserializeAuthorizationCode([NotNull] DeserializeAuthorizationCodeContext context) : base(context) { } + } + + /// + /// Represents an event called when deserializing an access token. + /// + public sealed class DeserializeAccessToken : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public DeserializeAccessToken([NotNull] DeserializeAccessTokenContext context) : base(context) { } + } + + /// + /// Represents an event called when deserializing an identity token. + /// + public sealed class DeserializeIdentityToken : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public DeserializeIdentityToken([NotNull] DeserializeIdentityTokenContext context) : base(context) { } + } + + /// + /// Represents an event called when deserializing a refresh token. + /// + public sealed class DeserializeRefreshToken : OpenIddictServerEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public DeserializeRefreshToken([NotNull] DeserializeRefreshTokenContext context) : base(context) { } + } + } +} diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index 28265df7..9a13deb2 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -34,6 +34,7 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.AddAuthentication(); + builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); builder.Services.TryAddScoped(provider => { @@ -44,6 +45,7 @@ namespace Microsoft.Extensions.DependencyInjection return new OpenIddictServerProvider( provider.GetRequiredService>(), + provider.GetRequiredService(), provider.GetService() ?? throw CreateException(), provider.GetService() ?? throw CreateException(), provider.GetService() ?? throw CreateException(), diff --git a/src/OpenIddict.Server/OpenIddictServerOptions.cs b/src/OpenIddict.Server/OpenIddictServerOptions.cs index bfa7c6a6..e39eed5f 100644 --- a/src/OpenIddict.Server/OpenIddictServerOptions.cs +++ b/src/OpenIddict.Server/OpenIddictServerOptions.cs @@ -34,20 +34,6 @@ namespace OpenIddict.Server /// public bool AcceptAnonymousClients { get; set; } - /// - /// Gets or sets the user-provided that the OpenIddict server - /// invokes to enable developer control over the entire authentication/authorization process. - /// - public OpenIdConnectServerProvider ApplicationProvider { get; set; } - - /// - /// Gets or sets the user-provided provider type that the OpenIddict server handler instantiates - /// to enable developer control over the entire authentication/authorization process. When this - /// property is set, the provider is resolved from the services container. If the provider is not - /// guaranteed to be thread-safe, registering it as a scoped dependency is strongly recommended. - /// - public Type ApplicationProviderType { get; set; } - /// /// Gets or sets the distributed cache used by OpenIddict. If no cache is explicitly /// provided, the cache registered in the dependency injection container is used. diff --git a/src/OpenIddict.Validation/IOpenIddictValidationEvent.cs b/src/OpenIddict.Validation/IOpenIddictValidationEvent.cs new file mode 100644 index 00000000..4e8bc57a --- /dev/null +++ b/src/OpenIddict.Validation/IOpenIddictValidationEvent.cs @@ -0,0 +1,7 @@ +namespace OpenIddict.Validation +{ + /// + /// Represents an OpenIddict validation event. + /// + public interface IOpenIddictValidationEvent { } +} diff --git a/src/OpenIddict.Validation/IOpenIddictValidationEventHandler.cs b/src/OpenIddict.Validation/IOpenIddictValidationEventHandler.cs new file mode 100644 index 00000000..6bb304c5 --- /dev/null +++ b/src/OpenIddict.Validation/IOpenIddictValidationEventHandler.cs @@ -0,0 +1,25 @@ +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Validation +{ + /// + /// Represents a handler able to process events. + /// + /// The type of the events handled by this instance. + public interface IOpenIddictValidationEventHandler where TEvent : class, IOpenIddictValidationEvent + { + /// + /// Processes the event. + /// + /// The event to process. + /// + /// The that can be used to abort the operation. + /// + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task HandleAsync([NotNull] TEvent notification, CancellationToken cancellationToken); + } +} diff --git a/src/OpenIddict.Validation/IOpenIddictValidationEventService.cs b/src/OpenIddict.Validation/IOpenIddictValidationEventService.cs new file mode 100644 index 00000000..06ea4fd8 --- /dev/null +++ b/src/OpenIddict.Validation/IOpenIddictValidationEventService.cs @@ -0,0 +1,22 @@ +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Validation +{ + /// + /// Dispatches events by invoking the corresponding handlers. + /// + public interface IOpenIddictValidationEventService + { + /// + /// Publishes a new event. + /// + /// The type of the event to publish. + /// The event to publish. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + Task PublishAsync([NotNull] TEvent notification, CancellationToken cancellationToken = default) + where TEvent : class, IOpenIddictValidationEvent; + } +} diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs index a14f3cd4..ed0b0982 100644 --- a/src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationHandler.cs @@ -4,11 +4,8 @@ * the license and the contributors participating to this project. */ -using System; using System.ComponentModel; -using System.Text; using System.Text.Encodings.Web; -using System.Threading.Tasks; using AspNet.Security.OAuth.Validation; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; @@ -28,36 +25,5 @@ namespace OpenIddict.Validation : base(options, logger, encoder, clock) { } - - protected override async Task InitializeEventsAsync() - { - await base.InitializeEventsAsync(); - - // If an application provider instance or type was specified, import the application provider events. - if (Options.ApplicationEvents != null || Options.ApplicationEventsType != null) - { - // Resolve the user provider from the options or from the services container. - var events = Options.ApplicationEvents; - if (events == null) - { - events = Context.RequestServices.GetService(Options.ApplicationEventsType) as OAuthValidationEvents; - } - - if (events == null) - { - throw new InvalidOperationException(new StringBuilder() - .AppendLine("The application events cannot be resolved from the dependency injection container. ") - .Append("Make sure they are correctly registered in 'ConfigureServices(IServiceCollection services)'.") - .ToString()); - } - - // Update the main events to invoke the user provider's event handlers. - Events.Import(events); - } - } - - private new OpenIddictValidationEvents Events => (OpenIddictValidationEvents) base.Events; - - private new OpenIddictValidationOptions Options => (OpenIddictValidationOptions) base.Options; } } diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs index 1da101f6..ab3e5b2c 100644 --- a/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationInitializer.cs @@ -6,7 +6,6 @@ using System; using System.ComponentModel; -using AspNet.Security.OAuth.Validation; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; @@ -49,19 +48,6 @@ namespace OpenIddict.Validation throw new ArgumentException("The options instance name cannot be null or empty.", nameof(name)); } - if (options.ApplicationEventsType != null) - { - if (options.ApplicationEvents != null) - { - throw new InvalidOperationException("Application events cannot be registered when a type is specified."); - } - - if (!typeof(OAuthValidationEvents).IsAssignableFrom(options.ApplicationEventsType)) - { - throw new InvalidOperationException("Application events must inherit from OAuthValidationEvents."); - } - } - if (options.DataProtectionProvider == null) { options.DataProtectionProvider = _dataProtectionProvider; diff --git a/src/OpenIddict.Validation/Internal/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs similarity index 77% rename from src/OpenIddict.Validation/Internal/OpenIddictValidationEvents.cs rename to src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs index 7190ec13..ccfa8172 100644 --- a/src/OpenIddict.Validation/Internal/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/Internal/OpenIddictValidationProvider.cs @@ -19,8 +19,19 @@ namespace OpenIddict.Validation /// Provides the logic necessary to extract, validate and handle OAuth2 requests. /// [EditorBrowsable(EditorBrowsableState.Never)] - public class OpenIddictValidationEvents : OAuthValidationEvents + public class OpenIddictValidationProvider : OAuthValidationEvents { + private readonly IOpenIddictValidationEventService _eventService; + + public OpenIddictValidationProvider([NotNull] IOpenIddictValidationEventService eventService) + => _eventService = eventService; + + public override Task ApplyChallenge([NotNull] ApplyChallengeContext context) + => _eventService.PublishAsync(new OpenIddictValidationEvents.ApplyChallenge(context)); + + public override Task CreateTicket([NotNull] CreateTicketContext context) + => _eventService.PublishAsync(new OpenIddictValidationEvents.CreateTicket(context)); + public override async Task DecryptToken([NotNull] DecryptTokenContext context) { var options = (OpenIddictValidationOptions) context.Options; @@ -80,16 +91,13 @@ namespace OpenIddict.Validation context.Success(); } - await base.DecryptToken(context); + await _eventService.PublishAsync(new OpenIddictValidationEvents.DecryptToken(context)); } - public void Import([NotNull] OAuthValidationEvents events) - { - OnApplyChallenge = events.ApplyChallenge; - OnCreateTicket = events.CreateTicket; - OnDecryptToken = events.DecryptToken; - OnRetrieveToken = events.RetrieveToken; - OnValidateToken = events.ValidateToken; - } + public override Task RetrieveToken([NotNull] RetrieveTokenContext context) + => _eventService.PublishAsync(new OpenIddictValidationEvents.RetrieveToken(context)); + + public override Task ValidateToken([NotNull] ValidateTokenContext context) + => _eventService.PublishAsync(new OpenIddictValidationEvents.ValidateToken(context)); } } diff --git a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs index c29a7f7c..4f1841a0 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationBuilder.cs @@ -7,10 +7,11 @@ using System; using System.ComponentModel; using System.Linq; -using AspNet.Security.OAuth.Validation; +using System.Threading; +using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.DataProtection; -using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenIddict.Abstractions; using OpenIddict.Validation; namespace Microsoft.Extensions.DependencyInjection @@ -41,93 +42,134 @@ namespace Microsoft.Extensions.DependencyInjection public IServiceCollection Services { get; } /// - /// Amends the default OpenIddict validation configuration. + /// Registers an event handler for the specified event type. /// - /// The delegate used to configure the OpenIddict options. - /// This extension can be safely called multiple times. + /// The handler added to the DI container. /// The . - public OpenIddictValidationBuilder Configure([NotNull] Action configuration) + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictValidationBuilder AddEventHandler( + [NotNull] IOpenIddictValidationEventHandler handler) + where TEvent : class, IOpenIddictValidationEvent { - if (configuration == null) + if (handler == null) { - throw new ArgumentNullException(nameof(configuration)); + throw new ArgumentNullException(nameof(handler)); } - Services.Configure(OpenIddictValidationDefaults.AuthenticationScheme, configuration); + Services.AddSingleton(handler); return this; } /// - /// Registers the specified values as valid audiences. Setting the audiences is recommended - /// when the authorization server issues access tokens for multiple distinct resource servers. + /// Registers an event handler for the specified event type. /// - /// The audiences valid for this resource server. + /// The handler added to the DI container. /// The . - public OpenIddictValidationBuilder AddAudiences([NotNull] params string[] audiences) - { - if (audiences == null) - { - throw new ArgumentNullException(nameof(audiences)); - } - - if (audiences.Any(audience => string.IsNullOrEmpty(audience))) - { - throw new ArgumentException("Audiences cannot be null or empty.", nameof(audiences)); - } - - return Configure(options => options.Audiences.UnionWith(audiences)); - } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictValidationBuilder AddEventHandler([NotNull] Func handler) + where TEvent : class, IOpenIddictValidationEvent + => AddEventHandler((notification, cancellationToken) => handler(notification)); /// - /// Registers application-specific OAuth2 validation events that are automatically - /// invoked for each request handled by the OpenIddict validation handler. + /// Registers an event handler for the specified event type. /// - /// The custom service. - /// The . + /// The handler added to the DI container. + /// The . [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictValidationBuilder RegisterEvents([NotNull] OAuthValidationEvents events) + public OpenIddictValidationBuilder AddEventHandler([NotNull] Func handler) + where TEvent : class, IOpenIddictValidationEvent { - if (events == null) + if (handler == null) { - throw new ArgumentNullException(nameof(events)); + throw new ArgumentNullException(nameof(handler)); } - return Configure(options => options.ApplicationEvents = events); + return AddEventHandler(new OpenIddictValidationEventHandler(handler)); } /// - /// Registers application-specific OAuth2 validation events that are automatically - /// invoked for each request handled by the OpenIddict validation handler. + /// Registers an event handler for the specified event type. /// - /// The type of the custom service. + /// The type of the event. + /// The type of the handler. + /// The lifetime of the registered service. /// The . [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictValidationBuilder RegisterEvents() where TEvents : OAuthValidationEvents - => RegisterEvents(typeof(TEvents)); + public OpenIddictValidationBuilder AddEventHandler( + ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TEvent : class, IOpenIddictValidationEvent + where THandler : IOpenIddictValidationEventHandler + => AddEventHandler(typeof(THandler)); /// - /// Registers application-specific OAuth2 validation events that are automatically - /// invoked for each request handled by the OpenIddict validation handler. + /// Registers an event handler for the specified event type. /// - /// The type of the custom service. + /// The type of the handler. + /// The lifetime of the registered service. /// The . [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictValidationBuilder RegisterEvents([NotNull] Type type) + public OpenIddictValidationBuilder AddEventHandler( + [NotNull] Type type, ServiceLifetime lifetime = ServiceLifetime.Scoped) + where TEvent : class, IOpenIddictValidationEvent { if (type == null) { throw new ArgumentNullException(nameof(type)); } - if (!typeof(OAuthValidationEvents).IsAssignableFrom(type)) + if (lifetime == ServiceLifetime.Transient) + { + throw new ArgumentException("Handlers cannot be registered as transient services.", nameof(lifetime)); + } + + if (!typeof(IOpenIddictValidationEventHandler).IsAssignableFrom(type)) { throw new ArgumentException("The specified type is invalid.", nameof(type)); } - Services.TryAddScoped(type); + Services.Add(new ServiceDescriptor(typeof(IOpenIddictValidationEventHandler), type, lifetime)); + + return this; + } + + /// + /// Amends the default OpenIddict validation configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictValidationBuilder Configure([NotNull] Action configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(OpenIddictValidationDefaults.AuthenticationScheme, configuration); - return Configure(options => options.ApplicationEventsType = type); + return this; + } + + /// + /// Registers the specified values as valid audiences. Setting the audiences is recommended + /// when the authorization server issues access tokens for multiple distinct resource servers. + /// + /// The audiences valid for this resource server. + /// The . + public OpenIddictValidationBuilder AddAudiences([NotNull] params string[] audiences) + { + if (audiences == null) + { + throw new ArgumentNullException(nameof(audiences)); + } + + if (audiences.Any(audience => string.IsNullOrEmpty(audience))) + { + throw new ArgumentException("Audiences cannot be null or empty.", nameof(audiences)); + } + + return Configure(options => options.Audiences.UnionWith(audiences)); } /// @@ -176,4 +218,4 @@ namespace Microsoft.Extensions.DependencyInjection public OpenIddictValidationBuilder UseReferenceTokens() => Configure(options => options.UseReferenceTokens = true); } -} \ No newline at end of file +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvent.cs b/src/OpenIddict.Validation/OpenIddictValidationEvent.cs new file mode 100644 index 00000000..ec75eeb6 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationEvent.cs @@ -0,0 +1,24 @@ +using System; +using JetBrains.Annotations; + +namespace OpenIddict.Validation +{ + /// + /// Represents an OpenIddict validation event. + /// + /// The type of the context instance associated with the event. + public class OpenIddictValidationEvent : IOpenIddictValidationEvent where TContext : class + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the event. + public OpenIddictValidationEvent([NotNull] TContext context) + => Context = context ?? throw new ArgumentNullException(nameof(context)); + + /// + /// Gets the context instance associated with the event. + /// + public TContext Context { get; } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEventHandler.cs b/src/OpenIddict.Validation/OpenIddictValidationEventHandler.cs new file mode 100644 index 00000000..c40704ca --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationEventHandler.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; + +namespace OpenIddict.Validation +{ + /// + /// Represents a handler able to process events. + /// + /// The type of the events handled by this instance. + public class OpenIddictValidationEventHandler : IOpenIddictValidationEventHandler + where TEvent : class, IOpenIddictValidationEvent + { + private readonly Func _handler; + + /// + /// Creates a new event using the specified handler delegate. + /// + /// The event handler delegate + public OpenIddictValidationEventHandler([NotNull] Func handler) + => _handler = handler ?? throw new ArgumentNullException(nameof(handler)); + + /// + /// Processes the event. + /// + /// The event to process. + /// + /// The that can be used to abort the operation. + /// + /// + /// A that can be used to monitor the asynchronous operation. + /// + public Task HandleAsync(TEvent notification, CancellationToken cancellationToken) + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + return _handler.Invoke(notification, cancellationToken); + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEventService.cs b/src/OpenIddict.Validation/OpenIddictValidationEventService.cs new file mode 100644 index 00000000..451b8835 --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationEventService.cs @@ -0,0 +1,63 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +namespace OpenIddict.Validation +{ + /// + /// Dispatches notifications by invoking the corresponding handlers. + /// + public class OpenIddictValidationEventService : IOpenIddictValidationEventService + { + private readonly IServiceProvider _provider; + + public OpenIddictValidationEventService([NotNull] IServiceProvider provider) + { + _provider = provider; + } + + /// + /// Publishes a new event. + /// + /// The type of the event to publish. + /// The event to publish. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public async Task PublishAsync([NotNull] TEvent notification, CancellationToken cancellationToken = default) + where TEvent : class, IOpenIddictValidationEvent + { + if (notification == null) + { + throw new ArgumentNullException(nameof(notification)); + } + + foreach (var handler in _provider.GetServices>()) + { + cancellationToken.ThrowIfCancellationRequested(); + + await handler.HandleAsync(notification, cancellationToken); + + // Note: the following logic determines whether next handlers should be invoked + // depending on whether the underlying event context was substantially updated. + switch (notification) + { + case OpenIddictValidationEvents.ApplyChallenge value when value.Context.Handled: return; + + case OpenIddictValidationEvents.CreateTicket value when value.Context.Result != null: return; + case OpenIddictValidationEvents.CreateTicket value when value.Context.Principal == null: return; + + case OpenIddictValidationEvents.DecryptToken value when value.Context.Result != null: return; + case OpenIddictValidationEvents.DecryptToken value when value.Context.Principal != null: return; + + case OpenIddictValidationEvents.RetrieveToken value when value.Context.Result != null: return; + case OpenIddictValidationEvents.RetrieveToken value when value.Context.Principal != null: return; + + case OpenIddictValidationEvents.ValidateToken value when value.Context.Result != null: return; + case OpenIddictValidationEvents.ValidateToken value when value.Context.Principal == null: return; + } + } + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs new file mode 100644 index 00000000..333bb7bd --- /dev/null +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -0,0 +1,71 @@ +using AspNet.Security.OAuth.Validation; +using JetBrains.Annotations; + +namespace OpenIddict.Validation +{ + /// + /// Contains common events used by the OpenIddict validation handler. + /// + public static class OpenIddictValidationEvents + { + /// + /// Invoked when a challenge response is returned to the caller. + /// + public sealed class ApplyChallenge : OpenIddictValidationEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ApplyChallenge([NotNull] ApplyChallengeContext context) : base(context) { } + } + + /// + /// Invoked when a ticket is to be created from an introspection response. + /// + public sealed class CreateTicket : OpenIddictValidationEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public CreateTicket([NotNull] CreateTicketContext context) : base(context) { } + } + + /// + /// Invoked when a token is to be decrypted. + /// + public sealed class DecryptToken : OpenIddictValidationEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public DecryptToken([NotNull] DecryptTokenContext context) : base(context) { } + } + + /// + /// Invoked when a token is to be parsed from a newly-received request. + /// + public sealed class RetrieveToken : OpenIddictValidationEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public RetrieveToken([NotNull] RetrieveTokenContext context) : base(context) { } + } + + /// + /// Invoked when a token is to be validated, before final processing. + /// + public sealed class ValidateToken : OpenIddictValidationEvent + { + /// + /// Creates a new instance of . + /// + /// The context instance associated with the notification. + public ValidateToken([NotNull] ValidateTokenContext context) : base(context) { } + } + } +} diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index c0b8ee95..438f6ad6 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -34,8 +34,9 @@ namespace Microsoft.Extensions.DependencyInjection builder.Services.AddAuthentication(); - builder.Services.TryAddScoped(); + builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); + builder.Services.TryAddScoped(); // Note: TryAddEnumerable() is used here to ensure the initializer is only registered once. builder.Services.TryAddEnumerable(new[] diff --git a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs index 43bccdf7..869ac26c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationOptions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationOptions.cs @@ -4,7 +4,6 @@ * the license and the contributors participating to this project. */ -using System; using AspNet.Security.OAuth.Validation; namespace OpenIddict.Validation @@ -20,23 +19,9 @@ namespace OpenIddict.Validation public OpenIddictValidationOptions() { Events = null; - EventsType = typeof(OpenIddictValidationEvents); + EventsType = typeof(OpenIddictValidationProvider); } - /// - /// Gets or sets the user-provided that the OpenIddict - /// validation handler invokes to enable developer control over the entire authentication process. - /// - public OAuthValidationEvents ApplicationEvents { get; set; } - - /// - /// Gets or sets the user-provided provider type that the OpenIddict validation handler - /// instantiates to enable developer control over the entire authentication process. When this - /// property is set, the provider is resolved from the services container. If the provider is not - /// guaranteed to be thread-safe, registering it as a scoped dependency is strongly recommended. - /// - public Type ApplicationEventsType { get; set; } - /// /// Gets or sets a boolean indicating whether reference tokens are used. /// diff --git a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs index 3ed140aa..72eacfbd 100644 --- a/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs +++ b/test/OpenIddict.Server.Tests/Internal/OpenIddictServerInitializerTests.cs @@ -9,7 +9,6 @@ using System.Text; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Client; using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; @@ -44,52 +43,6 @@ namespace OpenIddict.Server.Tests Assert.Equal("A random number generator must be registered.", exception.Message); } - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenApplicationProviderTypeAndInstanceAreProvided() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => - { - options.ApplicationProvider = new OpenIdConnectServerProvider(); - options.ApplicationProviderType = typeof(OpenIdConnectServerProvider); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal("An application provider cannot be registered when a type is specified.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionForInvalidApplicationProviderType() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.ApplicationProviderType = typeof(object)); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal("Application providers must inherit from OpenIdConnectServerProvider.", exception.Message); - } - [Fact] public async Task PostConfigure_ThrowsAnExceptionWhenNoFlowIsEnabled() { diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs index e93abfd8..752c4913 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerBuilderTests.cs @@ -7,8 +7,9 @@ using System; using System.IdentityModel.Tokens.Jwt; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Primitives; -using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -16,11 +17,63 @@ using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Moq; using Xunit; +using static OpenIddict.Server.OpenIddictServerEvents; namespace OpenIddict.Server.Tests { public class OpenIddictServerBuilderTests { + [Fact] + public void AddEventHandler_HandlerIsAttached() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + var handler = new OpenIddictServerEventHandler( + (notification, cancellationToken) => Task.CompletedTask); + + // Act + builder.AddEventHandler(handler); + + // Assert + Assert.Contains(services, service => + service.ServiceType == typeof(IOpenIddictServerEventHandler) && + service.ImplementationInstance == handler); + } + + [Fact] + public void AddEventHandler_ThrowsAnExceptionForInvalidHandlerType() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act and assert + var exception = Assert.Throws(delegate + { + return builder.AddEventHandler(typeof(object)); + }); + + Assert.Equal("type", exception.ParamName); + Assert.StartsWith("The specified type is invalid.", exception.Message); + } + + [Fact] + public void AddEventHandler_HandlerIsRegistered() + { + // Arrange + var services = CreateServices(); + var builder = CreateBuilder(services); + + // Act + builder.AddEventHandler(); + + // Assert + Assert.Contains(services, service => + service.ServiceType == typeof(IOpenIddictServerEventHandler) && + service.ImplementationType == typeof(CustomHandler)); + } + [Fact] public void Configure_OptionsAreCorrectlyAmended() { @@ -600,69 +653,6 @@ namespace OpenIddict.Server.Tests Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Issuer); } - [Fact] - public void RegisterProvider_ProviderIsAttached() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.RegisterProvider(new OpenIdConnectServerProvider()); - - var options = GetOptions(services); - - // Assert - Assert.NotNull(options.ApplicationProvider); - } - - [Fact] - public void RegisterProvider_ThrowsAnExceptionForInvalidProviderType() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - return builder.RegisterProvider(typeof(object)); - }); - - Assert.Equal("type", exception.ParamName); - Assert.StartsWith("The specified type is invalid.", exception.Message); - } - - [Fact] - public void RegisterProvider_ProviderTypeIsAttached() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.RegisterProvider(typeof(OpenIdConnectServerProvider)); - - var options = GetOptions(services); - - // Assert - Assert.Equal(typeof(OpenIdConnectServerProvider), options.ApplicationProviderType); - } - - [Fact] - public void RegisterProvider_ProviderIsRegistered() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act - builder.RegisterProvider(typeof(OpenIdConnectServerProvider)); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(OpenIdConnectServerProvider)); - } - [Fact] public void RegisterClaims_ClaimsAreAdded() { @@ -757,5 +747,12 @@ namespace OpenIddict.Server.Tests var options = provider.GetRequiredService>(); return options.Get(OpenIddictServerDefaults.AuthenticationScheme); } + + public class CustomHandler : OpenIddictServerEventHandler + { + public CustomHandler(Func handler) : base(handler) + { + } + } } } diff --git a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs deleted file mode 100644 index 44b815d1..00000000 --- a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationInitializerTests.cs +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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.Threading.Tasks; -using AspNet.Security.OAuth.Validation; -using AspNet.Security.OpenIdConnect.Client; -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Xunit; - -namespace OpenIddict.Validation.Tests -{ - public class OpenIddictValidationInitializerTests - { - [Fact] - public async Task PostConfigure_ThrowsAnExceptionWhenApplicationEventsTypeAndInstanceAreProvided() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => - { - options.ApplicationEvents = new OAuthValidationEvents(); - options.ApplicationEventsType = typeof(OAuthValidationEvents); - }); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal("Application events cannot be registered when a type is specified.", exception.Message); - } - - [Fact] - public async Task PostConfigure_ThrowsAnExceptionForInvalidApplicationEventsType() - { - // Arrange - var server = CreateAuthorizationServer(builder => - { - builder.Configure(options => options.ApplicationEventsType = typeof(object)); - }); - - var client = new OpenIdConnectClient(server.CreateClient()); - - // Act and assert - var exception = await Assert.ThrowsAsync(delegate - { - return client.GetAsync("/"); - }); - - // Assert - Assert.Equal("Application events must inherit from OAuthValidationEvents.", exception.Message); - } - - private static TestServer CreateAuthorizationServer(Action configuration = null) - { - var builder = new WebHostBuilder(); - - builder.UseEnvironment("Testing"); - - builder.ConfigureLogging(options => options.AddDebug()); - - builder.ConfigureServices(services => - { - services.AddAuthentication(); - services.AddOptions(); - services.AddDistributedMemoryCache(); - - services.AddOpenIddict() - .AddCore(options => - { - options.SetDefaultApplicationEntity() - .SetDefaultAuthorizationEntity() - .SetDefaultScopeEntity() - .SetDefaultTokenEntity(); - }) - - .AddValidation(options => configuration?.Invoke(options)); - }); - - builder.Configure(app => - { - app.UseAuthentication(); - - app.Run(context => context.ChallengeAsync(OpenIddictValidationDefaults.AuthenticationScheme)); - }); - - return new TestServer(builder); - } - - public class OpenIddictApplication { } - public class OpenIddictAuthorization { } - public class OpenIddictScope { } - public class OpenIddictToken { } - } -} diff --git a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationEventsTests.cs b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs similarity index 99% rename from test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationEventsTests.cs rename to test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs index 41d83e7e..77fc072d 100644 --- a/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationEventsTests.cs +++ b/test/OpenIddict.Validation.Tests/Internal/OpenIddictValidationProviderTests.cs @@ -34,7 +34,7 @@ using Xunit; namespace OpenIddict.Validation.Tests { - public class OpenIddictValidationEventsTests + public class OpenIddictValidationProviderTests { [Fact] public async Task DecryptToken_ThrowsAnExceptionWhenTokenManagerIsNotRegistered() diff --git a/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs b/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs index d1750617..27648534 100644 --- a/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs +++ b/test/OpenIddict.Validation.Tests/OpenIddictValidationBuilderTests.cs @@ -5,109 +5,99 @@ */ using System; -using AspNet.Security.OAuth.Validation; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Xunit; +using static OpenIddict.Validation.OpenIddictValidationEvents; namespace OpenIddict.Validation.Tests { public class OpenIddictValidationBuilderTests { [Fact] - public void Configure_OptionsAreCorrectlyAmended() + public void AddEventHandler_HandlerIsAttached() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); + var handler = new OpenIddictValidationEventHandler( + (notification, cancellationToken) => Task.CompletedTask); // Act - builder.Configure(configuration => configuration.ClaimsIssuer = "custom_issuer"); - - var options = GetOptions(services); + builder.AddEventHandler(handler); // Assert - Assert.Equal("custom_issuer", options.ClaimsIssuer); + Assert.Contains(services, service => + service.ServiceType == typeof(IOpenIddictValidationEventHandler) && + service.ImplementationInstance == handler); } [Fact] - public void AddAudiences_AudiencesAreAdded() + public void AddEventHandler_ThrowsAnExceptionForInvalidHandlerType() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); - // Act - builder.AddAudiences("Fabrikam", "Contoso"); - - var options = GetOptions(services); + // Act and assert + var exception = Assert.Throws(delegate + { + return builder.AddEventHandler(typeof(object)); + }); - // Assert - Assert.Equal(new[] { "Fabrikam", "Contoso" }, options.Audiences); + Assert.Equal("type", exception.ParamName); + Assert.StartsWith("The specified type is invalid.", exception.Message); } [Fact] - public void RegisterEvents_EventsAreAttached() + public void AddEventHandler_HandlerIsRegistered() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); // Act - builder.RegisterEvents(new OAuthValidationEvents()); - - var options = GetOptions(services); + builder.AddEventHandler(); // Assert - Assert.NotNull(options.ApplicationEvents); + Assert.Contains(services, service => + service.ServiceType == typeof(IOpenIddictValidationEventHandler) && + service.ImplementationType == typeof(CustomHandler)); } [Fact] - public void RegisterEvents_ThrowsAnExceptionForInvalidEventsType() - { - // Arrange - var services = CreateServices(); - var builder = CreateBuilder(services); - - // Act and assert - var exception = Assert.Throws(delegate - { - return builder.RegisterEvents(typeof(object)); - }); - - Assert.Equal("type", exception.ParamName); - Assert.StartsWith("The specified type is invalid.", exception.Message); - } - - [Fact] - public void RegisterEvents_EventsTypeIsAttached() + public void Configure_OptionsAreCorrectlyAmended() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); // Act - builder.RegisterEvents(typeof(OAuthValidationEvents)); + builder.Configure(configuration => configuration.ClaimsIssuer = "custom_issuer"); var options = GetOptions(services); // Assert - Assert.Equal(typeof(OAuthValidationEvents), options.ApplicationEventsType); + Assert.Equal("custom_issuer", options.ClaimsIssuer); } [Fact] - public void RegisterEvents_EventsAreRegistered() + public void AddAudiences_AudiencesAreAdded() { // Arrange var services = CreateServices(); var builder = CreateBuilder(services); // Act - builder.RegisterEvents(typeof(OAuthValidationEvents)); + builder.AddAudiences("Fabrikam", "Contoso"); + + var options = GetOptions(services); // Assert - Assert.Contains(services, service => service.ServiceType == typeof(OAuthValidationEvents)); + Assert.Equal(new[] { "Fabrikam", "Contoso" }, options.Audiences); } [Fact] @@ -186,5 +176,12 @@ namespace OpenIddict.Validation.Tests var options = provider.GetRequiredService>(); return options.Get(OpenIddictValidationDefaults.AuthenticationScheme); } + + public class CustomHandler : OpenIddictValidationEventHandler + { + public CustomHandler(Func handler) : base(handler) + { + } + } } }