Browse Source

Attach IServiceProvider to OpenIddict*Transaction and make OpenIddict*Dispatcher a singleton service

pull/2532/head
Kévin Chalet 2 weeks ago
parent
commit
5fc15473ce
  1. 2
      sandbox/OpenIddict.Sandbox.AspNet.Server/Controllers/AuthorizationController.cs
  2. 12
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  3. 33
      src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs
  4. 11
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs
  5. 19
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHandler.cs
  6. 13
      src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreHelpers.cs
  7. 1
      src/OpenIddict.Client.Owin/OpenIddictClientOwinExtensions.cs
  8. 55
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHandler.cs
  9. 13
      src/OpenIddict.Client.Owin/OpenIddictClientOwinHelpers.cs
  10. 10
      src/OpenIddict.Client.Owin/OpenIddictClientOwinMiddleware.cs
  11. 12
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs
  12. 30
      src/OpenIddict.Client/IOpenIddictClientFactory.cs
  13. 89
      src/OpenIddict.Client/OpenIddictClientDispatcher.cs
  14. 10
      src/OpenIddict.Client/OpenIddictClientEvents.cs
  15. 3
      src/OpenIddict.Client/OpenIddictClientExtensions.cs
  16. 48
      src/OpenIddict.Client/OpenIddictClientFactory.cs
  17. 254
      src/OpenIddict.Client/OpenIddictClientService.cs
  18. 49
      src/OpenIddict.Client/OpenIddictClientTransaction.cs
  19. 11
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs
  20. 19
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHandler.cs
  21. 16
      src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreHelpers.cs
  22. 1
      src/OpenIddict.Server.Owin/OpenIddictServerOwinExtensions.cs
  23. 54
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHandler.cs
  24. 13
      src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs
  25. 8
      src/OpenIddict.Server.Owin/OpenIddictServerOwinMiddleware.cs
  26. 30
      src/OpenIddict.Server/IOpenIddictServerFactory.cs
  27. 89
      src/OpenIddict.Server/OpenIddictServerDispatcher.cs
  28. 10
      src/OpenIddict.Server/OpenIddictServerEvents.cs
  29. 3
      src/OpenIddict.Server/OpenIddictServerExtensions.cs
  30. 48
      src/OpenIddict.Server/OpenIddictServerFactory.cs
  31. 33
      src/OpenIddict.Server/OpenIddictServerTransaction.cs
  32. 11
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreFeature.cs
  33. 19
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHandler.cs
  34. 17
      src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreHelpers.cs
  35. 1
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinExtensions.cs
  36. 46
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHandler.cs
  37. 13
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHelpers.cs
  38. 12
      src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs
  39. 30
      src/OpenIddict.Validation/IOpenIddictValidationFactory.cs
  40. 89
      src/OpenIddict.Validation/OpenIddictValidationDispatcher.cs
  41. 10
      src/OpenIddict.Validation/OpenIddictValidationEvents.cs
  42. 3
      src/OpenIddict.Validation/OpenIddictValidationExtensions.cs
  43. 48
      src/OpenIddict.Validation/OpenIddictValidationFactory.cs
  44. 53
      src/OpenIddict.Validation/OpenIddictValidationService.cs
  45. 41
      src/OpenIddict.Validation/OpenIddictValidationTransaction.cs
  46. 18
      test/OpenIddict.Server.Tests/OpenIddictServerExtensionsTests.cs

2
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
});

12
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -501,10 +501,6 @@ This may indicate that the event handler responsible for processing OpenID Conne
<value>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.</value>
</data>
<data name="ID0122" xml:space="preserve">
<value>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()'.</value>
</data>
<data name="ID0123" xml:space="preserve">
<value>Audiences cannot be null or empty.</value>
@ -693,10 +689,6 @@ Make sure that neither DefaultSignInScheme nor DefaultSignOutScheme point to an
<value>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.</value>
</data>
<data name="ID0169" xml:space="preserve">
<value>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()'.</value>
</data>
<data name="ID0170" xml:space="preserve">
<value>The local server integration can only be used with direct validation.</value>
@ -1044,10 +1036,6 @@ Reference the 'OpenIddict.Client.SystemNetHttp' package and call 'services.AddOp
<value>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.</value>
</data>
<data name="ID0317" xml:space="preserve">
<value>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()'.</value>
</data>
<data name="ID0318" xml:space="preserve">
<value>The core services must be registered when enabling the OpenIddict client feature.

33
src/OpenIddict.Abstractions/Primitives/OpenIddictParameter.cs

