Browse Source

Centralize protocol activations in a new service and add a runtime check to ensure the Windows integration is not used on unsupported platforms

pull/1674/head
Kévin Chalet 3 years ago
parent
commit
b054d4e4a6
  1. 5
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 26
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs
  3. 13
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs
  4. 95
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs
  5. 7
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs
  6. 79
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs
  7. 156
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs

5
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1424,7 +1424,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>An error occurred while authenticating the user.</value>
</data>
<data name="ID0375" xml:space="preserve">
<value>The Windows protocol activation cannot be resolved from the client transaction or contained invalid data.</value>
<value>The Windows protocol activation cannot be resolved from the client transaction.</value>
</data>
<data name="ID0376" xml:space="preserve">
<value>The identifier of the application instance that initiated the authentication process cannot be resolved from the state token.</value>
@ -1465,6 +1465,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0388" xml:space="preserve">
<value>The payload extracted from the inter-process notification is malformed, incomplete or was created by a different version of the OpenIddict client library.</value>
</data>
<data name="ID0389" xml:space="preserve">
<value>The OpenIddict client Windows integration is not supported on this platform.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
</data>

26
src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs

@ -9,15 +9,35 @@ using System.ComponentModel;
namespace OpenIddict.Client.Windows;
/// <summary>
/// Represents a Windows application activation.
/// Represents a Windows protocol activation.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientWindowsActivation
{
/// <summary>
/// Gets or sets the activation URI used to activate the application.
/// Creates a new instance of the <see cref="OpenIddictClientWindowsActivation"/> class.
/// </summary>
public Uri? ActivationUri { get; set; }
/// <param name="uri">The protocol activation URI.</param>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
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;
}
/// <summary>
/// Gets the protocol activation URI.
/// </summary>
public Uri ActivationUri { get; }
/// <summary>
/// Gets or sets a boolean indicating whether the activation

13
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<IHostedService, OpenIddictClientWindowsService>());
builder.Services.Insert(0, ServiceDescriptor.Singleton<IHostedService, OpenIddictClientWindowsHandler>());
}
// Register the marshal responsible for managing authentication operations.
// Register the services responsible for coordinating and managing authentication operations.
builder.Services.TryAddSingleton<OpenIddictClientWindowsMarshal>();
builder.Services.TryAddSingleton<OpenIddictClientWindowsService>();
// Register the built-in filters used by the default OpenIddict Windows client event handlers.
builder.Services.TryAddSingleton<RequireAuthenticationNonce>();

95
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;
/// <summary>
/// Contains the logic necessary to handle initial URI protocol activations.
/// </summary>
/// <remarks>
/// Note: redirected URI protocol activations are handled by <see cref="OpenIddictClientWindowsListener"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientWindowsHandler : IHostedService
{
private readonly IOptionsMonitor<OpenIddictClientWindowsOptions> _options;
private readonly OpenIddictClientWindowsService _service;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsHandler"/> class.
/// </summary>
/// <param name="options">The OpenIddict client Windows integration options.</param>
/// <param name="service">The OpenIddict client Windows service.</param>
public OpenIddictClientWindowsHandler(
IOptionsMonitor<OpenIddictClientWindowsOptions> options,
OpenIddictClientWindowsService service)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_service = service ?? throw new ArgumentNullException(nameof(service));
}
/// <inheritdoc/>
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;
}
}
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

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

79
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;
/// <summary>
@ -23,23 +17,29 @@ namespace OpenIddict.Client.Windows;
/// are redirected by other instances using inter-process communication.
/// </summary>
/// <remarks>
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientWindowsService"/>.
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientWindowsHandler"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientWindowsListener : BackgroundService
{
private readonly ILogger<OpenIddictClientWindowsListener> _logger;
private readonly IOptionsMonitor<OpenIddictClientWindowsOptions> _options;
private readonly IServiceProvider _provider;
private readonly OpenIddictClientWindowsService _service;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsHandler"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="options">The OpenIddict client Windows integration options.</param>
/// <param name="service">The OpenIddict client Windows service.</param>
public OpenIddictClientWindowsListener(
ILogger<OpenIddictClientWindowsListener> logger,
IOptionsMonitor<OpenIddictClientWindowsOptions> 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));
}
/// <inheritdoc/>
@ -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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
// 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();
}
}
}
}
}

156
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;
/// <summary>
/// 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).
/// </summary>
/// <remarks>
/// Note: redirected URI protocol activations are handled by <see cref="OpenIddictClientWindowsListener"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientWindowsService : IHostedService
public sealed class OpenIddictClientWindowsService
{
private readonly IOptionsMonitor<OpenIddictClientWindowsOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsService"/> class.
/// </summary>
/// <param name="options">The OpenIddict client Windows integration options.</param>
/// <param name="provider">The service provider.</param>
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
public OpenIddictClientWindowsService(
IOptionsMonitor<OpenIddictClientWindowsOptions> 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));
/// <inheritdoc/>
public Task StartAsync(CancellationToken cancellationToken)
/// <summary>
/// Handles the specified protocol activation.
/// </summary>
/// <param name="activation">The protocol activation details.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="activation"/> is <see langword="null"/>.</exception>
[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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
// 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();
}
}
}
/// <inheritdoc/>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

Loading…
Cancel
Save