From b054d4e4a63eeab8767321a5f9fcdfe5af425e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 8 Feb 2023 03:30:27 +0100 Subject: [PATCH] Centralize protocol activations in a new service and add a runtime check to ensure the Windows integration is not used on unsupported platforms --- .../OpenIddictResources.resx | 5 +- .../OpenIddictClientWindowsActivation.cs | 26 ++- .../OpenIddictClientWindowsExtensions.cs | 13 +- .../OpenIddictClientWindowsHandler.cs | 95 +++++++++++ .../OpenIddictClientWindowsHandlers.cs | 7 +- .../OpenIddictClientWindowsListener.cs | 79 ++------- .../OpenIddictClientWindowsService.cs | 156 ++++++------------ 7 files changed, 197 insertions(+), 184 deletions(-) create mode 100644 src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 3b121383..073a7aba 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1424,7 +1424,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId An error occurred while authenticating the user. - The Windows protocol activation cannot be resolved from the client transaction or contained invalid data. + The Windows protocol activation cannot be resolved from the client transaction. The identifier of the application instance that initiated the authentication process cannot be resolved from the state token. @@ -1465,6 +1465,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId The payload extracted from the inter-process notification is malformed, incomplete or was created by a different version of the OpenIddict client library. + + The OpenIddict client Windows integration is not supported on this platform. + The security token is missing. diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs index ccc5d426..920e0478 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs @@ -9,15 +9,35 @@ using System.ComponentModel; namespace OpenIddict.Client.Windows; /// -/// Represents a Windows application activation. +/// Represents a Windows protocol activation. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientWindowsActivation { /// - /// Gets or sets the activation URI used to activate the application. + /// Creates a new instance of the class. /// - public Uri? ActivationUri { get; set; } + /// The protocol activation URI. + /// is . + public OpenIddictClientWindowsActivation(Uri uri) + { + if (uri is null) + { + throw new ArgumentNullException(nameof(uri)); + } + + if (!uri.IsAbsoluteUri || !uri.IsWellFormedOriginalString()) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0144), nameof(uri)); + } + + ActivationUri = uri; + } + + /// + /// Gets the protocol activation URI. + /// + public Uri ActivationUri { get; } /// /// Gets or sets a boolean indicating whether the activation diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs index 677b22e1..bcf60bfa 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs @@ -4,6 +4,7 @@ * the license and the contributors participating to this project. */ +using System.Runtime.InteropServices; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; @@ -30,19 +31,25 @@ public static class OpenIddictClientWindowsExtensions throw new ArgumentNullException(nameof(builder)); } + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); + } + // Note: the OpenIddict IHostedService implementation is deliberately registered as early as possible to // ensure protocol activations can be handled before another service can stop the initialization of the // application (e.g Dapplo.Microsoft.Extensions.Hosting.AppServices relies on an IHostedService to implement // single instantiation, which would prevent the OpenIddict service from handling the protocol activation // if the OpenIddict IHostedService implementation was not registered before the Dapplo IHostedService). if (!builder.Services.Any(static descriptor => descriptor.ServiceType == typeof(IHostedService) && - descriptor.ImplementationType == typeof(OpenIddictClientWindowsService))) + descriptor.ImplementationType == typeof(OpenIddictClientWindowsHandler))) { - builder.Services.Insert(0, ServiceDescriptor.Singleton()); + builder.Services.Insert(0, ServiceDescriptor.Singleton()); } - // Register the marshal responsible for managing authentication operations. + // Register the services responsible for coordinating and managing authentication operations. builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); // Register the built-in filters used by the default OpenIddict Windows client event handlers. builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs new file mode 100644 index 00000000..427aad67 --- /dev/null +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs @@ -0,0 +1,95 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; + +namespace OpenIddict.Client.Windows; + +/// +/// Contains the logic necessary to handle initial URI protocol activations. +/// +/// +/// Note: redirected URI protocol activations are handled by . +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class OpenIddictClientWindowsHandler : IHostedService +{ + private readonly IOptionsMonitor _options; + private readonly OpenIddictClientWindowsService _service; + + /// + /// Creates a new instance of the class. + /// + /// The OpenIddict client Windows integration options. + /// The OpenIddict client Windows service. + public OpenIddictClientWindowsHandler( + IOptionsMonitor options, + OpenIddictClientWindowsService service) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _service = service ?? throw new ArgumentNullException(nameof(service)); + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + // Note: initial URI protocol activation handling is implemented as a regular IHostedService + // rather than as a BackgroundService to allow blocking the initialization of the host until + // the activation is fully processed by the OpenIddict pipeline. By doing that, the UI thread + // is not started until redirection requests (like authorization responses) are fully processed, + // which allows handling these requests transparently and helps avoid the "flashing window effect": + // once a request has been handled by the OpenIddict pipeline, a dedicated handler is responsible + // for stopping the application gracefully using the IHostApplicationLifetime.StopApplication() API. + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + // If the default activation processing logic was disabled in the options, ignore the activation. + if (_options.CurrentValue.DisableProtocolActivationProcessing) + { + return Task.CompletedTask; + } + + // Determine whether the current instance is initialized to react to a protocol activation. + // If it's not, return immediately to avoid adding latency to the application startup process. + if (GetProtocolActivation() is not OpenIddictClientWindowsActivation activation) + { + return Task.CompletedTask; + } + + return _service.HandleProtocolActivationAsync(activation, cancellationToken); + + [MethodImpl(MethodImplOptions.NoInlining)] + static OpenIddictClientWindowsActivation? GetProtocolActivation() + { +#if SUPPORTS_WINDOWS_RUNTIME + // On platforms that support WinRT, always favor the AppInstance.GetActivatedEventArgs() API. + if (OpenIddictClientWindowsHelpers.IsWindowsRuntimeSupported() && + OpenIddictClientWindowsHelpers.GetProtocolActivationUriWithWindowsRuntime() is Uri uri) + { + return new OpenIddictClientWindowsActivation(uri); + } +#endif + // Otherwise, try to extract the protocol activation from the command line arguments. + if (OpenIddictClientWindowsHelpers.GetProtocolActivationUriFromCommandLineArguments( + Environment.GetCommandLineArgs()) is Uri value) + { + return new OpenIddictClientWindowsActivation(value); + } + + return null; + } + } + + /// + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs index faa7c951..1b772467 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs @@ -90,8 +90,9 @@ public static partial class OpenIddictClientWindowsHandlers (context.BaseUri, context.RequestUri) = context.Transaction.GetWindowsActivation() switch { - { ActivationUri: Uri uri } when uri.IsAbsoluteUri - => (new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), uri), + { ActivationUri: Uri uri } => ( + BaseUri : new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), + RequestUri: uri), _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)) }; @@ -451,7 +452,7 @@ public static partial class OpenIddictClientWindowsHandlers // Write the protocol activation URI. writer.Write(context.Transaction.GetWindowsActivation() switch { - { ActivationUri: Uri uri } when uri.IsAbsoluteUri => uri.AbsoluteUri, + { ActivationUri: Uri uri } => uri.AbsoluteUri, _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)) }); diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs index 20bc68fa..9495c7c3 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs @@ -6,16 +6,10 @@ using System.ComponentModel; using System.IO.Pipes; -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -#if !SUPPORTS_HOST_APPLICATION_LIFETIME -using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime; -#endif - namespace OpenIddict.Client.Windows; /// @@ -23,23 +17,29 @@ namespace OpenIddict.Client.Windows; /// are redirected by other instances using inter-process communication. /// /// -/// Note: initial URI protocol activations are handled by . +/// Note: initial URI protocol activations are handled by . /// [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OpenIddictClientWindowsListener : BackgroundService { private readonly ILogger _logger; private readonly IOptionsMonitor _options; - private readonly IServiceProvider _provider; - + private readonly OpenIddictClientWindowsService _service; + + /// + /// Creates a new instance of the class. + /// + /// The logger. + /// The OpenIddict client Windows integration options. + /// The OpenIddict client Windows service. public OpenIddictClientWindowsListener( ILogger logger, IOptionsMonitor options, - IServiceProvider provider) + OpenIddictClientWindowsService service) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _options = options ?? throw new ArgumentNullException(nameof(options)); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + _service = service ?? throw new ArgumentNullException(nameof(service)); } /// @@ -81,7 +81,7 @@ public sealed class OpenIddictClientWindowsListener : BackgroundService await (reader.ReadInt32() switch { 0x01 when GetProtocolActivation(reader) is OpenIddictClientWindowsActivation activation - => HandleProtocolActivationAsync(_provider, activation, stoppingToken), + => _service.HandleProtocolActivationAsync(activation, stoppingToken), var value => throw new InvalidOperationException(SR.FormatID0387(value)) }); @@ -101,7 +101,7 @@ public sealed class OpenIddictClientWindowsListener : BackgroundService while (!stoppingToken.IsCancellationRequested); - static OpenIddictClientWindowsActivation? GetProtocolActivation(BinaryReader reader) + static OpenIddictClientWindowsActivation GetProtocolActivation(BinaryReader reader) { // Ensure the binary serialization format is supported. var version = reader.ReadInt32(); @@ -116,61 +116,10 @@ public sealed class OpenIddictClientWindowsListener : BackgroundService throw new InvalidOperationException(SR.GetResourceString(SR.ID0388)); } - return new OpenIddictClientWindowsActivation + return new OpenIddictClientWindowsActivation(uri) { - ActivationUri = uri, IsActivationRedirected = true }; } - - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task HandleProtocolActivationAsync(IServiceProvider provider, - OpenIddictClientWindowsActivation activation, CancellationToken cancellationToken) - { - var scope = provider.CreateScope(); - - try - { - var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - - // Create a client transaction and store the protocol activation details so they can be - // retrieved by the Windows-specific client event handlers that need to access them. - var transaction = await factory.CreateTransactionAsync(); - transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!, activation); - - var context = new ProcessRequestContext(transaction) - { - CancellationToken = cancellationToken - }; - - await dispatcher.DispatchAsync(context); - - if (context.IsRejected) - { - await dispatcher.DispatchAsync(new ProcessErrorContext(transaction) - { - CancellationToken = cancellationToken, - Error = context.Error ?? Errors.InvalidRequest, - ErrorDescription = context.ErrorDescription, - ErrorUri = context.ErrorUri, - Response = new OpenIddictResponse() - }); - } - } - - finally - { - if (scope is IAsyncDisposable disposable) - { - await disposable.DisposeAsync(); - } - - else - { - scope.Dispose(); - } - } - } } } diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs index a605d5fb..9be0b337 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs @@ -5,150 +5,88 @@ */ using System.ComponentModel; -using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Options; namespace OpenIddict.Client.Windows; /// -/// Contains the logic necessary to handle initial URI protocol activations. +/// Contains the logic necessary to handle URI protocol activations (that +/// are typically resolved when launching the application or redirected +/// by other instances using inter-process communication). /// -/// -/// Note: redirected URI protocol activations are handled by . -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class OpenIddictClientWindowsService : IHostedService +public sealed class OpenIddictClientWindowsService { - private readonly IOptionsMonitor _options; private readonly IServiceProvider _provider; /// /// Creates a new instance of the class. /// - /// The OpenIddict client Windows integration options. /// The service provider. /// is . - public OpenIddictClientWindowsService( - IOptionsMonitor options, - IServiceProvider provider) - { - _options = options ?? throw new ArgumentNullException(nameof(options)); - _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - } + public OpenIddictClientWindowsService(IServiceProvider provider) + => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - /// - public Task StartAsync(CancellationToken cancellationToken) + /// + /// Handles the specified protocol activation. + /// + /// The protocol activation details. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// is . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public async Task HandleProtocolActivationAsync( + OpenIddictClientWindowsActivation activation, CancellationToken cancellationToken = default) { - // Note: initial URI protocol activation handling is implemented as a regular IHostedService - // rather than as a BackgroundService to allow blocking the initialization of the host until - // the activation is fully processed by the OpenIddict pipeline. By doing that, the UI thread - // is not started until redirection requests (like authorization responses) are fully processed, - // which allows handling these requests transparently and helps avoid the "flashing window effect": - // once a request has been handled by the OpenIddict pipeline, a dedicated handler is responsible - // for stopping the application gracefully using the IHostApplicationLifetime.StopApplication() API. - - if (cancellationToken.IsCancellationRequested) + if (activation is null) { - return Task.FromCanceled(cancellationToken); + throw new ArgumentNullException(nameof(activation)); } - // If the default activation processing logic was disabled in the options, ignore the activation. - if (_options.CurrentValue.DisableProtocolActivationProcessing) - { - return Task.CompletedTask; - } + cancellationToken.ThrowIfCancellationRequested(); - // Determine whether the current instance is initialized to react to a protocol activation. - // If it's not, return immediately to avoid adding latency to the application startup process. - if (GetProtocolActivation() is not OpenIddictClientWindowsActivation activation) + var scope = _provider.CreateScope(); + + try { - return Task.CompletedTask; - } + var dispatcher = scope.ServiceProvider.GetRequiredService(); + var factory = scope.ServiceProvider.GetRequiredService(); - return HandleProtocolActivationAsync(_provider, activation, cancellationToken); + // Create a client transaction and store the protocol activation details so they can be + // retrieved by the Windows-specific client event handlers that need to access them. + var transaction = await factory.CreateTransactionAsync(); + transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!, activation); - [MethodImpl(MethodImplOptions.NoInlining)] - static OpenIddictClientWindowsActivation? GetProtocolActivation() - { -#if SUPPORTS_WINDOWS_RUNTIME - // On platforms that support WinRT, always favor the AppInstance.GetActivatedEventArgs() API. - if (OpenIddictClientWindowsHelpers.IsWindowsRuntimeSupported() && - OpenIddictClientWindowsHelpers.GetProtocolActivationUriWithWindowsRuntime() is Uri uri) + var context = new ProcessRequestContext(transaction) { - return new OpenIddictClientWindowsActivation - { - ActivationUri = uri, - IsActivationRedirected = false - }; - } -#endif - // Otherwise, try to extract the protocol activation from the command line arguments. - if (OpenIddictClientWindowsHelpers.GetProtocolActivationUriFromCommandLineArguments( - Environment.GetCommandLineArgs()) is Uri value) + CancellationToken = cancellationToken + }; + + await dispatcher.DispatchAsync(context); + + if (context.IsRejected) { - return new OpenIddictClientWindowsActivation + await dispatcher.DispatchAsync(new ProcessErrorContext(transaction) { - ActivationUri = value, - IsActivationRedirected = false - }; + CancellationToken = cancellationToken, + Error = context.Error ?? Errors.InvalidRequest, + ErrorDescription = context.ErrorDescription, + ErrorUri = context.ErrorUri, + Response = new OpenIddictResponse() + }); } - - return null; } - [MethodImpl(MethodImplOptions.NoInlining)] - static async Task HandleProtocolActivationAsync(IServiceProvider provider, - OpenIddictClientWindowsActivation activation, CancellationToken cancellationToken) + finally { - var scope = provider.CreateScope(); - - try + if (scope is IAsyncDisposable disposable) { - var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - - // Create a client transaction and store the protocol activation details so they can be - // retrieved by the Windows-specific client event handlers that need to access them. - var transaction = await factory.CreateTransactionAsync(); - transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!, activation); - - var context = new ProcessRequestContext(transaction) - { - CancellationToken = cancellationToken - }; - - await dispatcher.DispatchAsync(context); - - if (context.IsRejected) - { - await dispatcher.DispatchAsync(new ProcessErrorContext(transaction) - { - CancellationToken = cancellationToken, - Error = context.Error ?? Errors.InvalidRequest, - ErrorDescription = context.ErrorDescription, - ErrorUri = context.ErrorUri, - Response = new OpenIddictResponse() - }); - } + await disposable.DisposeAsync(); } - finally + else { - if (scope is IAsyncDisposable disposable) - { - await disposable.DisposeAsync(); - } - - else - { - scope.Dispose(); - } + scope.Dispose(); } } } - - /// - public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }