From 5fc15473ce07346fa1255a83db71376cba552dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 22 Aug 2026 10:34:55 +0200 Subject: [PATCH] Attach IServiceProvider to OpenIddict*Transaction and make OpenIddict*Dispatcher a singleton service --- .../Controllers/AuthorizationController.cs | 2 + .../OpenIddictResources.resx | 12 - .../Primitives/OpenIddictParameter.cs | 33 +-- .../OpenIddictClientAspNetCoreFeature.cs | 11 +- .../OpenIddictClientAspNetCoreHandler.cs | 19 +- .../OpenIddictClientAspNetCoreHelpers.cs | 13 +- .../OpenIddictClientOwinExtensions.cs | 1 - .../OpenIddictClientOwinHandler.cs | 55 ++-- .../OpenIddictClientOwinHelpers.cs | 13 +- .../OpenIddictClientOwinMiddleware.cs | 10 +- ...penIddictClientSystemIntegrationService.cs | 12 +- .../IOpenIddictClientFactory.cs | 30 --- .../OpenIddictClientDispatcher.cs | 89 ++---- .../OpenIddictClientEvents.cs | 10 +- .../OpenIddictClientExtensions.cs | 3 +- .../OpenIddictClientFactory.cs | 48 ---- .../OpenIddictClientService.cs | 254 +++++++++++------- .../OpenIddictClientTransaction.cs | 49 ++-- .../OpenIddictServerAspNetCoreFeature.cs | 11 +- .../OpenIddictServerAspNetCoreHandler.cs | 19 +- .../OpenIddictServerAspNetCoreHelpers.cs | 16 +- .../OpenIddictServerOwinExtensions.cs | 1 - .../OpenIddictServerOwinHandler.cs | 54 ++-- .../OpenIddictServerOwinHelpers.cs | 13 +- .../OpenIddictServerOwinMiddleware.cs | 8 +- .../IOpenIddictServerFactory.cs | 30 --- .../OpenIddictServerDispatcher.cs | 89 ++---- .../OpenIddictServerEvents.cs | 10 +- .../OpenIddictServerExtensions.cs | 3 +- .../OpenIddictServerFactory.cs | 48 ---- .../OpenIddictServerTransaction.cs | 33 ++- .../OpenIddictValidationAspNetCoreFeature.cs | 11 +- .../OpenIddictValidationAspNetCoreHandler.cs | 19 +- .../OpenIddictValidationAspNetCoreHelpers.cs | 17 +- .../OpenIddictValidationOwinExtensions.cs | 1 - .../OpenIddictValidationOwinHandler.cs | 46 ++-- .../OpenIddictValidationOwinHelpers.cs | 13 +- .../OpenIddictValidationOwinMiddleware.cs | 12 +- .../IOpenIddictValidationFactory.cs | 30 --- .../OpenIddictValidationDispatcher.cs | 89 ++---- .../OpenIddictValidationEvents.cs | 10 +- .../OpenIddictValidationExtensions.cs | 3 +- .../OpenIddictValidationFactory.cs | 48 ---- .../OpenIddictValidationService.cs | 53 ++-- .../OpenIddictValidationTransaction.cs | 41 +-- .../OpenIddictServerExtensionsTests.cs | 18 +- 46 files changed, 563 insertions(+), 847 deletions(-) delete mode 100644 src/OpenIddict.Client/IOpenIddictClientFactory.cs delete mode 100644 src/OpenIddict.Client/OpenIddictClientFactory.cs delete mode 100644 src/OpenIddict.Server/IOpenIddictServerFactory.cs delete mode 100644 src/OpenIddict.Server/OpenIddictServerFactory.cs delete mode 100644 src/OpenIddict.Validation/IOpenIddictValidationFactory.cs delete mode 100644 src/OpenIddict.Validation/OpenIddictValidationFactory.cs diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs index f66b2a49..c65ff544 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs @@ -212,6 +212,7 @@ public class AuthorizationController : Controller var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() { ApplicationId = await _applicationManager.GetIdAsync(application), + LoginId = result.Identity.GetClaim("login_id"), Subject = user.Id }); @@ -361,6 +362,7 @@ public class AuthorizationController : Controller var session = sessions.LastOrDefault() ?? await _sessionManager.CreateAsync(new() { ApplicationId = await _applicationManager.GetIdAsync(application), + LoginId = result.Identity.GetClaim("login_id"), Subject = user.Id }); diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index b0cc79ed..02703dba 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -501,10 +501,6 @@ This may indicate that the event handler responsible for processing OpenID Conne No service provider was found in the OWIN context. For the OpenIddict server services to work correctly, a per-request 'IServiceProvider' must be attached to the OWIN environment with the dictionary key 'System.IServiceProvider'. Note: when using a dependency injection container supporting middleware resolution (like Autofac), the 'app.UseOpenIddictServer()' extension MUST NOT be called. - - - The authentication handler used by the OpenIddict server components cannot be resolved from the DI container. -To register the OWIN integration, use 'services.AddOpenIddict().AddServer().UseOwin()'. Audiences cannot be null or empty. @@ -693,10 +689,6 @@ Make sure that neither DefaultSignInScheme nor DefaultSignOutScheme point to an No service provider was found in the OWIN context. For the OpenIddict validation services to work correctly, a per-request 'IServiceProvider' must be attached to the OWIN environment with the dictionary key 'System.IServiceProvider'. Note: when using a dependency injection container supporting middleware resolution (like Autofac), the 'app.UseOpenIddictValidation()' extension MUST NOT be called. - - - The authentication handler used by the OpenIddict validation components cannot be resolved from the DI container. -To register the OWIN integration, use 'services.AddOpenIddict().AddValidation().UseOwin()'. The local server integration can only be used with direct validation. @@ -1044,10 +1036,6 @@ Reference the 'OpenIddict.Client.SystemNetHttp' package and call 'services.AddOp No service provider was found in the OWIN context. For the OpenIddict client services to work correctly, a per-request 'IServiceProvider' must be attached to the OWIN environment with the dictionary key 'System.IServiceProvider'. Note: when using a dependency injection container supporting middleware resolution (like Autofac), the 'app.UseOpenIddictClient()' extension MUST NOT be called. - - - The authentication handler used by the OpenIddict client components cannot be resolved from the DI container. -To register the OWIN integration, use 'services.AddOpenIddict().AddClient().UseOwin()'. The core services must be registered when enabling the OpenIddict client feature. diff --git a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs index 0d0ae2bc..d38513c2 100644 --- a/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs +++ b/src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs @@ -175,15 +175,10 @@ public readonly struct OpenIddictParameter : IEquatable null => null }; - /// - /// Determines whether the current - /// instance is equal to the specified . - /// - /// The other object to which to compare this instance. - /// - /// if the two instances have both the same representation - /// (e.g ) and value, otherwise. - /// + /// + /// + /// Two instances are considered equal if they have the same representation. + /// public bool Equals(OpenIddictParameter other) { return (_value, other._value) switch @@ -282,22 +277,14 @@ public readonly struct OpenIddictParameter : IEquatable }; } - /// - /// Determines whether the current - /// instance is equal to the specified . - /// - /// The other object to which to compare this instance. - /// - /// if the two instances have both the same representation - /// (e.g ) and value, otherwise. - /// + /// + /// + /// Two instances are considered equal if they have the same representation. + /// public override bool Equals([NotNullWhen(true)] object? obj) => obj is OpenIddictParameter parameter && Equals(parameter); - /// - /// Returns the hash code of the current instance. - /// - /// The hash code for the current instance. + /// public override int GetHashCode() { return _value switch @@ -1010,7 +997,7 @@ public readonly struct OpenIddictParameter : IEquatable long value => value.ToString(CultureInfo.InvariantCulture), // When the parameter is a JSON boolean value, use its string representation. - JsonElement { ValueKind: JsonValueKind.True } => "true", + JsonElement { ValueKind: JsonValueKind.True } => "true", JsonElement { ValueKind: JsonValueKind.False } => "false", // When the parameter is a JsonElement, try to convert it if it's of a supported type. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs index e9667112..ed8da712 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs @@ -9,14 +9,17 @@ using System.ComponentModel; namespace OpenIddict.Client.AspNetCore; /// -/// Exposes the current client transaction to the ASP.NET Core host. +/// Exposes the current client transaction to the ASP.NET Core application. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientAspNetCoreFeature { /// - /// Gets or sets the client transaction that encapsulates all specific - /// information about an individual OpenID Connect client request. + /// Gets the transaction that encapsulates all specific information about an individual operation. /// - public OpenIddictClientTransaction? Transaction { get; set; } + public required OpenIddictClientTransaction Transaction + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } } diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs index 87645a20..0c127b92 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs @@ -8,6 +8,7 @@ using System.ComponentModel; using System.Globalization; using System.Security.Claims; using System.Text.Encodings.Web; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants; @@ -24,22 +25,17 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler /// Creates a new instance of the class. /// public OpenIddictClientAspNetCoreHandler( IOpenIddictClientDispatcher dispatcher, - IOpenIddictClientFactory factory, IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) - { - _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - _factory = factory ?? throw new ArgumentNullException(nameof(factory)); - } + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// public async Task HandleRequestAsync() @@ -59,9 +55,16 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler()?.Transaction; if (transaction is null) { + var options = Context.RequestServices.GetRequiredService>(); + // Create a new transaction and attach the HTTP request to make it available to the ASP.NET Core handlers. - transaction = await _factory.CreateTransactionAsync(source.Token); - transaction.Properties[typeof(HttpRequest).FullName!] = new WeakReference(Request); + transaction = new OpenIddictClientTransaction + { + CancellationToken = source.Token, + Options = options.CurrentValue, + Properties = { [typeof(HttpRequest).FullName!] = Request }, + ServiceProvider = Context.RequestServices + }; // Attach the OpenIddict client transaction to the ASP.NET Core features // so that it can retrieved while performing sign-in/sign-out operations. diff --git a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs index 1806322d..54566964 100644 --- a/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs +++ b/src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs @@ -23,17 +23,8 @@ public static class OpenIddictClientAspNetCoreHelpers { ArgumentNullException.ThrowIfNull(transaction); - if (!transaction.Properties.TryGetValue(typeof(HttpRequest).FullName!, out object? property)) - { - return null; - } - - if (property is WeakReference reference && reference.TryGetTarget(out HttpRequest? request)) - { - return request; - } - - return null; + return transaction.Properties.TryGetValue(typeof(HttpRequest).FullName!, out object? property) + && property is HttpRequest request ? request : null; } /// diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs index 4229683a..c05e2f88 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs @@ -31,7 +31,6 @@ public static class OpenIddictClientOwinExtensions // Note: unlike regular OWIN middleware, the OpenIddict client middleware is registered // as a scoped service in the DI container. This allows containers that support middleware // resolution (like Autofac) to use it without requiring additional configuration. - builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); // Register the built-in event handlers used by the OpenIddict OWIN client components. diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs index 774da6c3..0beca3c2 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs @@ -10,6 +10,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Microsoft.Owin.Security.Infrastructure; using static OpenIddict.Client.Owin.OpenIddictClientOwinConstants; @@ -23,29 +24,20 @@ namespace OpenIddict.Client.Owin; [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientOwinHandler : AuthenticationHandler { - private readonly IOpenIddictClientDispatcher _dispatcher; - private readonly IOpenIddictClientFactory _factory; - private readonly IOptionsMonitor _options; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// - /// The OpenIddict client dispatcher used by this instance. - /// The OpenIddict client factory used by this instance. - /// The OpenIddict client OWIN options. - public OpenIddictClientOwinHandler( - IOpenIddictClientDispatcher dispatcher, - IOpenIddictClientFactory factory, - IOptionsMonitor options) - { - _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - _factory = factory ?? throw new ArgumentNullException(nameof(factory)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - } + /// The service provider. + public OpenIddictClientOwinHandler(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); /// protected override async Task InitializeCoreAsync() { + var dispatcher = _provider.GetRequiredService(); + // Note: to ensure internal operations are not immediately cancelled when the request is aborted // (which may represent a security risk if sensitive operations are in progress), an ad-hoc token // source is always created and configured to be triggered 5 seconds after the request is aborted. @@ -61,9 +53,16 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler(typeof(OpenIddictClientTransaction).FullName); if (transaction is null) { + var options = _provider.GetRequiredService>(); + // Create a new transaction and attach the OWIN request to make it available to the OWIN handlers. - transaction = await _factory.CreateTransactionAsync(source.Token); - transaction.Properties[typeof(IOwinRequest).FullName!] = new WeakReference(Request); + transaction = new OpenIddictClientTransaction + { + CancellationToken = source.Token, + Options = options.CurrentValue, + Properties = { [typeof(IOwinRequest).FullName!] = Request }, + ServiceProvider = _provider + }; // Attach the OpenIddict client transaction to the OWIN shared dictionary // so that it can retrieved while performing sign-in/sign-out operations. @@ -71,7 +70,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler(); + var transaction = Context.Get(typeof(OpenIddictClientTransaction).FullName) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0315)); @@ -110,7 +111,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler protected override async Task AuthenticateCoreAsync() { + var dispatcher = _provider.GetRequiredService(); + var transaction = Context.Get(typeof(OpenIddictClientTransaction).FullName) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0315)); @@ -140,7 +143,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler(typeof(ProcessAuthenticationContext).FullName!); if (context is null) { - await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)); + await dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)); // Store the context object in the transaction so it can be later retrieved by handlers // that want to access the authentication result without triggering a new authentication flow. @@ -287,7 +290,9 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler(); + var options = _provider.GetRequiredService>(); + var descriptions = options.CurrentValue.ForwardedAuthenticationTypes; // Note: unlike the ASP.NET Core host, the OWIN host MUST check whether the status code // corresponds to a challenge response, as LookupChallenge() will always return a non-null @@ -308,7 +313,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler reference && reference.TryGetTarget(out IOwinRequest? request)) - { - return request; - } - - return null; + return transaction.Properties.TryGetValue(typeof(IOwinRequest).FullName!, out object? property) + && property is IOwinRequest request ? request : null; } /// diff --git a/src/OpenIddict.Client.Owin/OpenIddictClientOwinMiddleware.cs b/src/OpenIddict.Client.Owin/OpenIddictClientOwinMiddleware.cs index f4ac9216..3ac11be6 100644 --- a/src/OpenIddict.Client.Owin/OpenIddictClientOwinMiddleware.cs +++ b/src/OpenIddict.Client.Owin/OpenIddictClientOwinMiddleware.cs @@ -28,10 +28,12 @@ using AuthenticateDelegate = Func< /// /// Provides the entry point necessary to register the OpenIddict client handler in an OWIN pipeline. +/// +/// /// Note: this middleware is intended to be used with dependency injection containers /// that support middleware resolution, like Autofac. Since it depends on scoped services, /// it is NOT recommended to instantiate it as a singleton like a regular OWIN middleware. -/// +/// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientOwinMiddleware : AuthenticationMiddleware { @@ -54,8 +56,7 @@ public sealed class OpenIddictClientOwinMiddleware : AuthenticationMiddleware>() - ?.CurrentValue ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0316)); + var options = _provider.GetRequiredService>().CurrentValue; // Retrieve the existing authentication delegate. var function = context.Get("security.Authenticate"); @@ -154,8 +155,7 @@ public sealed class OpenIddictClientOwinMiddleware : AuthenticationMiddleware /// A new instance of the class. protected override AuthenticationHandler CreateHandler() - => _provider.GetService() - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0317)); + => new OpenIddictClientOwinHandler(_provider); /// /// Provides the options used by the class. diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs index ff628085..9698e750 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs @@ -13,7 +13,6 @@ using Microsoft.Extensions.Options; #if ANDROID using Android.Content; -using OpenIddict.Extensions; #endif namespace OpenIddict.Client.SystemIntegration; @@ -143,12 +142,17 @@ public sealed class OpenIddictClientSystemIntegrationService await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>(); // Create a client transaction and store the specified instance so // it can be retrieved by the event handlers that need to access it. - var transaction = await factory.CreateTransactionAsync(cancellationToken); - transaction.SetProperty(typeof(TProperty).FullName!, property); + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + Properties = { [typeof(TProperty).FullName!] = property }, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessRequestContext(transaction); await dispatcher.DispatchAsync(context); diff --git a/src/OpenIddict.Client/IOpenIddictClientFactory.cs b/src/OpenIddict.Client/IOpenIddictClientFactory.cs deleted file mode 100644 index 91713f97..00000000 --- a/src/OpenIddict.Client/IOpenIddictClientFactory.cs +++ /dev/null @@ -1,30 +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.ComponentModel; - -namespace OpenIddict.Client; - -/// -/// Represents a service responsible for creating transactions. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IOpenIddictClientFactory -{ - /// - /// Creates a new that is used as a - /// way to store per-request data needed to process the requested operation. - /// - /// The that can be used to abort the operation. - /// - /// Note: the specified is automatically attached to the returned transaction. - /// - /// - /// A that can be used to monitor the asynchronous - /// operation, whose result returns the created transaction. - /// - ValueTask CreateTransactionAsync(CancellationToken cancellationToken); -} diff --git a/src/OpenIddict.Client/OpenIddictClientDispatcher.cs b/src/OpenIddict.Client/OpenIddictClientDispatcher.cs index 5e7dae63..77a430c5 100644 --- a/src/OpenIddict.Client/OpenIddictClientDispatcher.cs +++ b/src/OpenIddict.Client/OpenIddictClientDispatcher.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace OpenIddict.Client; @@ -16,113 +15,73 @@ namespace OpenIddict.Client; [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientDispatcher : IOpenIddictClientDispatcher { - private readonly ILogger _logger; - private readonly IOptionsMonitor _options; - private readonly IServiceProvider _provider; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictClientDispatcher( - ILogger logger, - IOptionsMonitor options, - IServiceProvider provider) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - } - /// public async ValueTask DispatchAsync(TContext context) where TContext : BaseContext { ArgumentNullException.ThrowIfNull(context); - await foreach (var handler in GetHandlersAsync()) + // Note: the descriptors collection is sorted during options initialization for performance reasons. + foreach (var descriptor in context.Options.Handlers) { context.CancellationToken.ThrowIfCancellationRequested(); + if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) + { + continue; + } + + var handler = descriptor.ServiceDescriptor.ImplementationInstance as IOpenIddictClientHandler + ?? context.ServiceProvider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictClientHandler + ?? throw new InvalidOperationException(SR.FormatID0098(descriptor.ServiceDescriptor.ServiceType)); + try { await handler.HandleAsync(context); } - catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception) && _logger.IsEnabled(LogLevel.Debug)) + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception) && context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6132, exception, SR.GetResourceString(SR.ID6132), handler.GetType().FullName, typeof(TContext).FullName); + context.Logger.LogDebug(6132, exception, SR.GetResourceString(SR.ID6132), handler.GetType().FullName, typeof(TContext).FullName); throw; } - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6133, SR.GetResourceString(SR.ID6133), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6133, SR.GetResourceString(SR.ID6133), typeof(TContext).FullName, handler.GetType().FullName); } switch (context) { case BaseRequestContext { IsRequestHandled: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6134, SR.GetResourceString(SR.ID6134), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6134, SR.GetResourceString(SR.ID6134), typeof(TContext).FullName, handler.GetType().FullName); } return; case BaseRequestContext { IsRequestSkipped: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6135, SR.GetResourceString(SR.ID6135), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6135, SR.GetResourceString(SR.ID6135), typeof(TContext).FullName, handler.GetType().FullName); } return; case BaseValidatingContext { IsRejected: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6136, SR.GetResourceString(SR.ID6136), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6136, SR.GetResourceString(SR.ID6136), typeof(TContext).FullName, handler.GetType().FullName); } return; - - default: continue; - } - } - - async IAsyncEnumerable> GetHandlersAsync() - { - // Note: the descriptors collection is sorted during options initialization for performance reasons. - var descriptors = _options.CurrentValue.Handlers; - if (descriptors.Count is 0) - { - yield break; - } - - for (var index = 0; index < descriptors.Count; index++) - { - var descriptor = descriptors[index]; - if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) - { - continue; - } - - yield return descriptor.ServiceDescriptor switch - { - { ImplementationInstance: IOpenIddictClientHandler handler } => handler, - - _ when _provider.GetService(descriptor.ServiceDescriptor.ServiceType) - is IOpenIddictClientHandler handler => handler, - - _ => throw new InvalidOperationException(SR.FormatID0312(descriptor.ServiceDescriptor.ServiceType)) - }; } } async ValueTask IsActiveAsync(OpenIddictClientHandlerDescriptor descriptor) { - for (var index = 0; index < descriptor.FilterTypes.Length; index++) + foreach (var type in descriptor.FilterTypes) { - if (_provider.GetService(descriptor.FilterTypes[index]) is not IOpenIddictClientHandlerFilter filter) - { - throw new InvalidOperationException(SR.FormatID0099(descriptor.FilterTypes[index])); - } + var filter = context.ServiceProvider.GetService(type) as IOpenIddictClientHandlerFilter + ?? throw new InvalidOperationException(SR.FormatID0099(type)); if (!await filter.IsActiveAsync(context)) { diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.cs b/src/OpenIddict.Client/OpenIddictClientEvents.cs index 4f11e13f..c9288b3f 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.cs @@ -8,6 +8,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace OpenIddict.Client; @@ -66,7 +67,8 @@ public static partial class OpenIddictClientEvents /// /// Gets the logger responsible for logging processed operations. /// - public ILogger Logger => Transaction.Logger; + public ILogger Logger + => field ??= Transaction.ServiceProvider.GetRequiredService>(); /// /// Gets the OpenIddict client options. @@ -90,6 +92,12 @@ public static partial class OpenIddictClientEvents get => Transaction.Registration; set => Transaction.Registration = value; } + + /// + /// Gets the service provider associated with the current transaction. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public IServiceProvider ServiceProvider => Transaction.ServiceProvider; } /// diff --git a/src/OpenIddict.Client/OpenIddictClientExtensions.cs b/src/OpenIddict.Client/OpenIddictClientExtensions.cs index 8b1b726f..f6d0ef48 100644 --- a/src/OpenIddict.Client/OpenIddictClientExtensions.cs +++ b/src/OpenIddict.Client/OpenIddictClientExtensions.cs @@ -28,8 +28,7 @@ public static class OpenIddictClientExtensions builder.Services.AddLogging(); builder.Services.AddOptions(); - builder.Services.TryAddScoped(); - builder.Services.TryAddScoped(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); // Register the built-in filters used by the default OpenIddict client event handlers. diff --git a/src/OpenIddict.Client/OpenIddictClientFactory.cs b/src/OpenIddict.Client/OpenIddictClientFactory.cs deleted file mode 100644 index 1ae7a02f..00000000 --- a/src/OpenIddict.Client/OpenIddictClientFactory.cs +++ /dev/null @@ -1,48 +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.ComponentModel; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace OpenIddict.Client; - -/// -/// Represents a service responsible for creating transactions. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class OpenIddictClientFactory : IOpenIddictClientFactory -{ - private readonly ILogger _logger; - private readonly IOptionsMonitor _options; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictClientFactory( - ILogger logger, - IOptionsMonitor options) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// - public ValueTask CreateTransactionAsync(CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return new(Task.FromCanceled(cancellationToken)); - } - - return new(new OpenIddictClientTransaction - { - CancellationToken = cancellationToken, - Logger = _logger, - Options = _options.CurrentValue - }); - } -} diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index da9a3507..3d30b5b9 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -258,15 +258,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessAuthenticationContext(transaction) { @@ -328,15 +330,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessChallengeContext(transaction) { @@ -410,14 +414,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessAuthenticationContext(transaction) { @@ -500,14 +507,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessAuthenticationContext(transaction) { @@ -592,16 +602,17 @@ public class OpenIddictClientService try { - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService>(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessAuthenticationContext(transaction) { @@ -704,15 +715,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessChallengeContext(transaction) { @@ -783,14 +796,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessAuthenticationContext(transaction) { @@ -870,14 +886,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessAuthenticationContext(transaction) { @@ -957,14 +976,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessAuthenticationContext(transaction) { @@ -1042,14 +1064,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessIntrospectionContext(transaction) { @@ -1101,14 +1126,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessRevocationContext(transaction) { @@ -1168,14 +1196,17 @@ public class OpenIddictClientService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var request = new OpenIddictRequest(); request = await PrepareConfigurationRequestAsync(); @@ -1288,15 +1319,17 @@ public class OpenIddictClientService request.CancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); + var options = scope.ServiceProvider.GetRequiredService>(); - var transaction = await factory.CreateTransactionAsync(request.CancellationToken); + var transaction = new OpenIddictClientTransaction + { + CancellationToken = request.CancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var context = new ProcessSignOutContext(transaction) { @@ -1359,14 +1392,17 @@ public class OpenIddictClientService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; var request = new OpenIddictRequest(); request = await PrepareJsonWebKeySetRequestAsync(); @@ -1497,14 +1533,17 @@ public class OpenIddictClientService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; request = await PrepareDeviceAuthorizationRequestAsync(); request = await ApplyDeviceAuthorizationRequestAsync(); @@ -1638,14 +1677,17 @@ public class OpenIddictClientService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; request = await PrepareIntrospectionRequestAsync(); request = await ApplyIntrospectionRequestAsync(); @@ -1782,14 +1824,17 @@ public class OpenIddictClientService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; request = await PreparePushedAuthorizationRequestAsync(); request = await ApplyPushedAuthorizationRequestAsync(); @@ -1923,14 +1968,17 @@ public class OpenIddictClientService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; request = await PrepareRevocationRequestAsync(); request = await ApplyRevocationRequestAsync(); @@ -2065,14 +2113,17 @@ public class OpenIddictClientService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; request = await PrepareTokenRequestAsync(); request = await ApplyTokenRequestAsync(); @@ -2208,14 +2259,17 @@ public class OpenIddictClientService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictClientTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = scope.ServiceProvider + }; request = await PrepareUserInfoRequestAsync(); request = await ApplyUserInfoRequestAsync(); diff --git a/src/OpenIddict.Client/OpenIddictClientTransaction.cs b/src/OpenIddict.Client/OpenIddictClientTransaction.cs index 88a46d20..9935640e 100644 --- a/src/OpenIddict.Client/OpenIddictClientTransaction.cs +++ b/src/OpenIddict.Client/OpenIddictClientTransaction.cs @@ -5,23 +5,22 @@ */ using System.ComponentModel; -using Microsoft.Extensions.Logging; namespace OpenIddict.Client; /// -/// Represents the context associated with an OpenID Connect client request. +/// Represents the context associated with an OpenID Connect client operation. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientTransaction { /// - /// Gets or sets the cancellation token used to determine if the operation was aborted. + /// Gets the cancellation token used to determine if the operation was aborted. /// - public CancellationToken CancellationToken { get; set; } + public required CancellationToken CancellationToken { get; init; } /// - /// Gets or sets the type of the endpoint processing the current request. + /// Gets or sets the type of the endpoint processing the current transaction. /// public OpenIddictClientEndpointType EndpointType { get; set; } @@ -36,29 +35,36 @@ public sealed class OpenIddictClientTransaction public Uri? BaseUri { get; set; } /// - /// Gets or sets the logger associated with the current request. + /// Gets the options associated with the current transaction. /// - public ILogger Logger { get; set; } = default!; + public required OpenIddictClientOptions Options + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } /// - /// Gets or sets the options associated with the current request. - /// - public OpenIddictClientOptions Options { get; set; } = default!; - - /// - /// Gets the additional properties associated with the current request. + /// Gets the additional properties associated with the current transaction. /// public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); /// - /// Gets or sets the client registration used for the current request. + /// Gets or sets the client registration used for the current transaction. /// - public OpenIddictClientRegistration Registration { get; set; } = default!; + public OpenIddictClientRegistration Registration + { + get; + set { ArgumentNullException.ThrowIfNull(value); field = value; } + } = default!; /// - /// Gets or sets the server configuration used for the current request. + /// Gets or sets the server configuration used for the current transaction. /// - public OpenIddictConfiguration Configuration { get; set; } = default!; + public OpenIddictConfiguration Configuration + { + get; + set { ArgumentNullException.ThrowIfNull(value); field = value; } + } = default!; /// /// Gets or sets the current OpenID Connect request. @@ -69,4 +75,13 @@ public sealed class OpenIddictClientTransaction /// Gets or sets the current OpenID Connect response being returned. /// public OpenIddictResponse? Response { get; set; } + + /// + /// Gets the service provider used to resolve services. + /// + public required IServiceProvider ServiceProvider + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs index 47a62032..e1d5be65 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs @@ -9,14 +9,17 @@ using System.ComponentModel; namespace OpenIddict.Server.AspNetCore; /// -/// Exposes the current server transaction to the ASP.NET Core host. +/// Exposes the current server transaction to the ASP.NET Core application. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictServerAspNetCoreFeature { /// - /// Gets or sets the server transaction that encapsulates all specific - /// information about an individual OpenID Connect server request. + /// Gets the transaction that encapsulates all specific information about an individual operation. /// - public OpenIddictServerTransaction? Transaction { get; set; } + public required OpenIddictServerTransaction Transaction + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } } diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs index 35d69def..410fa02b 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Security.Claims; using System.Text.Encodings.Web; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using static OpenIddict.Server.AspNetCore.OpenIddictServerAspNetCoreConstants; @@ -24,22 +25,17 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler /// Creates a new instance of the class. /// public OpenIddictServerAspNetCoreHandler( IOpenIddictServerDispatcher dispatcher, - IOpenIddictServerFactory factory, IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) - { - _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - _factory = factory ?? throw new ArgumentNullException(nameof(factory)); - } + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// public async Task HandleRequestAsync() @@ -59,9 +55,16 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler()?.Transaction; if (transaction is null) { + var options = Context.RequestServices.GetRequiredService>(); + // Create a new transaction and attach the HTTP request to make it available to the ASP.NET Core handlers. - transaction = await _factory.CreateTransactionAsync(source.Token); - transaction.Properties[typeof(HttpRequest).FullName!] = new WeakReference(Request); + transaction = new OpenIddictServerTransaction + { + CancellationToken = source.Token, + Options = options.CurrentValue, + Properties = { [typeof(HttpRequest).FullName!] = Request }, + ServiceProvider = Context.RequestServices + }; // Attach the OpenIddict server transaction to the ASP.NET Core features // so that it can retrieved while performing sign-in/sign-out operations. diff --git a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs index b441d1ce..85ef068f 100644 --- a/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs +++ b/src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs @@ -23,17 +23,9 @@ public static class OpenIddictServerAspNetCoreHelpers { ArgumentNullException.ThrowIfNull(transaction); - if (!transaction.Properties.TryGetValue(typeof(HttpRequest).FullName!, out object? property)) - { - return null; - } + return transaction.Properties.TryGetValue(typeof(HttpRequest).FullName!, out object? property) + && property is HttpRequest request ? request : null; - if (property is WeakReference reference && reference.TryGetTarget(out HttpRequest? request)) - { - return request; - } - - return null; } /// @@ -52,7 +44,7 @@ public static class OpenIddictServerAspNetCoreHelpers /// Retrieves the instance stored in . /// /// The context instance. - /// The instance or null if it couldn't be found. + /// The instance or if it couldn't be found. public static OpenIddictRequest? GetOpenIddictServerRequest(this HttpContext context) { ArgumentNullException.ThrowIfNull(context); @@ -64,7 +56,7 @@ public static class OpenIddictServerAspNetCoreHelpers /// Retrieves the instance stored in . /// /// The context instance. - /// The instance or null if it couldn't be found. + /// The instance or if it couldn't be found. public static OpenIddictResponse? GetOpenIddictServerResponse(this HttpContext context) { ArgumentNullException.ThrowIfNull(context); diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs index 98ee11c6..e5c65cde 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs @@ -31,7 +31,6 @@ public static class OpenIddictServerOwinExtensions // Note: unlike regular OWIN middleware, the OpenIddict server middleware is registered // as a scoped service in the DI container. This allows containers that support middleware // resolution (like Autofac) to use it without requiring additional configuration. - builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); // Register the built-in event handlers used by the OpenIddict OWIN server components. diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs index 93dce5a9..68b8d3c8 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs @@ -6,6 +6,8 @@ using System.ComponentModel; using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.Owin.Security.Infrastructure; using static OpenIddict.Server.Owin.OpenIddictServerOwinConstants; using Properties = OpenIddict.Server.Owin.OpenIddictServerOwinConstants.Properties; @@ -18,25 +20,20 @@ namespace OpenIddict.Server.Owin; [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictServerOwinHandler : AuthenticationHandler { - private readonly IOpenIddictServerDispatcher _dispatcher; - private readonly IOpenIddictServerFactory _factory; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// - /// The OpenIddict server dispatcher used by this instance. - /// The OpenIddict server factory used by this instance. - public OpenIddictServerOwinHandler( - IOpenIddictServerDispatcher dispatcher, - IOpenIddictServerFactory factory) - { - _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - _factory = factory ?? throw new ArgumentNullException(nameof(factory)); - } + /// The service provider. + public OpenIddictServerOwinHandler(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); /// protected override async Task InitializeCoreAsync() { + var dispatcher = _provider.GetRequiredService(); + // Note: to ensure internal operations are not immediately cancelled when the request is aborted // (which may represent a security risk if sensitive operations are in progress), an ad-hoc token // source is always created and configured to be triggered 5 seconds after the request is aborted. @@ -52,9 +49,16 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler(typeof(OpenIddictServerTransaction).FullName); if (transaction is null) { + var options = _provider.GetRequiredService>(); + // Create a new transaction and attach the OWIN request to make it available to the OWIN handlers. - transaction = await _factory.CreateTransactionAsync(source.Token); - transaction.Properties[typeof(IOwinRequest).FullName!] = new WeakReference(Request); + transaction = new OpenIddictServerTransaction + { + CancellationToken = source.Token, + Options = options.CurrentValue, + Properties = { [typeof(IOwinRequest).FullName!] = Request }, + ServiceProvider = _provider + }; // Attach the OpenIddict server transaction to the OWIN shared dictionary // so that it can retrieved while performing sign-in/sign-out operations. @@ -62,7 +66,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler(); + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0112)); @@ -101,7 +107,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler protected override async Task AuthenticateCoreAsync() { + var dispatcher = _provider.GetRequiredService(); + var transaction = Context.Get(typeof(OpenIddictServerTransaction).FullName) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0112)); @@ -131,7 +139,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler(typeof(ProcessAuthenticationContext).FullName!); if (context is null) { - await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)); + await dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)); // Store the context object in the transaction so it can be later retrieved by handlers // that want to access the authentication result without triggering a new authentication flow. @@ -279,6 +287,8 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler(); + // Note: unlike the ASP.NET Core host, the OWIN host MUST check whether the status code // corresponds to a challenge response, as LookupChallenge() will always return a non-null // value when active authentication is used, even if no challenge was actually triggered. @@ -295,7 +305,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler reference && reference.TryGetTarget(out IOwinRequest? request)) - { - return request; - } - - return null; + return transaction.Properties.TryGetValue(typeof(IOwinRequest).FullName!, out object? property) + && property is IOwinRequest request ? request : null; } /// diff --git a/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs b/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs index 1cece580..49d73de8 100644 --- a/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs +++ b/src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs @@ -5,17 +5,18 @@ */ using System.ComponentModel; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Owin.Security.Infrastructure; namespace OpenIddict.Server.Owin; /// /// Provides the entry point necessary to register the OpenIddict server handler in an OWIN pipeline. +/// +/// /// Note: this middleware is intended to be used with dependency injection containers /// that support middleware resolution, like Autofac. Since it depends on scoped services, /// it is NOT recommended to instantiate it as a singleton like a regular OWIN middleware. -/// +/// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictServerOwinMiddleware : AuthenticationMiddleware { @@ -37,8 +38,7 @@ public sealed class OpenIddictServerOwinMiddleware : AuthenticationMiddleware /// A new instance of the class. protected override AuthenticationHandler CreateHandler() - => _provider.GetService() - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0122)); + => new OpenIddictServerOwinHandler(_provider); /// /// Provides the options used by the class. diff --git a/src/OpenIddict.Server/IOpenIddictServerFactory.cs b/src/OpenIddict.Server/IOpenIddictServerFactory.cs deleted file mode 100644 index 695333d1..00000000 --- a/src/OpenIddict.Server/IOpenIddictServerFactory.cs +++ /dev/null @@ -1,30 +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.ComponentModel; - -namespace OpenIddict.Server; - -/// -/// Represents a service responsible for creating transactions. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IOpenIddictServerFactory -{ - /// - /// Creates a new that is used as a - /// way to store per-request data needed to process the requested operation. - /// - /// The that can be used to abort the operation. - /// - /// Note: the specified is automatically attached to the returned transaction. - /// - /// - /// A that can be used to monitor the asynchronous - /// operation, whose result returns the created transaction. - /// - ValueTask CreateTransactionAsync(CancellationToken cancellationToken); -} diff --git a/src/OpenIddict.Server/OpenIddictServerDispatcher.cs b/src/OpenIddict.Server/OpenIddictServerDispatcher.cs index 6edc5576..be8884a0 100644 --- a/src/OpenIddict.Server/OpenIddictServerDispatcher.cs +++ b/src/OpenIddict.Server/OpenIddictServerDispatcher.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace OpenIddict.Server; @@ -16,113 +15,73 @@ namespace OpenIddict.Server; [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictServerDispatcher : IOpenIddictServerDispatcher { - private readonly ILogger _logger; - private readonly IOptionsMonitor _options; - private readonly IServiceProvider _provider; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictServerDispatcher( - ILogger logger, - IOptionsMonitor options, - IServiceProvider provider) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - } - /// public async ValueTask DispatchAsync(TContext context) where TContext : BaseContext { ArgumentNullException.ThrowIfNull(context); - await foreach (var handler in GetHandlersAsync()) + // Note: the descriptors collection is sorted during options initialization for performance reasons. + foreach (var descriptor in context.Options.Handlers) { context.CancellationToken.ThrowIfCancellationRequested(); + if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) + { + continue; + } + + var handler = descriptor.ServiceDescriptor.ImplementationInstance as IOpenIddictServerHandler + ?? context.ServiceProvider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictServerHandler + ?? throw new InvalidOperationException(SR.FormatID0098(descriptor.ServiceDescriptor.ServiceType)); + try { await handler.HandleAsync(context); } - catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception) && _logger.IsEnabled(LogLevel.Debug)) + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception) && context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6132, exception, SR.GetResourceString(SR.ID6132), handler.GetType().FullName, typeof(TContext).FullName); + context.Logger.LogDebug(6132, exception, SR.GetResourceString(SR.ID6132), handler.GetType().FullName, typeof(TContext).FullName); throw; } - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6133, SR.GetResourceString(SR.ID6133), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6133, SR.GetResourceString(SR.ID6133), typeof(TContext).FullName, handler.GetType().FullName); } switch (context) { case BaseRequestContext { IsRequestHandled: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6134, SR.GetResourceString(SR.ID6134), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6134, SR.GetResourceString(SR.ID6134), typeof(TContext).FullName, handler.GetType().FullName); } return; case BaseRequestContext { IsRequestSkipped: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6135, SR.GetResourceString(SR.ID6135), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6135, SR.GetResourceString(SR.ID6135), typeof(TContext).FullName, handler.GetType().FullName); } return; case BaseValidatingContext { IsRejected: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6136, SR.GetResourceString(SR.ID6136), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6136, SR.GetResourceString(SR.ID6136), typeof(TContext).FullName, handler.GetType().FullName); } return; - - default: continue; - } - } - - async IAsyncEnumerable> GetHandlersAsync() - { - // Note: the descriptors collection is sorted during options initialization for performance reasons. - var descriptors = _options.CurrentValue.Handlers; - if (descriptors.Count is 0) - { - yield break; - } - - for (var index = 0; index < descriptors.Count; index++) - { - var descriptor = descriptors[index]; - if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) - { - continue; - } - - yield return descriptor.ServiceDescriptor switch - { - { ImplementationInstance: IOpenIddictServerHandler handler } => handler, - - _ when _provider.GetService(descriptor.ServiceDescriptor.ServiceType) - is IOpenIddictServerHandler handler => handler, - - _ => throw new InvalidOperationException(SR.FormatID0098(descriptor.ServiceDescriptor.ServiceType)) - }; } } async ValueTask IsActiveAsync(OpenIddictServerHandlerDescriptor descriptor) { - for (var index = 0; index < descriptor.FilterTypes.Length; index++) + foreach (var type in descriptor.FilterTypes) { - if (_provider.GetService(descriptor.FilterTypes[index]) is not IOpenIddictServerHandlerFilter filter) - { - throw new InvalidOperationException(SR.FormatID0099(descriptor.FilterTypes[index])); - } + var filter = context.ServiceProvider.GetService(type) as IOpenIddictServerHandlerFilter + ?? throw new InvalidOperationException(SR.FormatID0099(type)); if (!await filter.IsActiveAsync(context)) { diff --git a/src/OpenIddict.Server/OpenIddictServerEvents.cs b/src/OpenIddict.Server/OpenIddictServerEvents.cs index dce4f282..cf6c6b46 100644 --- a/src/OpenIddict.Server/OpenIddictServerEvents.cs +++ b/src/OpenIddict.Server/OpenIddictServerEvents.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace OpenIddict.Server; @@ -64,12 +65,19 @@ public static partial class OpenIddictServerEvents /// /// Gets the logger responsible for logging processed operations. /// - public ILogger Logger => Transaction.Logger; + public ILogger Logger + => field ??= Transaction.ServiceProvider.GetRequiredService>(); /// /// Gets the OpenIddict server options. /// public OpenIddictServerOptions Options => Transaction.Options; + + /// + /// Gets the service provider associated with the current transaction. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public IServiceProvider ServiceProvider => Transaction.ServiceProvider; } /// diff --git a/src/OpenIddict.Server/OpenIddictServerExtensions.cs b/src/OpenIddict.Server/OpenIddictServerExtensions.cs index 816a216c..baba1377 100644 --- a/src/OpenIddict.Server/OpenIddictServerExtensions.cs +++ b/src/OpenIddict.Server/OpenIddictServerExtensions.cs @@ -29,8 +29,7 @@ public static class OpenIddictServerExtensions builder.Services.AddLogging(); builder.Services.AddOptions(); - builder.Services.TryAddScoped(); - builder.Services.TryAddScoped(); + builder.Services.TryAddSingleton(); // Register the built-in server event handlers used by the OpenIddict server components. // Note: the order used here is not important, as the actual order is set in the options. diff --git a/src/OpenIddict.Server/OpenIddictServerFactory.cs b/src/OpenIddict.Server/OpenIddictServerFactory.cs deleted file mode 100644 index 7c0394f6..00000000 --- a/src/OpenIddict.Server/OpenIddictServerFactory.cs +++ /dev/null @@ -1,48 +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.ComponentModel; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace OpenIddict.Server; - -/// -/// Represents a service responsible for creating transactions. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class OpenIddictServerFactory : IOpenIddictServerFactory -{ - private readonly ILogger _logger; - private readonly IOptionsMonitor _options; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictServerFactory( - ILogger logger, - IOptionsMonitor options) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// - public ValueTask CreateTransactionAsync(CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return new(Task.FromCanceled(cancellationToken)); - } - - return new(new OpenIddictServerTransaction - { - CancellationToken = cancellationToken, - Logger = _logger, - Options = _options.CurrentValue - }); - } -} diff --git a/src/OpenIddict.Server/OpenIddictServerTransaction.cs b/src/OpenIddict.Server/OpenIddictServerTransaction.cs index 49b63487..ad8780dd 100644 --- a/src/OpenIddict.Server/OpenIddictServerTransaction.cs +++ b/src/OpenIddict.Server/OpenIddictServerTransaction.cs @@ -6,20 +6,19 @@ using System.ComponentModel; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Logging; namespace OpenIddict.Server; /// -/// Represents the context associated with an OpenID Connect server request. +/// Represents the context associated with an OpenID Connect server operation. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictServerTransaction { /// - /// Gets or sets the cancellation token used to determine if the operation was aborted. + /// Gets the cancellation token used to determine if the operation was aborted. /// - public CancellationToken CancellationToken { get; set; } + public required CancellationToken CancellationToken { get; init; } /// /// Gets or sets the X.509 client certificate used by the remote peer, if available. @@ -27,7 +26,7 @@ public sealed class OpenIddictServerTransaction public X509Certificate2? RemoteCertificate { get; set; } /// - /// Gets or sets the type of the endpoint processing the current request. + /// Gets or sets the type of the endpoint processing the current transaction. /// public OpenIddictServerEndpointType EndpointType { get; set; } @@ -42,17 +41,16 @@ public sealed class OpenIddictServerTransaction public Uri? BaseUri { get; set; } /// - /// Gets or sets the logger associated with the current request. + /// Gets the options associated with the current transaction. /// - public ILogger Logger { get; set; } = default!; + public required OpenIddictServerOptions Options + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } /// - /// Gets or sets the options associated with the current request. - /// - public OpenIddictServerOptions Options { get; set; } = default!; - - /// - /// Gets the additional properties associated with the current request. + /// Gets the additional properties associated with the current transaction. /// public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); @@ -65,4 +63,13 @@ public sealed class OpenIddictServerTransaction /// Gets or sets the current OpenID Connect response being returned. /// public OpenIddictResponse? Response { get; set; } + + /// + /// Gets the service provider used to resolve services. + /// + public required IServiceProvider ServiceProvider + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } } diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreFeature.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreFeature.cs index 456d6518..2fa0d704 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreFeature.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreFeature.cs @@ -9,14 +9,17 @@ using System.ComponentModel; namespace OpenIddict.Validation.AspNetCore; /// -/// Exposes the current validation transaction to the ASP.NET Core host. +/// Exposes the current validation transaction to the ASP.NET Core application. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictValidationAspNetCoreFeature { /// - /// Gets or sets the validation transaction that encapsulates all specific - /// information about an individual OpenID Connect validation request. + /// Gets the transaction that encapsulates all specific information about an individual operation. /// - public OpenIddictValidationTransaction? Transaction { get; set; } + public required OpenIddictValidationTransaction Transaction + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } } diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs index e9807f6c..c081c21f 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Security.Claims; using System.Text.Encodings.Web; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using static OpenIddict.Validation.AspNetCore.OpenIddictValidationAspNetCoreConstants; @@ -22,22 +23,17 @@ public sealed class OpenIddictValidationAspNetCoreHandler : AuthenticationHandle IAuthenticationRequestHandler { private readonly IOpenIddictValidationDispatcher _dispatcher; - private readonly IOpenIddictValidationFactory _factory; /// /// Creates a new instance of the class. /// public OpenIddictValidationAspNetCoreHandler( IOpenIddictValidationDispatcher dispatcher, - IOpenIddictValidationFactory factory, IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) : base(options, logger, encoder) - { - _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - _factory = factory ?? throw new ArgumentNullException(nameof(factory)); - } + => _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); /// public async Task HandleRequestAsync() @@ -57,9 +53,16 @@ public sealed class OpenIddictValidationAspNetCoreHandler : AuthenticationHandle var transaction = Context.Features.Get()?.Transaction; if (transaction is null) { + var options = Context.RequestServices.GetRequiredService>(); + // Create a new transaction and attach the HTTP request to make it available to the ASP.NET Core handlers. - transaction = await _factory.CreateTransactionAsync(source.Token); - transaction.Properties[typeof(HttpRequest).FullName!] = new WeakReference(Request); + transaction = new OpenIddictValidationTransaction + { + CancellationToken = source.Token, + Options = options.CurrentValue, + Properties = { [typeof(HttpRequest).FullName!] = Request }, + ServiceProvider = Context.RequestServices + }; // Attach the OpenIddict validation transaction to the ASP.NET Core features // so that it can retrieved while performing challenge/forbid operations. diff --git a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHelpers.cs b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHelpers.cs index a51cfce3..c6a54189 100644 --- a/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHelpers.cs +++ b/src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHelpers.cs @@ -23,17 +23,8 @@ public static class OpenIddictValidationAspNetCoreHelpers { ArgumentNullException.ThrowIfNull(transaction); - if (!transaction.Properties.TryGetValue(typeof(HttpRequest).FullName!, out object? property)) - { - return null; - } - - if (property is WeakReference reference && reference.TryGetTarget(out HttpRequest? request)) - { - return request; - } - - return null; + return transaction.Properties.TryGetValue(typeof(HttpRequest).FullName!, out object? property) + && property is HttpRequest request ? request : null; } /// @@ -52,7 +43,7 @@ public static class OpenIddictValidationAspNetCoreHelpers /// Retrieves the instance stored in . /// /// The context instance. - /// The instance or null if it couldn't be found. + /// The instance or if it couldn't be found. public static OpenIddictRequest? GetOpenIddictValidationRequest(this HttpContext context) { ArgumentNullException.ThrowIfNull(context); @@ -64,7 +55,7 @@ public static class OpenIddictValidationAspNetCoreHelpers /// Retrieves the instance stored in . /// /// The context instance. - /// The instance or null if it couldn't be found. + /// The instance or if it couldn't be found. public static OpenIddictResponse? GetOpenIddictValidationResponse(this HttpContext context) { ArgumentNullException.ThrowIfNull(context); diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs index 1266cb9e..e330061f 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs @@ -29,7 +29,6 @@ public static class OpenIddictValidationOwinExtensions // Note: unlike regular OWIN middleware, the OpenIddict validation middleware is registered // as a scoped service in the DI container. This allows containers that support middleware // resolution (like Autofac) to use it without requiring additional configuration. - builder.Services.TryAddScoped(); builder.Services.TryAddScoped(); // Register the built-in event handlers used by the OpenIddict OWIN validation components. diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs index d6612fa8..e93b5f2a 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs @@ -6,6 +6,8 @@ using System.ComponentModel; using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Microsoft.Owin.Security.Infrastructure; using static OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants; using Properties = OpenIddict.Validation.Owin.OpenIddictValidationOwinConstants.Properties; @@ -18,25 +20,20 @@ namespace OpenIddict.Validation.Owin; [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler { - private readonly IOpenIddictValidationDispatcher _dispatcher; - private readonly IOpenIddictValidationFactory _factory; + private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// - /// The OpenIddict validation provider used by this instance. - /// The OpenIddict validation factory used by this instance. - public OpenIddictValidationOwinHandler( - IOpenIddictValidationDispatcher dispatcher, - IOpenIddictValidationFactory factory) - { - _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); - _factory = factory ?? throw new ArgumentNullException(nameof(factory)); - } + /// The service provider. + public OpenIddictValidationOwinHandler(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); /// protected override async Task InitializeCoreAsync() { + var dispatcher = _provider.GetRequiredService(); + // Note: to ensure internal operations are not immediately cancelled when the request is aborted // (which may represent a security risk if sensitive operations are in progress), an ad-hoc token // source is always created and configured to be triggered 5 seconds after the request is aborted. @@ -52,9 +49,16 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler(typeof(OpenIddictValidationTransaction).FullName); if (transaction is null) { + var options = _provider.GetRequiredService>(); + // Create a new transaction and attach the OWIN request to make it available to the OWIN handlers. - transaction = await _factory.CreateTransactionAsync(source.Token); - transaction.Properties[typeof(IOwinRequest).FullName!] = new WeakReference(Request); + transaction = new OpenIddictValidationTransaction + { + CancellationToken = source.Token, + Options = options.CurrentValue, + Properties = { [typeof(IOwinRequest).FullName!] = Request }, + ServiceProvider = _provider + }; // Attach the OpenIddict validation transaction to the OWIN shared dictionary // so that it can retrieved while performing sign-in/sign-out operations. @@ -62,7 +66,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler(); + var transaction = Context.Get(typeof(OpenIddictValidationTransaction).FullName) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0166)); @@ -101,7 +107,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler protected override async Task AuthenticateCoreAsync() { + var dispatcher = _provider.GetRequiredService(); + var transaction = Context.Get(typeof(OpenIddictValidationTransaction).FullName) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0166)); @@ -131,7 +139,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler(typeof(ProcessAuthenticationContext).FullName!); if (context is null) { - await _dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)); + await dispatcher.DispatchAsync(context = new ProcessAuthenticationContext(transaction)); // Store the context object in the transaction so it can be later retrieved by handlers // that want to access the authentication result without triggering a new authentication flow. @@ -211,6 +219,8 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler(); + // Note: unlike the ASP.NET Core host, the OWIN host MUST check whether the status code // corresponds to a challenge response, as LookupChallenge() will always return a non-null // value when active authentication is used, even if no challenge was actually triggered. @@ -227,7 +237,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler reference && reference.TryGetTarget(out IOwinRequest? request)) - { - return request; - } - - return null; + return transaction.Properties.TryGetValue(typeof(IOwinRequest).FullName!, out object? property) + && property is IOwinRequest request ? request : null; } /// diff --git a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs index 28ed56db..2524c240 100644 --- a/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs +++ b/src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs @@ -13,10 +13,12 @@ namespace OpenIddict.Validation.Owin; /// /// Provides the entry point necessary to register the OpenIddict validation handler in an OWIN pipeline. +/// +/// /// Note: this middleware is intended to be used with dependency injection containers /// that support middleware resolution, like Autofac. Since it depends on scoped services, /// it is NOT recommended to instantiate it as a singleton like a regular OWIN middleware. -/// +/// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictValidationOwinMiddleware : AuthenticationMiddleware { @@ -32,9 +34,8 @@ public sealed class OpenIddictValidationOwinMiddleware : AuthenticationMiddlewar IServiceProvider provider) : base(next, new InternalOptions() { - AuthenticationMode = provider.GetService>() - ?.CurrentValue.AuthenticationMode - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0169)) + AuthenticationMode = provider.GetRequiredService>() + .CurrentValue.AuthenticationMode }) => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); @@ -43,8 +44,7 @@ public sealed class OpenIddictValidationOwinMiddleware : AuthenticationMiddlewar /// /// A new instance of the class. protected override AuthenticationHandler CreateHandler() - => _provider.GetService() - ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0169)); + => new OpenIddictValidationOwinHandler(_provider); /// /// Provides the options used by the class. diff --git a/src/OpenIddict.Validation/IOpenIddictValidationFactory.cs b/src/OpenIddict.Validation/IOpenIddictValidationFactory.cs deleted file mode 100644 index 5ad893d4..00000000 --- a/src/OpenIddict.Validation/IOpenIddictValidationFactory.cs +++ /dev/null @@ -1,30 +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.ComponentModel; - -namespace OpenIddict.Validation; - -/// -/// Represents a service responsible for creating transactions. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public interface IOpenIddictValidationFactory -{ - /// - /// Creates a new that is used as a - /// way to store per-request data needed to process the requested operation. - /// - /// The that can be used to abort the operation. - /// - /// Note: the specified is automatically attached to the returned transaction. - /// - /// - /// A that can be used to monitor the asynchronous - /// operation, whose result returns the created transaction. - /// - ValueTask CreateTransactionAsync(CancellationToken cancellationToken); -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationDispatcher.cs b/src/OpenIddict.Validation/OpenIddictValidationDispatcher.cs index b0fc80e1..3cc2012c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationDispatcher.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationDispatcher.cs @@ -6,7 +6,6 @@ using System.ComponentModel; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace OpenIddict.Validation; @@ -16,113 +15,73 @@ namespace OpenIddict.Validation; [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictValidationDispatcher : IOpenIddictValidationDispatcher { - private readonly ILogger _logger; - private readonly IOptionsMonitor _options; - private readonly IServiceProvider _provider; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictValidationDispatcher( - ILogger logger, - IOptionsMonitor options, - IServiceProvider provider) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - } - /// public async ValueTask DispatchAsync(TContext context) where TContext : BaseContext { ArgumentNullException.ThrowIfNull(context); - await foreach (var handler in GetHandlersAsync()) + // Note: the descriptors collection is sorted during options initialization for performance reasons. + foreach (var descriptor in context.Options.Handlers) { context.CancellationToken.ThrowIfCancellationRequested(); + if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) + { + continue; + } + + var handler = descriptor.ServiceDescriptor.ImplementationInstance as IOpenIddictValidationHandler + ?? context.ServiceProvider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictValidationHandler + ?? throw new InvalidOperationException(SR.FormatID0098(descriptor.ServiceDescriptor.ServiceType)); + try { await handler.HandleAsync(context); } - catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception) && _logger.IsEnabled(LogLevel.Debug)) + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception) && context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6132, exception, SR.GetResourceString(SR.ID6132), handler.GetType().FullName, typeof(TContext).FullName); + context.Logger.LogDebug(6132, exception, SR.GetResourceString(SR.ID6132), handler.GetType().FullName, typeof(TContext).FullName); throw; } - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6133, SR.GetResourceString(SR.ID6133), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6133, SR.GetResourceString(SR.ID6133), typeof(TContext).FullName, handler.GetType().FullName); } switch (context) { case BaseRequestContext { IsRequestHandled: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6134, SR.GetResourceString(SR.ID6134), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6134, SR.GetResourceString(SR.ID6134), typeof(TContext).FullName, handler.GetType().FullName); } return; case BaseRequestContext { IsRequestSkipped: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6135, SR.GetResourceString(SR.ID6135), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6135, SR.GetResourceString(SR.ID6135), typeof(TContext).FullName, handler.GetType().FullName); } return; case BaseValidatingContext { IsRejected: true }: - if (_logger.IsEnabled(LogLevel.Debug)) + if (context.Logger.IsEnabled(LogLevel.Debug)) { - _logger.LogDebug(6136, SR.GetResourceString(SR.ID6136), typeof(TContext).FullName, handler.GetType().FullName); + context.Logger.LogDebug(6136, SR.GetResourceString(SR.ID6136), typeof(TContext).FullName, handler.GetType().FullName); } return; - - default: continue; - } - } - - async IAsyncEnumerable> GetHandlersAsync() - { - // Note: the descriptors collection is sorted during options initialization for performance reasons. - var descriptors = _options.CurrentValue.Handlers; - if (descriptors.Count is 0) - { - yield break; - } - - for (var index = 0; index < descriptors.Count; index++) - { - var descriptor = descriptors[index]; - if (descriptor.ContextType != typeof(TContext) || !await IsActiveAsync(descriptor)) - { - continue; - } - - yield return descriptor.ServiceDescriptor switch - { - { ImplementationInstance: IOpenIddictValidationHandler handler } => handler, - - _ when _provider.GetService(descriptor.ServiceDescriptor.ServiceType) - is IOpenIddictValidationHandler handler => handler, - - _ => throw new InvalidOperationException(SR.FormatID0138(descriptor.ServiceDescriptor.ServiceType)) - }; } } async ValueTask IsActiveAsync(OpenIddictValidationHandlerDescriptor descriptor) { - for (var index = 0; index < descriptor.FilterTypes.Length; index++) + foreach (var type in descriptor.FilterTypes) { - if (_provider.GetService(descriptor.FilterTypes[index]) is not IOpenIddictValidationHandlerFilter filter) - { - throw new InvalidOperationException(SR.FormatID0099(descriptor.FilterTypes[index])); - } + var filter = context.ServiceProvider.GetService(type) as IOpenIddictValidationHandlerFilter + ?? throw new InvalidOperationException(SR.FormatID0099(type)); if (!await filter.IsActiveAsync(context)) { diff --git a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs index 0fd03904..4c5c62e7 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationEvents.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationEvents.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; namespace OpenIddict.Validation; @@ -74,12 +75,19 @@ public static partial class OpenIddictValidationEvents /// /// Gets the logger responsible for logging processed operations. /// - public ILogger Logger => Transaction.Logger; + public ILogger Logger + => field ??= Transaction.ServiceProvider.GetRequiredService>(); /// /// Gets the OpenIddict validation options. /// public OpenIddictValidationOptions Options => Transaction.Options; + + /// + /// Gets the service provider associated with the current transaction. + /// + [EditorBrowsable(EditorBrowsableState.Advanced)] + public IServiceProvider ServiceProvider => Transaction.ServiceProvider; } /// diff --git a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs index 787284b5..67e1f76c 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationExtensions.cs @@ -30,8 +30,7 @@ public static class OpenIddictValidationExtensions builder.Services.AddOptions(); builder.Services.TryAddSingleton(); - builder.Services.TryAddScoped(); - builder.Services.TryAddScoped(); + builder.Services.TryAddSingleton(); // Register the built-in validation event handlers used by the OpenIddict validation components. // Note: the order used here is not important, as the actual order is set in the options. diff --git a/src/OpenIddict.Validation/OpenIddictValidationFactory.cs b/src/OpenIddict.Validation/OpenIddictValidationFactory.cs deleted file mode 100644 index 4b806f42..00000000 --- a/src/OpenIddict.Validation/OpenIddictValidationFactory.cs +++ /dev/null @@ -1,48 +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.ComponentModel; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace OpenIddict.Validation; - -/// -/// Represents a service responsible for creating transactions. -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class OpenIddictValidationFactory : IOpenIddictValidationFactory -{ - private readonly ILogger _logger; - private readonly IOptionsMonitor _options; - - /// - /// Creates a new instance of the class. - /// - public OpenIddictValidationFactory( - ILogger logger, - IOptionsMonitor options) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - } - - /// - public ValueTask CreateTransactionAsync(CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return new(Task.FromCanceled(cancellationToken)); - } - - return new(new OpenIddictValidationTransaction - { - CancellationToken = cancellationToken, - Logger = _logger, - Options = _options.CurrentValue - }); - } -} diff --git a/src/OpenIddict.Validation/OpenIddictValidationService.cs b/src/OpenIddict.Validation/OpenIddictValidationService.cs index b3da7d7a..b01cda86 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationService.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationService.cs @@ -9,6 +9,7 @@ using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using static OpenIddict.Abstractions.OpenIddictExceptions; @@ -40,14 +41,17 @@ public class OpenIddictValidationService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictValidationTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = _provider + }; var context = new ProcessAuthenticationContext(transaction) { @@ -85,14 +89,17 @@ public class OpenIddictValidationService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictValidationTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = _provider + }; var request = new OpenIddictRequest(); request = await PrepareConfigurationRequestAsync(); @@ -207,14 +214,17 @@ public class OpenIddictValidationService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictValidationTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = _provider + }; var request = new OpenIddictRequest(); request = await PrepareJsonWebKeySetRequestAsync(); @@ -337,14 +347,17 @@ public class OpenIddictValidationService cancellationToken.ThrowIfCancellationRequested(); - // Note: this service is registered as a singleton service. As such, it cannot - // directly depend on scoped services like the event dispatcher. To work around - // this limitation, a scope is manually created for each method to this service. await using var scope = _provider.CreateAsyncScope(); var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - var transaction = await factory.CreateTransactionAsync(cancellationToken); + var options = scope.ServiceProvider.GetRequiredService>(); + + var transaction = new OpenIddictValidationTransaction + { + CancellationToken = cancellationToken, + Options = options.CurrentValue, + ServiceProvider = _provider + }; request = await PrepareIntrospectionRequestAsync(); request = await ApplyIntrospectionRequestAsync(); diff --git a/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs b/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs index 341f851a..2d5016d0 100644 --- a/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs +++ b/src/OpenIddict.Validation/OpenIddictValidationTransaction.cs @@ -6,20 +6,19 @@ using System.ComponentModel; using System.Security.Cryptography.X509Certificates; -using Microsoft.Extensions.Logging; namespace OpenIddict.Validation; /// -/// Represents the context associated with an OpenID Connect validation request. +/// Represents the context associated with an OpenID Connect validation operation. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictValidationTransaction { /// - /// Gets or sets the cancellation token used to determine if the operation was aborted. + /// Gets the cancellation token used to determine if the operation was aborted. /// - public CancellationToken CancellationToken { get; set; } + public required CancellationToken CancellationToken { get; init; } /// /// Gets or sets the X.509 client certificate used by the remote peer, if available. @@ -27,7 +26,7 @@ public sealed class OpenIddictValidationTransaction public X509Certificate2? RemoteCertificate { get; set; } /// - /// Gets or sets the type of the endpoint processing the current request. + /// Gets or sets the type of the endpoint processing the current transaction. /// public OpenIddictValidationEndpointType EndpointType { get; set; } @@ -42,24 +41,27 @@ public sealed class OpenIddictValidationTransaction public Uri? BaseUri { get; set; } /// - /// Gets or sets the logger associated with the current request. + /// Gets the options associated with the current transaction. /// - public ILogger Logger { get; set; } = default!; + public required OpenIddictValidationOptions Options + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } /// - /// Gets or sets the options associated with the current request. - /// - public OpenIddictValidationOptions Options { get; set; } = default!; - - /// - /// Gets the additional properties associated with the current request. + /// Gets the additional properties associated with the current transaction. /// public Dictionary Properties { get; } = new(StringComparer.OrdinalIgnoreCase); /// - /// Gets or sets the server configuration used for the current request. + /// Gets or sets the server configuration used for the current transaction. /// - public OpenIddictConfiguration Configuration { get; set; } = default!; + public OpenIddictConfiguration Configuration + { + get; + set { ArgumentNullException.ThrowIfNull(value); field = value; } + } = default!; /// /// Gets or sets the current OpenID Connect request. @@ -70,4 +72,13 @@ public sealed class OpenIddictValidationTransaction /// Gets or sets the current OpenID Connect response being returned. /// public OpenIddictResponse? Response { get; set; } + + /// + /// Gets the service provider used to resolve services. + /// + public required IServiceProvider ServiceProvider + { + get; + init { ArgumentNullException.ThrowIfNull(value); field = value; } + } } diff --git a/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs b/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs index b8de9d76..474958d4 100644 --- a/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs +++ b/test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs @@ -79,23 +79,7 @@ public class OpenIddictServerExtensionsTests // Assert Assert.Contains(services, service => service.ServiceType == typeof(IOpenIddictServerDispatcher) && service.ImplementationType == typeof(OpenIddictServerDispatcher) && - service.Lifetime is ServiceLifetime.Scoped); - } - - [Fact] - public void AddServer_RegistersServerFactory() - { - // Arrange - var services = new ServiceCollection(); - var builder = new OpenIddictBuilder(services); - - // Act - builder.AddServer(); - - // Assert - Assert.Contains(services, service => service.ServiceType == typeof(IOpenIddictServerFactory) && - service.ImplementationType == typeof(OpenIddictServerFactory) && - service.Lifetime is ServiceLifetime.Scoped); + service.Lifetime is ServiceLifetime.Singleton); } public static IEnumerable DefaultHandlers