@ -175,15 +175,10 @@ public readonly struct OpenIddictParameter : IEquatable<OpenIddictParameter>
null => null
};
/// <summary>
/// Determines whether the current <see cref="OpenIddictParameter"/>
/// instance is equal to the specified <see cref="OpenIddictParameter"/>.
/// </summary>
/// <param name="other">The other object to which to compare this instance.</param>
/// <returns>
/// <see langword="true"/> if the two instances have both the same representation
/// (e.g <see cref="string"/>) and value, <see langword="false"/> otherwise.
/// </returns>
/// <inheritdoc/>
/// <remarks>
/// Two instances are considered equal if they have the same representation.
/// </remarks>
public bool Equals(OpenIddictParameter other)
{
return (_value, other._value) switch
@ -282,22 +277,14 @@ public readonly struct OpenIddictParameter : IEquatable<OpenIddictParameter>
};
}
/// <summary>
/// Determines whether the current <see cref="OpenIddictParameter"/>
/// instance is equal to the specified <see cref="object"/>.
/// </summary>
/// <param name="obj">The other object to which to compare this instance.</param>
/// <returns>
/// <see langword="true"/> if the two instances have both the same representation
/// (e.g <see cref="string"/>) and value, <see langword="false"/> otherwise.
/// </returns>
/// <inheritdoc/>
/// <remarks>
/// Two instances are considered equal if they have the same representation.
/// </remarks>
public override bool Equals([NotNullWhen(true)] object? obj)
=> obj is OpenIddictParameter parameter && Equals(parameter);
/// <summary>
/// Returns the hash code of the current <see cref="OpenIddictParameter"/> instance.
/// </summary>
/// <returns>The hash code for the current instance.</returns>
/// <inheritdoc/>
public override int GetHashCode()
{
return _value switch
@ -1010,7 +997,7 @@ public readonly struct OpenIddictParameter : IEquatable<OpenIddictParameter>
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.

11
src/OpenIddict.Client.AspNetCore/OpenIddictClientAspNetCoreFeature.cs

@ -9,14 +9,17 @@ using System.ComponentModel;
namespace OpenIddict.Client.AspNetCore;
/// <summary>
/// Exposes the current client transaction to the ASP.NET Core host.
/// Exposes the current client transaction to the ASP.NET Core application.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientAspNetCoreFeature
{
/// <summary>
/// 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.
/// </summary>
public OpenIddictClientTransaction? Transaction { get; set; }
public required OpenIddictClientTransaction Transaction
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
}

19
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<Au
IAuthenticationSignOutHandler
{
private readonly IOpenIddictClientDispatcher _dispatcher;
private readonly IOpenIddictClientFactory _factory;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientAspNetCoreHandler"/> class.
/// </summary>
public OpenIddictClientAspNetCoreHandler(
IOpenIddictClientDispatcher dispatcher,
IOpenIddictClientFactory factory,
IOptionsMonitor<AuthenticationSchemeOptions> 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));
/// <inheritdoc/>
public async Task<bool> HandleRequestAsync()
@ -59,9 +55,16 @@ public sealed class OpenIddictClientAspNetCoreHandler : AuthenticationHandler<Au
var transaction = Context.Features.Get<OpenIddictClientAspNetCoreFeature>()?.Transaction;
if (transaction is null)
{
var options = Context.RequestServices.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
// 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<HttpRequest>(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.

13
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<HttpRequest> 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;
}
/// <summary>

1
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<OpenIddictClientOwinHandler>();
builder.Services.TryAddScoped<OpenIddictClientOwinMiddleware>();
// Register the built-in event handlers used by the OpenIddict OWIN client components.

55
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<AuthenticationOptions>
{
private readonly IOpenIddictClientDispatcher _dispatcher;
private readonly IOpenIddictClientFactory _factory;
private readonly IOptionsMonitor<OpenIddictClientOwinOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientOwinHandler"/> class.
/// </summary>
/// <param name="dispatcher">The OpenIddict client dispatcher used by this instance.</param>
/// <param name="factory">The OpenIddict client factory used by this instance.</param>
/// <param name="options">The OpenIddict client OWIN options.</param>
public OpenIddictClientOwinHandler(
IOpenIddictClientDispatcher dispatcher,
IOpenIddictClientFactory factory,
IOptionsMonitor<OpenIddictClientOwinOptions> options)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <param name="provider">The service provider.</param>
public OpenIddictClientOwinHandler(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
var dispatcher = _provider.GetRequiredService<IOpenIddictClientDispatcher>();
// 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<Authenti
var transaction = Context.Get<OpenIddictClientTransaction>(typeof(OpenIddictClientTransaction).FullName);
if (transaction is null)
{
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
// 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<IOwinRequest>(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<Authenti
}
var context = new ProcessRequestContext(transaction);
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
// Store the context in the transaction so that it can be retrieved from InvokeAsync().
transaction.SetProperty(typeof(ProcessRequestContext).FullName!, context);
@ -84,6 +83,8 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<Authenti
// in InitializeCoreAsync() to ensure the request context is available from AuthenticateCoreAsync() when
// active authentication is used, as AuthenticateCoreAsync() is always called before InvokeAsync() in this case.
var dispatcher = _provider.GetRequiredService<IOpenIddictClientDispatcher>();
var transaction = Context.Get<OpenIddictClientTransaction>(typeof(OpenIddictClientTransaction).FullName)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0315));
@ -110,7 +111,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
@ -131,6 +132,8 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<Authenti
/// <inheritdoc/>
protected override async Task<AuthenticationTicket?> AuthenticateCoreAsync()
{
var dispatcher = _provider.GetRequiredService<IOpenIddictClientDispatcher>();
var transaction = Context.Get<OpenIddictClientTransaction>(typeof(OpenIddictClientTransaction).FullName)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0315));
@ -140,7 +143,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<Authenti
var context = transaction.GetProperty<ProcessAuthenticationContext>(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<Authenti
// OpenIddictClientOwinMiddleware is assumed to be the only middleware allowed to write
// to the response stream when a response grant (sign-in/out or challenge) was applied.
var descriptions = _options.CurrentValue.ForwardedAuthenticationTypes;
var dispatcher = _provider.GetRequiredService<IOpenIddictClientDispatcher>();
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOwinOptions>>();
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<Authenti
Request = new OpenIddictRequest()
};
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
@ -325,7 +330,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{
@ -352,7 +357,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<Authenti
Request = new OpenIddictRequest()
};
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
@ -369,7 +374,7 @@ public sealed class OpenIddictClientOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{

13
src/OpenIddict.Client.Owin/OpenIddictClientOwinHelpers.cs

@ -37,17 +37,8 @@ public static class OpenIddictClientOwinHelpers
{
ArgumentNullException.ThrowIfNull(transaction);
if (!transaction.Properties.TryGetValue(typeof(IOwinRequest).FullName!, out object? property))
{
return null;
}
if (property is WeakReference<IOwinRequest> 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;
}
/// <summary>

10
src/OpenIddict.Client.Owin/OpenIddictClientOwinMiddleware.cs

@ -28,10 +28,12 @@ using AuthenticateDelegate = Func<
/// <summary>
/// Provides the entry point necessary to register the OpenIddict client handler in an OWIN pipeline.
/// </summary>
/// <remarks>
/// 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.
/// </summary>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientOwinMiddleware : AuthenticationMiddleware<AuthenticationOptions>
{
@ -54,8 +56,7 @@ public sealed class OpenIddictClientOwinMiddleware : AuthenticationMiddleware<Au
ArgumentNullException.ThrowIfNull(context);
// Resolve the list of forwarded authentication types from the options.
var options = _provider.GetService<IOptionsMonitor<OpenIddictClientOwinOptions>>()
?.CurrentValue ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0316));
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictClientOwinOptions>>().CurrentValue;
// Retrieve the existing authentication delegate.
var function = context.Get<AuthenticateDelegate?>("security.Authenticate");
@ -154,8 +155,7 @@ public sealed class OpenIddictClientOwinMiddleware : AuthenticationMiddleware<Au
/// </summary>
/// <returns>A new instance of the <see cref="OpenIddictClientOwinHandler"/> class.</returns>
protected override AuthenticationHandler<AuthenticationOptions> CreateHandler()
=> _provider.GetService<OpenIddictClientOwinHandler>()
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0317));
=> new OpenIddictClientOwinHandler(_provider);
/// <summary>
/// Provides the options used by the <see cref="OpenIddictClientOwinMiddleware"/> class.

