Browse Source

Add an option to disable protocol activation handling and extract the protocol activation details earlier

pull/1671/head
Kévin Chalet 3 years ago
parent
commit
0f2f3b45e5
  1. 2
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  2. 6
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs
  3. 8
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs
  4. 37
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlers.cs
  5. 25
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs
  6. 13
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs
  7. 7
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs
  8. 142
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs

2
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 protocol activation arguments cannot be resolved from the client transaction.</value>
<value>The Windows protocol activation cannot be resolved from the client transaction or contained invalid data.</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>

6
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
{
/// <summary>
/// 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.
/// </summary>
public ImmutableArray<string> ActivationArguments { get; set; }
public Uri? ActivationUri { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether the activation

8
src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs

@ -47,6 +47,14 @@ public sealed class OpenIddictClientWindowsBuilder
return this;
}
/// <summary>
/// Disables the built-in protocol activation processing logic, which
/// can be used to offload this task a separate dedicated executable.
/// </summary>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
public OpenIddictClientWindowsBuilder DisableProtocolActivationProcessing()
=> Configure(options => options.DisableProtocolActivationProcessing = true);
/// <summary>
/// Sets the timeout after which authentication demands that
/// are not completed are automatically aborted by OpenIddict.

37
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

25
src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs

@ -169,6 +169,31 @@ public static class OpenIddictClientWindowsHelpers
}
#endif
/// <summary>
/// Resolves the protocol activation from the command line arguments, if applicable.
/// </summary>
/// <returns>
/// The <see cref="Uri"/> if the application instance was activated
/// via a protocol activation, <see langword="null"/> otherwise.
/// </returns>
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
};
/// <summary>
/// Starts the system browser using ShellExecute.
/// </summary>

13
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<string>(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
});

7
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
/// </summary>
public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10);
/// <summary>
/// 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.
/// </summary>
public bool DisableProtocolActivationProcessing { get; set; }
/// <summary>
/// Gets or sets the identifier used to represent the current application
/// instance and redirect protocol activations when necessary.

142
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<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(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
public OpenIddictClientWindowsService(
IOptionsMonitor<OpenIddictClientWindowsOptions> options,
IServiceProvider provider)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
/// <inheritdoc/>
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<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
// 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<string> 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<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()
});
}
}
#endif
return ImmutableArray.CreateRange(Environment.GetCommandLineArgs());
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
}

Loading…
Cancel
Save