From 0f2f3b45e5fa13182ba913a15cb6062a9ef149d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 6 Feb 2023 07:20:58 +0100 Subject: [PATCH] Add an option to disable protocol activation handling and extract the protocol activation details earlier --- .../OpenIddictResources.resx | 2 +- .../OpenIddictClientWindowsActivation.cs | 6 +- .../OpenIddictClientWindowsBuilder.cs | 8 + .../OpenIddictClientWindowsHandlers.cs | 37 ++--- .../OpenIddictClientWindowsHelpers.cs | 25 +++ .../OpenIddictClientWindowsListener.cs | 13 +- .../OpenIddictClientWindowsOptions.cs | 7 +- .../OpenIddictClientWindowsService.cs | 142 ++++++++++++------ 8 files changed, 146 insertions(+), 94 deletions(-) diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index c0ac3dfa..bb7f6692 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 protocol activation arguments cannot be resolved from the client transaction. + The Windows protocol activation cannot be resolved from the client transaction or contained invalid data. The identifier of the application instance that initiated the authentication process cannot be resolved from the state token. diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs index 22d2031c..ccc5d426 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs @@ -4,7 +4,6 @@ * the license and the contributors participating to this project. */ -using System.Collections.Immutable; using System.ComponentModel; namespace OpenIddict.Client.Windows; @@ -16,10 +15,9 @@ namespace OpenIddict.Client.Windows; public sealed class OpenIddictClientWindowsActivation { /// - /// Gets or sets the activation arguments used to - /// launch the current instance of the application. + /// Gets or sets the activation URI used to activate the application. /// - public ImmutableArray ActivationArguments { get; set; } + public Uri? ActivationUri { get; set; } /// /// Gets or sets a boolean indicating whether the activation diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs index 7ac56ef6..a9bf3d6d 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs @@ -47,6 +47,14 @@ public sealed class OpenIddictClientWindowsBuilder return this; } + /// + /// Disables the built-in protocol activation processing logic, which + /// can be used to offload this task a separate dedicated executable. + /// + /// The . + public OpenIddictClientWindowsBuilder DisableProtocolActivationProcessing() + => Configure(options => options.DisableProtocolActivationProcessing = true); + /// /// Sets the timeout after which authentication demands that /// are not completed are automatically aborted by OpenIddict. diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs index 3f448d53..faa7c951 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs @@ -90,25 +90,10 @@ public static partial class OpenIddictClientWindowsHandlers (context.BaseUri, context.RequestUri) = context.Transaction.GetWindowsActivation() switch { - null => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)), - - // In most cases, the first segment present in the command line arguments contains the path of the - // executable, but it's technically possible to start an application in a way that the command line - // arguments will never include the executable path. To support both cases, the URI is extracted - // from the second segment when 2 segments are present. Otherwise, the first segment is used. - // - // For more information, see https://devblogs.microsoft.com/oldnewthing/20060515-07/?p=31203. - - { ActivationArguments: [_, string argument] } when Uri.TryCreate(argument, UriKind.Absolute, out Uri? uri) && - !uri.IsFile && uri.IsWellFormedOriginalString() - => (new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), uri), - - { ActivationArguments: [string argument] } when Uri.TryCreate(argument, UriKind.Absolute, out Uri? uri) && - !uri.IsFile && uri.IsWellFormedOriginalString() + { ActivationUri: Uri uri } when uri.IsAbsoluteUri => (new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), uri), - // If no protocol activation URI could be resolved, use fake static URIs. - _ => (new Uri("local://", UriKind.Absolute), new Uri("local://", UriKind.Absolute)) + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)) }; return default; @@ -444,9 +429,6 @@ public static partial class OpenIddictClientWindowsHandlers // has been received by the other instance, ask the host to stop the application. if (!string.Equals(identifier, _options.CurrentValue.InstanceIdentifier, StringComparison.OrdinalIgnoreCase)) { - var activation = context.Transaction.GetWindowsActivation() ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)); - using (var buffer = new MemoryStream()) using (var writer = new BinaryWriter(buffer)) using (var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(10))) @@ -466,14 +448,13 @@ public static partial class OpenIddictClientWindowsHandlers writer.Write(0x01); writer.Write(0x01); - // Write the number of arguments present in the activation. - writer.Write(activation.ActivationArguments.Length); - - // Write all the arguments present in the activation. - for (var index = 0; index < activation.ActivationArguments.Length; index++) + // Write the protocol activation URI. + writer.Write(context.Transaction.GetWindowsActivation() switch { - writer.Write(activation.ActivationArguments[index]); - } + { ActivationUri: Uri uri } when uri.IsAbsoluteUri => uri.AbsoluteUri, + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)) + }); // Transfer the payload to the target. buffer.Seek(0L, SeekOrigin.Begin); @@ -1161,7 +1142,7 @@ public static partial class OpenIddictClientWindowsHandlers Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); - // Most Windows applications (except WinRT applications) are multi-instanced. As such, any protocol activation + // Most Windows applications (except UWP applications) are multi-instanced. As such, any protocol activation // triggered by launching one of the URI schemes associated with the application will create a new instance, // different from the one that initially started the authentication flow. To deal with that without having to // share persistent state between instances, OpenIddict stores the identifier of the instance that starts the diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs index 0fec0045..239e1b6a 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs @@ -169,6 +169,31 @@ public static class OpenIddictClientWindowsHelpers } #endif + /// + /// Resolves the protocol activation from the command line arguments, if applicable. + /// + /// + /// The if the application instance was activated + /// via a protocol activation, otherwise. + /// + internal static Uri? GetProtocolActivationUriFromCommandLineArguments(string?[]? arguments) => arguments switch + { + // In most cases, the first segment present in the command line arguments contains the path of the + // executable, but it's technically possible to start an application in a way that the command line + // arguments will never include the executable path. To support both cases, the URI is extracted + // from the second segment when 2 segments are present. Otherwise, the first segment is used. + // + // For more information, see https://devblogs.microsoft.com/oldnewthing/20060515-07/?p=31203. + + [_, string argument] when Uri.TryCreate(argument, UriKind.Absolute, out Uri? uri) && + !uri.IsFile && uri.IsWellFormedOriginalString() => uri, + + [string argument] when Uri.TryCreate(argument, UriKind.Absolute, out Uri? uri) && + !uri.IsFile && uri.IsWellFormedOriginalString() => uri, + + _ => null + }; + /// /// Starts the system browser using ShellExecute. /// diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs index 80c9ac97..f40b076c 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs @@ -4,7 +4,6 @@ * the license and the contributors participating to this project. */ -using System.Collections.Immutable; using System.ComponentModel; using System.IO.Pipes; using Microsoft.Extensions.DependencyInjection; @@ -93,25 +92,19 @@ public sealed class OpenIddictClientWindowsListener : BackgroundService continue; } - var length = reader.ReadInt32(); - if (length is not > 0) + var value = reader.ReadString(); + if (string.IsNullOrEmpty(value) || !Uri.TryCreate(value, UriKind.Absolute, out Uri? uri)) { continue; } - var builder = ImmutableArray.CreateBuilder(length); - for (var index = 0; index < length; index++) - { - builder.Add(reader.ReadString()); - } - // Create a client transaction and store the command line arguments 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!, new OpenIddictClientWindowsActivation { - ActivationArguments = builder.MoveToImmutable(), + ActivationUri = uri, IsActivationRedirected = true }); diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs index 6fb653cf..a4b79676 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs @@ -5,7 +5,6 @@ */ using System.IO.Pipes; -using Microsoft.Extensions.Hosting; #if !SUPPORTS_HOST_ENVIRONMENT using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment; @@ -24,6 +23,12 @@ public sealed class OpenIddictClientWindowsOptions /// public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10); + /// + /// Gets or sets a boolean indicating whether protocol activation processing should be + /// disabled, which can be used to offload this task to a separate dedicated executable. + /// + public bool DisableProtocolActivationProcessing { get; set; } + /// /// Gets or sets the identifier used to represent the current application /// instance and redirect protocol activations when necessary. diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs index 9666ae33..3ed44bee 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs +++ b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs @@ -4,10 +4,11 @@ * the license and the contributors participating to this project. */ -using System.Collections.Immutable; using System.ComponentModel; +using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; namespace OpenIddict.Client.Windows; @@ -20,21 +21,26 @@ namespace OpenIddict.Client.Windows; [EditorBrowsable(EditorBrowsableState.Never)] public sealed class OpenIddictClientWindowsService : IHostedService { + 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(IServiceProvider provider) - => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + public OpenIddictClientWindowsService( + IOptionsMonitor options, + IServiceProvider provider) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } /// - public async Task StartAsync(CancellationToken cancellationToken) + public Task StartAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - // 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 @@ -43,67 +49,103 @@ public sealed class OpenIddictClientWindowsService : IHostedService // 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. - var scope = _provider.CreateScope(); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } - try + // If the default activation processing logic was disabled in the options, ignore the activation. + if (_options.CurrentValue.DisableProtocolActivationProcessing) { - var dispatcher = scope.ServiceProvider.GetRequiredService(); - var factory = scope.ServiceProvider.GetRequiredService(); - - // Create a client transaction and store the command line arguments 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!, - new OpenIddictClientWindowsActivation - { - ActivationArguments = GetActivationArguments(), - IsActivationRedirected = false - }); + return Task.CompletedTask; + } - var context = new ProcessRequestContext(transaction) - { - CancellationToken = cancellationToken - }; + // 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; + } - await dispatcher.DispatchAsync(context); + return ExecuteAsync(_provider, activation, cancellationToken); - if (context.IsRejected) + [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) { - await dispatcher.DispatchAsync(new ProcessErrorContext(transaction) + return new OpenIddictClientWindowsActivation { - CancellationToken = cancellationToken, - Error = context.Error ?? Errors.InvalidRequest, - ErrorDescription = context.ErrorDescription, - ErrorUri = context.ErrorUri, - Response = new OpenIddictResponse() - }); + ActivationUri = uri, + IsActivationRedirected = false + }; } - } - - finally - { - if (scope is IAsyncDisposable disposable) +#endif + // Otherwise, try to extract the protocol activation from the command line arguments. + if (OpenIddictClientWindowsHelpers.GetProtocolActivationUriFromCommandLineArguments( + Environment.GetCommandLineArgs()) is Uri value) { - await disposable.DisposeAsync(); + return new OpenIddictClientWindowsActivation + { + ActivationUri = value, + IsActivationRedirected = false + }; } - else - { - scope.Dispose(); - } + return null; } - static ImmutableArray GetActivationArguments() + [MethodImpl(MethodImplOptions.NoInlining)] + static async Task ExecuteAsync(IServiceProvider provider, + OpenIddictClientWindowsActivation activation, CancellationToken cancellationToken) { -#if SUPPORTS_WINDOWS_RUNTIME - if (OpenIddictClientWindowsHelpers.IsWindowsRuntimeSupported() && - OpenIddictClientWindowsHelpers.GetProtocolActivationUriWithWindowsRuntime() is Uri uri) + var scope = provider.CreateScope(); + + try { - return ImmutableArray.Create(uri.AbsoluteUri); + 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() + }); + } } -#endif - return ImmutableArray.CreateRange(Environment.GetCommandLineArgs()); + finally + { + if (scope is IAsyncDisposable disposable) + { + await disposable.DisposeAsync(); + } + + else + { + scope.Dispose(); + } + } } }