12
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
// 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);

30
src/OpenIddict.Client/IOpenIddictClientFactory.cs

@ -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;
/// <summary>
/// Represents a service responsible for creating transactions.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IOpenIddictClientFactory
{
/// <summary>
/// Creates a new <see cref="OpenIddictClientTransaction"/> that is used as a
/// way to store per-request data needed to process the requested operation.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <remarks>
/// Note: the specified <see cref="CancellationToken"/> is automatically attached to the returned transaction.
/// </remarks>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous
/// operation, whose result returns the created transaction.
/// </returns>
ValueTask<OpenIddictClientTransaction> CreateTransactionAsync(CancellationToken cancellationToken);
}

89
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<OpenIddictClientDispatcher> _logger;
private readonly IOptionsMonitor<OpenIddictClientOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientDispatcher"/> class.
/// </summary>
public OpenIddictClientDispatcher(
ILogger<OpenIddictClientDispatcher> logger,
IOptionsMonitor<OpenIddictClientOptions> options,
IServiceProvider provider)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
/// <inheritdoc/>
public async ValueTask DispatchAsync<TContext>(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<TContext>
?? context.ServiceProvider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictClientHandler<TContext>
?? 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<IOpenIddictClientHandler<TContext>> 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<TContext> handler } => handler,
_ when _provider.GetService(descriptor.ServiceDescriptor.ServiceType)
is IOpenIddictClientHandler<TContext> handler => handler,
_ => throw new InvalidOperationException(SR.FormatID0312(descriptor.ServiceDescriptor.ServiceType))
};
}
}
async ValueTask<bool> 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<TContext> filter)
{
throw new InvalidOperationException(SR.FormatID0099(descriptor.FilterTypes[index]));
}
var filter = context.ServiceProvider.GetService(type) as IOpenIddictClientHandlerFilter<TContext>
?? throw new InvalidOperationException(SR.FormatID0099(type));
if (!await filter.IsActiveAsync(context))
{

10
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
/// <summary>
/// Gets the logger responsible for logging processed operations.
/// </summary>
public ILogger Logger => Transaction.Logger;
public ILogger Logger
=> field ??= Transaction.ServiceProvider.GetRequiredService<ILogger<OpenIddictClientDispatcher>>();
/// <summary>
/// Gets the OpenIddict client options.
@ -90,6 +92,12 @@ public static partial class OpenIddictClientEvents
get => Transaction.Registration;
set => Transaction.Registration = value;
}
/// <summary>
/// Gets the service provider associated with the current transaction.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public IServiceProvider ServiceProvider => Transaction.ServiceProvider;
}
/// <summary>

3
src/OpenIddict.Client/OpenIddictClientExtensions.cs

@ -28,8 +28,7 @@ public static class OpenIddictClientExtensions
builder.Services.AddLogging();
builder.Services.AddOptions();
builder.Services.TryAddScoped<IOpenIddictClientDispatcher, OpenIddictClientDispatcher>();
builder.Services.TryAddScoped<IOpenIddictClientFactory, OpenIddictClientFactory>();
builder.Services.TryAddSingleton<IOpenIddictClientDispatcher, OpenIddictClientDispatcher>();
builder.Services.TryAddSingleton<OpenIddictClientService>();
// Register the built-in filters used by the default OpenIddict client event handlers.

48
src/OpenIddict.Client/OpenIddictClientFactory.cs

@ -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;
/// <summary>
/// Represents a service responsible for creating transactions.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientFactory : IOpenIddictClientFactory
{
private readonly ILogger<OpenIddictClientDispatcher> _logger;
private readonly IOptionsMonitor<OpenIddictClientOptions> _options;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientFactory"/> class.
/// </summary>
public OpenIddictClientFactory(
ILogger<OpenIddictClientDispatcher> logger,
IOptionsMonitor<OpenIddictClientOptions> options)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <inheritdoc/>
public ValueTask<OpenIddictClientTransaction> CreateTransactionAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return new(Task.FromCanceled<OpenIddictClientTransaction>(cancellationToken));
}
return new(new OpenIddictClientTransaction
{
CancellationToken = cancellationToken,
Logger = _logger,
Options = _options.CurrentValue
});
}
}

254
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(request.CancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(request.CancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(request.CancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(request.CancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(request.CancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(request.CancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(request.CancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictClientOptions>>();
var transaction = new OpenIddictClientTransaction
{
CancellationToken = cancellationToken,
Options = options.CurrentValue,
ServiceProvider = scope.ServiceProvider
};
request = await PrepareUserInfoRequestAsync();
request = await ApplyUserInfoRequestAsync();

49
src/OpenIddict.Client/OpenIddictClientTransaction.cs

@ -5,23 +5,22 @@
*/
using System.ComponentModel;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Client;
/// <summary>
/// Represents the context associated with an OpenID Connect client request.
/// Represents the context associated with an OpenID Connect client operation.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientTransaction
{
/// <summary>
/// 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.
/// </summary>
public CancellationToken CancellationToken { get; set; }
public required CancellationToken CancellationToken { get; init; }
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.
/// Gets or sets the type of the endpoint processing the current transaction.
/// </summary>
public OpenIddictClientEndpointType EndpointType { get; set; }
@ -36,29 +35,36 @@ public sealed class OpenIddictClientTransaction
public Uri? BaseUri { get; set; }
/// <summary>
/// Gets or sets the logger associated with the current request.
/// Gets the options associated with the current transaction.
/// </summary>
public ILogger Logger { get; set; } = default!;
public required OpenIddictClientOptions Options
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
/// <summary>
/// Gets or sets the options associated with the current request.
/// </summary>
public OpenIddictClientOptions Options { get; set; } = default!;
/// <summary>
/// Gets the additional properties associated with the current request.
/// Gets the additional properties associated with the current transaction.
/// </summary>
public Dictionary<string, object?> Properties { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets the client registration used for the current request.
/// Gets or sets the client registration used for the current transaction.
/// </summary>
public OpenIddictClientRegistration Registration { get; set; } = default!;
public OpenIddictClientRegistration Registration
{
get;
set { ArgumentNullException.ThrowIfNull(value); field = value; }
} = default!;
/// <summary>
/// Gets or sets the server configuration used for the current request.
/// Gets or sets the server configuration used for the current transaction.
/// </summary>
public OpenIddictConfiguration Configuration { get; set; } = default!;
public OpenIddictConfiguration Configuration
{
get;
set { ArgumentNullException.ThrowIfNull(value); field = value; }
} = default!;
/// <summary>
/// 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.
/// </summary>
public OpenIddictResponse? Response { get; set; }
/// <summary>
/// Gets the service provider used to resolve services.
/// </summary>
public required IServiceProvider ServiceProvider
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
}

11
src/OpenIddict.Server.AspNetCore/OpenIddictServerAspNetCoreFeature.cs

@ -9,14 +9,17 @@ using System.ComponentModel;
namespace OpenIddict.Server.AspNetCore;
/// <summary>
/// Exposes the current server transaction to the ASP.NET Core host.
/// Exposes the current server transaction to the ASP.NET Core application.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictServerAspNetCoreFeature
{
/// <summary>
/// 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.
/// </summary>
public OpenIddictServerTransaction? Transaction { get; set; }
public required OpenIddictServerTransaction Transaction
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
}

19
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<Au
IAuthenticationSignOutHandler
{
private readonly IOpenIddictServerDispatcher _dispatcher;
private readonly IOpenIddictServerFactory _factory;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerAspNetCoreHandler"/> class.
/// </summary>
public OpenIddictServerAspNetCoreHandler(
IOpenIddictServerDispatcher dispatcher,
IOpenIddictServerFactory factory,
IOptionsMonitor<AuthenticationSchemeOptions> 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));
/// <inheritdoc/>
public async Task<bool> HandleRequestAsync()
@ -59,9 +55,16 @@ public sealed class OpenIddictServerAspNetCoreHandler : AuthenticationHandler<Au
var transaction = Context.Features.Get<OpenIddictServerAspNetCoreFeature>()?.Transaction;
if (transaction is null)
{
var options = Context.RequestServices.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>();
// 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<HttpRequest>(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.

16
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<HttpRequest> reference && reference.TryGetTarget(out HttpRequest? request))
{
return request;
}
return null;
}
/// <summary>
@ -52,7 +44,7 @@ public static class OpenIddictServerAspNetCoreHelpers
/// Retrieves the <see cref="OpenIddictRequest"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictRequest"/> instance or <c>null</c> if it couldn't be found.</returns>
/// <returns>The <see cref="OpenIddictRequest"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static OpenIddictRequest? GetOpenIddictServerRequest(this HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
@ -64,7 +56,7 @@ public static class OpenIddictServerAspNetCoreHelpers
/// Retrieves the <see cref="OpenIddictResponse"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictResponse"/> instance or <c>null</c> if it couldn't be found.</returns>
/// <returns>The <see cref="OpenIddictResponse"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static OpenIddictResponse? GetOpenIddictServerResponse(this HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);

1
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<OpenIddictServerOwinHandler>();
builder.Services.TryAddScoped<OpenIddictServerOwinMiddleware>();
// Register the built-in event handlers used by the OpenIddict OWIN server components.

54
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<AuthenticationOptions>
{
private readonly IOpenIddictServerDispatcher _dispatcher;
private readonly IOpenIddictServerFactory _factory;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerOwinHandler"/> class.
/// </summary>
/// <param name="dispatcher">The OpenIddict server dispatcher used by this instance.</param>
/// <param name="factory">The OpenIddict server factory used by this instance.</param>
public OpenIddictServerOwinHandler(
IOpenIddictServerDispatcher dispatcher,
IOpenIddictServerFactory factory)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
/// <param name="provider">The service provider.</param>
public OpenIddictServerOwinHandler(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
var dispatcher = _provider.GetRequiredService<IOpenIddictServerDispatcher>();
// 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<Authenti
var transaction = Context.Get<OpenIddictServerTransaction>(typeof(OpenIddictServerTransaction).FullName);
if (transaction is null)
{
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictServerOptions>>();
// 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<IOwinRequest>(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<Authenti
}
var context = new ProcessRequestContext(transaction);
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
// Store the context in the transaction so that it can be retrieved from InvokeAsync().
transaction.SetProperty(typeof(ProcessRequestContext).FullName!, context);
@ -75,6 +79,8 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
// in InitializeCoreAsync() to ensure the request context is available from AuthenticateCoreAsync() when
// active authentication is used, as AuthenticateCoreAsync() is always called before InvokeAsync() in this case.
var dispatcher = _provider.GetRequiredService<IOpenIddictServerDispatcher>();
var transaction = Context.Get<OpenIddictServerTransaction>(typeof(OpenIddictServerTransaction).FullName)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0112));
@ -101,7 +107,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
@ -122,6 +128,8 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
/// <inheritdoc/>
protected override async Task<AuthenticationTicket?> AuthenticateCoreAsync()
{
var dispatcher = _provider.GetRequiredService<IOpenIddictServerDispatcher>();
var transaction = Context.Get<OpenIddictServerTransaction>(typeof(OpenIddictServerTransaction).FullName)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0112));
@ -131,7 +139,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
var context = transaction.GetProperty<ProcessAuthenticationContext>(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<Authenti
// OpenIddictServerOwinMiddleware is assumed to be the only middleware allowed to write
// to the response stream when a response grant (sign-in/out or challenge) was applied.
var dispatcher = _provider.GetRequiredService<IOpenIddictServerDispatcher>();
// 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<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
@ -312,7 +322,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{
@ -337,7 +347,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
@ -354,7 +364,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{
@ -378,7 +388,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
@ -395,7 +405,7 @@ public sealed class OpenIddictServerOwinHandler : AuthenticationHandler<Authenti
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{

13
src/OpenIddict.Server.Owin/OpenIddictServerOwinHelpers.cs

@ -37,17 +37,8 @@ public static class OpenIddictServerOwinHelpers
{
ArgumentNullException.ThrowIfNull(transaction);
if (!transaction.Properties.TryGetValue(typeof(IOwinRequest).FullName!, out object? property))
{
return null;
}
if (property is WeakReference<IOwinRequest> 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;
}
/// <summary>

8
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;
/// <summary>
/// Provides the entry point necessary to register the OpenIddict server handler in an OWIN pipeline.
/// </summary>
/// <remarks>
/// 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.
/// </summary>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictServerOwinMiddleware : AuthenticationMiddleware<AuthenticationOptions>
{
@ -37,8 +38,7 @@ public sealed class OpenIddictServerOwinMiddleware : AuthenticationMiddleware<Au
/// </summary>
/// <returns>A new instance of the <see cref="OpenIddictServerOwinHandler"/> class.</returns>
protected override AuthenticationHandler<AuthenticationOptions> CreateHandler()
=> _provider.GetService<OpenIddictServerOwinHandler>()
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0122));
=> new OpenIddictServerOwinHandler(_provider);
/// <summary>
/// Provides the options used by the <see cref="OpenIddictServerOwinMiddleware"/> class.

30
src/OpenIddict.Server/IOpenIddictServerFactory.cs

@ -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;
/// <summary>
/// Represents a service responsible for creating transactions.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IOpenIddictServerFactory
{
/// <summary>
/// Creates a new <see cref="OpenIddictServerTransaction"/> that is used as a
/// way to store per-request data needed to process the requested operation.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <remarks>
/// Note: the specified <see cref="CancellationToken"/> is automatically attached to the returned transaction.
/// </remarks>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous
/// operation, whose result returns the created transaction.
/// </returns>
ValueTask<OpenIddictServerTransaction> CreateTransactionAsync(CancellationToken cancellationToken);
}

89
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<OpenIddictServerDispatcher> _logger;
private readonly IOptionsMonitor<OpenIddictServerOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerDispatcher"/> class.
/// </summary>
public OpenIddictServerDispatcher(
ILogger<OpenIddictServerDispatcher> logger,
IOptionsMonitor<OpenIddictServerOptions> options,
IServiceProvider provider)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
/// <inheritdoc/>
public async ValueTask DispatchAsync<TContext>(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<TContext>
?? context.ServiceProvider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictServerHandler<TContext>
?? 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<IOpenIddictServerHandler<TContext>> 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<TContext> handler } => handler,
_ when _provider.GetService(descriptor.ServiceDescriptor.ServiceType)
is IOpenIddictServerHandler<TContext> handler => handler,
_ => throw new InvalidOperationException(SR.FormatID0098(descriptor.ServiceDescriptor.ServiceType))
};
}
}
async ValueTask<bool> 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<TContext> filter)
{
throw new InvalidOperationException(SR.FormatID0099(descriptor.FilterTypes[index]));
}
var filter = context.ServiceProvider.GetService(type) as IOpenIddictServerHandlerFilter<TContext>
?? throw new InvalidOperationException(SR.FormatID0099(type));
if (!await filter.IsActiveAsync(context))
{

10
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
/// <summary>
/// Gets the logger responsible for logging processed operations.
/// </summary>
public ILogger Logger => Transaction.Logger;
public ILogger Logger
=> field ??= Transaction.ServiceProvider.GetRequiredService<ILogger<OpenIddictServerDispatcher>>();
/// <summary>
/// Gets the OpenIddict server options.
/// </summary>
public OpenIddictServerOptions Options => Transaction.Options;
/// <summary>
/// Gets the service provider associated with the current transaction.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public IServiceProvider ServiceProvider => Transaction.ServiceProvider;
}
/// <summary>

3
src/OpenIddict.Server/OpenIddictServerExtensions.cs

@ -29,8 +29,7 @@ public static class OpenIddictServerExtensions
builder.Services.AddLogging();
builder.Services.AddOptions();
builder.Services.TryAddScoped<IOpenIddictServerDispatcher, OpenIddictServerDispatcher>();
builder.Services.TryAddScoped<IOpenIddictServerFactory, OpenIddictServerFactory>();
builder.Services.TryAddSingleton<IOpenIddictServerDispatcher, OpenIddictServerDispatcher>();
// 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.

48
src/OpenIddict.Server/OpenIddictServerFactory.cs

@ -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;
/// <summary>
/// Represents a service responsible for creating transactions.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictServerFactory : IOpenIddictServerFactory
{
private readonly ILogger _logger;
private readonly IOptionsMonitor<OpenIddictServerOptions> _options;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictServerDispatcher"/> class.
/// </summary>
public OpenIddictServerFactory(
ILogger<OpenIddictServerDispatcher> logger,
IOptionsMonitor<OpenIddictServerOptions> options)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <inheritdoc/>
public ValueTask<OpenIddictServerTransaction> CreateTransactionAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return new(Task.FromCanceled<OpenIddictServerTransaction>(cancellationToken));
}
return new(new OpenIddictServerTransaction
{
CancellationToken = cancellationToken,
Logger = _logger,
Options = _options.CurrentValue
});
}
}

33
src/OpenIddict.Server/OpenIddictServerTransaction.cs

@ -6,20 +6,19 @@
using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Server;
/// <summary>
/// Represents the context associated with an OpenID Connect server request.
/// Represents the context associated with an OpenID Connect server operation.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictServerTransaction
{
/// <summary>
/// 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.
/// </summary>
public CancellationToken CancellationToken { get; set; }
public required CancellationToken CancellationToken { get; init; }
/// <summary>
/// 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; }
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.
/// Gets or sets the type of the endpoint processing the current transaction.
/// </summary>
public OpenIddictServerEndpointType EndpointType { get; set; }
@ -42,17 +41,16 @@ public sealed class OpenIddictServerTransaction
public Uri? BaseUri { get; set; }
/// <summary>
/// Gets or sets the logger associated with the current request.
/// Gets the options associated with the current transaction.
/// </summary>
public ILogger Logger { get; set; } = default!;
public required OpenIddictServerOptions Options
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
/// <summary>
/// Gets or sets the options associated with the current request.
/// </summary>
public OpenIddictServerOptions Options { get; set; } = default!;
/// <summary>
/// Gets the additional properties associated with the current request.
/// Gets the additional properties associated with the current transaction.
/// </summary>
public Dictionary<string, object?> Properties { get; } = new(StringComparer.OrdinalIgnoreCase);
@ -65,4 +63,13 @@ public sealed class OpenIddictServerTransaction
/// Gets or sets the current OpenID Connect response being returned.
/// </summary>
public OpenIddictResponse? Response { get; set; }
/// <summary>
/// Gets the service provider used to resolve services.
/// </summary>
public required IServiceProvider ServiceProvider
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
}

11
src/OpenIddict.Validation.AspNetCore/OpenIddictValidationAspNetCoreFeature.cs

@ -9,14 +9,17 @@ using System.ComponentModel;
namespace OpenIddict.Validation.AspNetCore;
/// <summary>
/// Exposes the current validation transaction to the ASP.NET Core host.
/// Exposes the current validation transaction to the ASP.NET Core application.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictValidationAspNetCoreFeature
{
/// <summary>
/// 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.
/// </summary>
public OpenIddictValidationTransaction? Transaction { get; set; }
public required OpenIddictValidationTransaction Transaction
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
}

19
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;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationAspNetCoreHandler"/> class.
/// </summary>
public OpenIddictValidationAspNetCoreHandler(
IOpenIddictValidationDispatcher dispatcher,
IOpenIddictValidationFactory factory,
IOptionsMonitor<AuthenticationSchemeOptions> 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));
/// <inheritdoc/>
public async Task<bool> HandleRequestAsync()
@ -57,9 +53,16 @@ public sealed class OpenIddictValidationAspNetCoreHandler : AuthenticationHandle
var transaction = Context.Features.Get<OpenIddictValidationAspNetCoreFeature>()?.Transaction;
if (transaction is null)
{
var options = Context.RequestServices.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
// 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<HttpRequest>(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.

17
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<HttpRequest> 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;
}
/// <summary>
@ -52,7 +43,7 @@ public static class OpenIddictValidationAspNetCoreHelpers
/// Retrieves the <see cref="OpenIddictRequest"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictRequest"/> instance or <c>null</c> if it couldn't be found.</returns>
/// <returns>The <see cref="OpenIddictRequest"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static OpenIddictRequest? GetOpenIddictValidationRequest(this HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);
@ -64,7 +55,7 @@ public static class OpenIddictValidationAspNetCoreHelpers
/// Retrieves the <see cref="OpenIddictResponse"/> instance stored in <see cref="BaseContext"/>.
/// </summary>
/// <param name="context">The context instance.</param>
/// <returns>The <see cref="OpenIddictResponse"/> instance or <c>null</c> if it couldn't be found.</returns>
/// <returns>The <see cref="OpenIddictResponse"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static OpenIddictResponse? GetOpenIddictValidationResponse(this HttpContext context)
{
ArgumentNullException.ThrowIfNull(context);

1
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<OpenIddictValidationOwinHandler>();
builder.Services.TryAddScoped<OpenIddictValidationOwinMiddleware>();
// Register the built-in event handlers used by the OpenIddict OWIN validation components.

46
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<AuthenticationOptions>
{
private readonly IOpenIddictValidationDispatcher _dispatcher;
private readonly IOpenIddictValidationFactory _factory;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationOwinHandler"/> class.
/// </summary>
/// <param name="dispatcher">The OpenIddict validation provider used by this instance.</param>
/// <param name="factory">The OpenIddict validation factory used by this instance.</param>
public OpenIddictValidationOwinHandler(
IOpenIddictValidationDispatcher dispatcher,
IOpenIddictValidationFactory factory)
{
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
}
/// <param name="provider">The service provider.</param>
public OpenIddictValidationOwinHandler(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <inheritdoc/>
protected override async Task InitializeCoreAsync()
{
var dispatcher = _provider.GetRequiredService<IOpenIddictValidationDispatcher>();
// 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<Auth
var transaction = Context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName);
if (transaction is null)
{
var options = _provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
// 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<IOwinRequest>(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<Auth
}
var context = new ProcessRequestContext(transaction);
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
// Store the context in the transaction so that it can be retrieved from InvokeAsync().
transaction.SetProperty(typeof(ProcessRequestContext).FullName!, context);
@ -75,6 +79,8 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Auth
// in InitializeCoreAsync() to ensure the request context is available from AuthenticateCoreAsync() when
// active authentication is used, as AuthenticateCoreAsync() is always called before InvokeAsync() in this case.
var dispatcher = _provider.GetRequiredService<IOpenIddictValidationDispatcher>();
var transaction = Context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0166));
@ -101,7 +107,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Auth
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled)
{
@ -122,6 +128,8 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Auth
/// <inheritdoc/>
protected override async Task<AuthenticationTicket?> AuthenticateCoreAsync()
{
var dispatcher = _provider.GetRequiredService<IOpenIddictValidationDispatcher>();
var transaction = Context.Get<OpenIddictValidationTransaction>(typeof(OpenIddictValidationTransaction).FullName)
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0166));
@ -131,7 +139,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Auth
var context = transaction.GetProperty<ProcessAuthenticationContext>(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<Auth
// OpenIddictValidationOwinMiddleware is assumed to be the only middleware allowed to write
// to the response stream when a response grant (sign-in/out or challenge) was applied.
var dispatcher = _provider.GetRequiredService<IOpenIddictValidationDispatcher>();
// 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<Auth
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(context);
await dispatcher.DispatchAsync(context);
if (context.IsRequestHandled || context.IsRequestSkipped)
{
@ -244,7 +254,7 @@ public sealed class OpenIddictValidationOwinHandler : AuthenticationHandler<Auth
Response = new OpenIddictResponse()
};
await _dispatcher.DispatchAsync(notification);
await dispatcher.DispatchAsync(notification);
if (notification.IsRequestHandled || context.IsRequestSkipped)
{

13
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinHelpers.cs

@ -37,17 +37,8 @@ public static class OpenIddictValidationOwinHelpers
{
ArgumentNullException.ThrowIfNull(transaction);
if (!transaction.Properties.TryGetValue(typeof(IOwinRequest).FullName!, out object? property))
{
return null;
}
if (property is WeakReference<IOwinRequest> 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;
}
/// <summary>

12
src/OpenIddict.Validation.Owin/OpenIddictValidationOwinMiddleware.cs

@ -13,10 +13,12 @@ namespace OpenIddict.Validation.Owin;
/// <summary>
/// Provides the entry point necessary to register the OpenIddict validation handler in an OWIN pipeline.
/// </summary>
/// <remarks>
/// 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.
/// </summary>
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictValidationOwinMiddleware : AuthenticationMiddleware<AuthenticationOptions>
{
@ -32,9 +34,8 @@ public sealed class OpenIddictValidationOwinMiddleware : AuthenticationMiddlewar
IServiceProvider provider)
: base(next, new InternalOptions()
{
AuthenticationMode = provider.GetService<IOptionsMonitor<OpenIddictValidationOwinOptions>>()
?.CurrentValue.AuthenticationMode
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0169))
AuthenticationMode = provider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOwinOptions>>()
.CurrentValue.AuthenticationMode
})
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
@ -43,8 +44,7 @@ public sealed class OpenIddictValidationOwinMiddleware : AuthenticationMiddlewar
/// </summary>
/// <returns>A new instance of the <see cref="OpenIddictValidationOwinHandler"/> class.</returns>
protected override AuthenticationHandler<AuthenticationOptions> CreateHandler()
=> _provider.GetService<OpenIddictValidationOwinHandler>()
?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0169));
=> new OpenIddictValidationOwinHandler(_provider);
/// <summary>
/// Provides the options used by the <see cref="OpenIddictValidationOwinMiddleware"/> class.

30
src/OpenIddict.Validation/IOpenIddictValidationFactory.cs

@ -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;
/// <summary>
/// Represents a service responsible for creating transactions.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IOpenIddictValidationFactory
{
/// <summary>
/// Creates a new <see cref="OpenIddictValidationTransaction"/> that is used as a
/// way to store per-request data needed to process the requested operation.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <remarks>
/// Note: the specified <see cref="CancellationToken"/> is automatically attached to the returned transaction.
/// </remarks>
/// <returns>
/// A <see cref="ValueTask"/> that can be used to monitor the asynchronous
/// operation, whose result returns the created transaction.
/// </returns>
ValueTask<OpenIddictValidationTransaction> CreateTransactionAsync(CancellationToken cancellationToken);
}

89
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<OpenIddictValidationDispatcher> _logger;
private readonly IOptionsMonitor<OpenIddictValidationOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationDispatcher"/> class.
/// </summary>
public OpenIddictValidationDispatcher(
ILogger<OpenIddictValidationDispatcher> logger,
IOptionsMonitor<OpenIddictValidationOptions> options,
IServiceProvider provider)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
/// <inheritdoc/>
public async ValueTask DispatchAsync<TContext>(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<TContext>
?? context.ServiceProvider.GetService(descriptor.ServiceDescriptor.ServiceType) as IOpenIddictValidationHandler<TContext>
?? 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<IOpenIddictValidationHandler<TContext>> 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<TContext> handler } => handler,
_ when _provider.GetService(descriptor.ServiceDescriptor.ServiceType)
is IOpenIddictValidationHandler<TContext> handler => handler,
_ => throw new InvalidOperationException(SR.FormatID0138(descriptor.ServiceDescriptor.ServiceType))
};
}
}
async ValueTask<bool> 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<TContext> filter)
{
throw new InvalidOperationException(SR.FormatID0099(descriptor.FilterTypes[index]));
}
var filter = context.ServiceProvider.GetService(type) as IOpenIddictValidationHandlerFilter<TContext>
?? throw new InvalidOperationException(SR.FormatID0099(type));
if (!await filter.IsActiveAsync(context))
{

10
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
/// <summary>
/// Gets the logger responsible for logging processed operations.
/// </summary>
public ILogger Logger => Transaction.Logger;
public ILogger Logger
=> field ??= Transaction.ServiceProvider.GetRequiredService<ILogger<OpenIddictValidationDispatcher>>();
/// <summary>
/// Gets the OpenIddict validation options.
/// </summary>
public OpenIddictValidationOptions Options => Transaction.Options;
/// <summary>
/// Gets the service provider associated with the current transaction.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public IServiceProvider ServiceProvider => Transaction.ServiceProvider;
}
/// <summary>

3
src/OpenIddict.Validation/OpenIddictValidationExtensions.cs

@ -30,8 +30,7 @@ public static class OpenIddictValidationExtensions
builder.Services.AddOptions();
builder.Services.TryAddSingleton<OpenIddictValidationService>();
builder.Services.TryAddScoped<IOpenIddictValidationDispatcher, OpenIddictValidationDispatcher>();
builder.Services.TryAddScoped<IOpenIddictValidationFactory, OpenIddictValidationFactory>();
builder.Services.TryAddSingleton<IOpenIddictValidationDispatcher, OpenIddictValidationDispatcher>();
// 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.

48
src/OpenIddict.Validation/OpenIddictValidationFactory.cs

@ -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;
/// <summary>
/// Represents a service responsible for creating transactions.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictValidationFactory : IOpenIddictValidationFactory
{
private readonly ILogger<OpenIddictValidationDispatcher> _logger;
private readonly IOptionsMonitor<OpenIddictValidationOptions> _options;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictValidationFactory"/> class.
/// </summary>
public OpenIddictValidationFactory(
ILogger<OpenIddictValidationDispatcher> logger,
IOptionsMonitor<OpenIddictValidationOptions> options)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
}
/// <inheritdoc/>
public ValueTask<OpenIddictValidationTransaction> CreateTransactionAsync(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
return new(Task.FromCanceled<OpenIddictValidationTransaction>(cancellationToken));
}
return new(new OpenIddictValidationTransaction
{
CancellationToken = cancellationToken,
Logger = _logger,
Options = _options.CurrentValue
});
}
}

53
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<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
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<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
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<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
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<IOpenIddictValidationDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictValidationFactory>();
var transaction = await factory.CreateTransactionAsync(cancellationToken);
var options = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<OpenIddictValidationOptions>>();
var transaction = new OpenIddictValidationTransaction
{
CancellationToken = cancellationToken,
Options = options.CurrentValue,
ServiceProvider = _provider
};
request = await PrepareIntrospectionRequestAsync();
request = await ApplyIntrospectionRequestAsync();

41
src/OpenIddict.Validation/OpenIddictValidationTransaction.cs

@ -6,20 +6,19 @@
using System.ComponentModel;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Extensions.Logging;
namespace OpenIddict.Validation;
/// <summary>
/// Represents the context associated with an OpenID Connect validation request.
/// Represents the context associated with an OpenID Connect validation operation.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictValidationTransaction
{
/// <summary>
/// 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.
/// </summary>
public CancellationToken CancellationToken { get; set; }
public required CancellationToken CancellationToken { get; init; }
/// <summary>
/// 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; }
/// <summary>
/// Gets or sets the type of the endpoint processing the current request.
/// Gets or sets the type of the endpoint processing the current transaction.
/// </summary>
public OpenIddictValidationEndpointType EndpointType { get; set; }
@ -42,24 +41,27 @@ public sealed class OpenIddictValidationTransaction
public Uri? BaseUri { get; set; }
/// <summary>
/// Gets or sets the logger associated with the current request.
/// Gets the options associated with the current transaction.
/// </summary>
public ILogger Logger { get; set; } = default!;
public required OpenIddictValidationOptions Options
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
/// <summary>
/// Gets or sets the options associated with the current request.
/// </summary>
public OpenIddictValidationOptions Options { get; set; } = default!;
/// <summary>
/// Gets the additional properties associated with the current request.
/// Gets the additional properties associated with the current transaction.
/// </summary>
public Dictionary<string, object?> Properties { get; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>
/// Gets or sets the server configuration used for the current request.
/// Gets or sets the server configuration used for the current transaction.
/// </summary>
public OpenIddictConfiguration Configuration { get; set; } = default!;
public OpenIddictConfiguration Configuration
{
get;
set { ArgumentNullException.ThrowIfNull(value); field = value; }
} = default!;
/// <summary>
/// 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.
/// </summary>
public OpenIddictResponse? Response { get; set; }
/// <summary>
/// Gets the service provider used to resolve services.
/// </summary>
public required IServiceProvider ServiceProvider
{
get;
init { ArgumentNullException.ThrowIfNull(value); field = value; }
}
}

18
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<object[]> DefaultHandlers

Loading…
Cancel
Save