46 changed files with 3781 additions and 867 deletions
@ -0,0 +1,30 @@ |
|||||
|
/* |
||||
|
* 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.Runtime.Versioning; |
||||
|
|
||||
|
namespace OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides various settings needed to configure the OpenIddict client system integration.
|
||||
|
/// </summary>
|
||||
|
public enum OpenIddictClientSystemIntegrationAuthenticationMode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Browser-based authentication.
|
||||
|
/// </summary>
|
||||
|
SystemBrowser = 0, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Windows web authentication broker-based authentication.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Note: the web authentication broker is only supported in UWP applications
|
||||
|
/// and its use is generally not recommended due to its inherent limitations.
|
||||
|
/// </remarks>
|
||||
|
[SupportedOSPlatform("windows10.0.17763")] |
||||
|
WebAuthenticationBroker = 1 |
||||
|
} |
||||
@ -0,0 +1,238 @@ |
|||||
|
/* |
||||
|
* 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 System.IO.Pipes; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Runtime.Versioning; |
||||
|
using OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
namespace Microsoft.Extensions.DependencyInjection; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Exposes the necessary methods required to configure
|
||||
|
/// the OpenIddict client system integration.
|
||||
|
/// </summary>
|
||||
|
public sealed class OpenIddictClientSystemIntegrationBuilder |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of <see cref="OpenIddictClientSystemIntegrationBuilder"/>.
|
||||
|
/// </summary>
|
||||
|
/// <param name="services">The services collection.</param>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder(IServiceCollection services) |
||||
|
=> Services = services ?? throw new ArgumentNullException(nameof(services)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the services collection.
|
||||
|
/// </summary>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public IServiceCollection Services { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Amends the default OpenIddict client system integration configuration.
|
||||
|
/// </summary>
|
||||
|
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
|
||||
|
/// <remarks>This extension can be safely called multiple times.</remarks>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder Configure(Action<OpenIddictClientSystemIntegrationOptions> configuration) |
||||
|
{ |
||||
|
if (configuration is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(configuration)); |
||||
|
} |
||||
|
|
||||
|
Services.Configure(configuration); |
||||
|
|
||||
|
return this; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Uses the Windows web authentication broker to start authentication flows.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Note: the web authentication broker is only supported in UWP applications
|
||||
|
/// and its use is generally not recommended due to its inherent limitations.
|
||||
|
/// </remarks>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
[SupportedOSPlatform("windows10.0.17763")] |
||||
|
public OpenIddictClientSystemIntegrationBuilder UseWebAuthenticationBroker() |
||||
|
{ |
||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); |
||||
|
} |
||||
|
|
||||
|
#if SUPPORTS_WINDOWS_RUNTIME
|
||||
|
if (!OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported()) |
||||
|
{ |
||||
|
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); |
||||
|
} |
||||
|
|
||||
|
return Configure(options => options.AuthenticationMode = |
||||
|
OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker); |
||||
|
#else
|
||||
|
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); |
||||
|
#endif
|
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Uses the system browser to start authentication flows.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder UseSystemBrowser() |
||||
|
=> Configure(options => options.AuthenticationMode = |
||||
|
OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the timeout after which authentication demands that
|
||||
|
/// are not completed are automatically aborted by OpenIddict.
|
||||
|
/// </summary>
|
||||
|
/// <param name="timeout">The authentication timeout.</param>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder SetAuthenticationTimeout(TimeSpan timeout) |
||||
|
=> Configure(options => options.AuthenticationTimeout = timeout); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disables the built-in protocol activation processing logic.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder DisableActivationHandling() |
||||
|
=> Configure(options => options.EnableActivationHandling = false); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enables the built-in protocol activation processing logic.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder EnableActivationHandling() |
||||
|
=> Configure(options => options.EnableActivationHandling = true); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disables the built-in protocol activation redirection logic.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder DisableActivationRedirection() |
||||
|
=> Configure(options => options.EnableActivationRedirection = false); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enables the built-in protocol activation redirection logic.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder EnableActivationRedirection() |
||||
|
=> Configure(options => options.EnableActivationRedirection = true); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disables the built-in web server used to handle callbacks.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder DisableEmbeddedWebServer() |
||||
|
=> Configure(options => options.EnableEmbeddedWebServer = false); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enables the built-in web server used to handle callbacks.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder EnableEmbeddedWebServer() |
||||
|
=> Configure(options => options.EnableEmbeddedWebServer = true); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Disables the pipe server used to process notifications (e.g protocol
|
||||
|
/// activation redirections) sent by other instances of the application.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder DisablePipeServer() |
||||
|
=> Configure(options => options.EnablePipeServer = false); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Enables the pipe server used to process protocol
|
||||
|
/// activations redirected by other instances of the application.
|
||||
|
/// </summary>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
public OpenIddictClientSystemIntegrationBuilder EnablePipeServer() |
||||
|
=> Configure(options => options.EnablePipeServer = true); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the identifier used to represent the current application
|
||||
|
/// instance and redirect protocol activations when necessary.
|
||||
|
/// </summary>
|
||||
|
/// <param name="identifier">The identifier of the current instance.</param>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Advanced)] |
||||
|
public OpenIddictClientSystemIntegrationBuilder SetInstanceIdentifier(string identifier) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier)); |
||||
|
} |
||||
|
|
||||
|
return Configure(options => options.InstanceIdentifier = identifier); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the base name of the pipe created by OpenIddict to enable
|
||||
|
/// inter-process communication and handle protocol activation redirections.
|
||||
|
/// </summary>
|
||||
|
/// <param name="name">The name of the pipe.</param>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Advanced)] |
||||
|
public OpenIddictClientSystemIntegrationBuilder SetPipeName(string name) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(name)) |
||||
|
{ |
||||
|
throw new ArgumentException(SR.FormatID0366(nameof(name)), nameof(name)); |
||||
|
} |
||||
|
|
||||
|
return Configure(options => options.PipeName = name); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the options applied to the pipe created by OpenIddict to enable
|
||||
|
/// inter-process communication and handle protocol activation redirections.
|
||||
|
/// </summary>
|
||||
|
/// <param name="flags">The options flags applied to the pipe.</param>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Advanced)] |
||||
|
public OpenIddictClientSystemIntegrationBuilder SetPipeOptions(PipeOptions flags) |
||||
|
=> Configure(options => options.PipeOptions = flags); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Sets the security policy applied to the pipe created by OpenIddict to enable
|
||||
|
/// inter-process communication and handle protocol activation redirections.
|
||||
|
/// </summary>
|
||||
|
/// <param name="security">The security policy applied to the pipe.</param>
|
||||
|
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Advanced), SupportedOSPlatform("windows")] |
||||
|
public OpenIddictClientSystemIntegrationBuilder SetPipeSecurity(PipeSecurity security) |
||||
|
{ |
||||
|
if (security is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(security)); |
||||
|
} |
||||
|
|
||||
|
return Configure(options => options.PipeSecurity = security); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Determines whether the specified object is equal to the current object.
|
||||
|
/// </summary>
|
||||
|
/// <param name="obj">The object to compare with the current object.</param>
|
||||
|
/// <returns><see langword="true"/> if the specified object is equal to the current object; otherwise, false.</returns>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public override bool Equals(object? obj) => base.Equals(obj); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Serves as the default hash function.
|
||||
|
/// </summary>
|
||||
|
/// <returns>A hash code for the current object.</returns>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public override int GetHashCode() => base.GetHashCode(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a string that represents the current object.
|
||||
|
/// </summary>
|
||||
|
/// <returns>A string that represents the current object.</returns>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public override string? ToString() => base.ToString(); |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
/* |
||||
|
* 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 System.IO.Pipes; |
||||
|
using System.Net; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Runtime.Versioning; |
||||
|
using System.Security.AccessControl; |
||||
|
using System.Security.Principal; |
||||
|
using System.Text; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Microsoft.IdentityModel.Tokens; |
||||
|
using OpenIddict.Extensions; |
||||
|
|
||||
|
#if !SUPPORTS_HOST_ENVIRONMENT
|
||||
|
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment; |
||||
|
#endif
|
||||
|
|
||||
|
namespace OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the methods required to ensure that the OpenIddict client system integration configuration is valid.
|
||||
|
/// </summary>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Advanced)] |
||||
|
public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureOptions<OpenIddictClientOptions>, |
||||
|
IPostConfigureOptions<OpenIddictClientOptions>, |
||||
|
IPostConfigureOptions<OpenIddictClientSystemIntegrationOptions> |
||||
|
{ |
||||
|
private readonly IHostEnvironment _environment; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationConfiguration"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="environment">The host environment.</param>
|
||||
|
public OpenIddictClientSystemIntegrationConfiguration(IHostEnvironment environment) |
||||
|
=> _environment = environment ?? throw new ArgumentNullException(nameof(environment)); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void Configure(OpenIddictClientOptions options) |
||||
|
{ |
||||
|
if (options is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(options)); |
||||
|
} |
||||
|
|
||||
|
// Register the built-in event handlers used by the OpenIddict client system integration components.
|
||||
|
options.Handlers.AddRange(OpenIddictClientSystemIntegrationHandlers.DefaultHandlers); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void PostConfigure(string? name, OpenIddictClientOptions options) |
||||
|
{ |
||||
|
if (options is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(options)); |
||||
|
} |
||||
|
|
||||
|
// Ensure an explicit client URI was set when using the system integration.
|
||||
|
if (options.ClientUri is not { IsAbsoluteUri: true }) |
||||
|
{ |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0384)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public void PostConfigure(string? name, OpenIddictClientSystemIntegrationOptions options) |
||||
|
{ |
||||
|
if (options is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(options)); |
||||
|
} |
||||
|
|
||||
|
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && |
||||
|
!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); |
||||
|
} |
||||
|
|
||||
|
// Note: the OpenIddict client system integration is currently only supported on Windows
|
||||
|
// and Linux. As such, using the system browser as the default authentication method in
|
||||
|
// conjunction with the embedded web server and activation handling should be always supported.
|
||||
|
options.AuthenticationMode ??= OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser; |
||||
|
options.EnableActivationHandling ??= true; |
||||
|
options.EnableActivationRedirection ??= true; |
||||
|
options.EnablePipeServer ??= true; |
||||
|
options.EnableEmbeddedWebServer ??= HttpListener.IsSupported; |
||||
|
|
||||
|
// If no explicit instance identifier was specified, use a random GUID.
|
||||
|
if (string.IsNullOrEmpty(options.InstanceIdentifier)) |
||||
|
{ |
||||
|
options.InstanceIdentifier = Guid.NewGuid().ToString(); |
||||
|
} |
||||
|
|
||||
|
// If no explicit pipe name was specified, compute the SHA-256 hash of the
|
||||
|
// application name resolved from the host and use it as a unique identifier.
|
||||
|
if (string.IsNullOrEmpty(options.PipeName)) |
||||
|
{ |
||||
|
if (string.IsNullOrEmpty(_environment.ApplicationName)) |
||||
|
{ |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0386)); |
||||
|
} |
||||
|
|
||||
|
var builder = new StringBuilder(); |
||||
|
|
||||
|
// Note: on Windows, the name is deliberately prefixed with "LOCAL\" to support
|
||||
|
// partial trust/sandboxed applications that are executed in an AppContainer
|
||||
|
// and cannot communicate with applications outside the sandbox container.
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
builder.Append(@"LOCAL\"); |
||||
|
} |
||||
|
|
||||
|
builder.Append(@"OpenIddict.Client.SystemIntegration-"); |
||||
|
builder.Append(Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash( |
||||
|
Encoding.UTF8.GetBytes(_environment.ApplicationName)))); |
||||
|
|
||||
|
options.PipeName = builder.ToString(); |
||||
|
} |
||||
|
|
||||
|
#if SUPPORTS_CURRENT_USER_ONLY_PIPE_OPTION
|
||||
|
if (options.PipeOptions is null && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
// Note: the CurrentUserOnly option is also supported on Windows, but is less
|
||||
|
// flexible than using a PipeSecurity object (e.g cross-process communication
|
||||
|
// between elevated and non-elevated processes is not possible with this option).
|
||||
|
// As such, it's not used on Windows (instead, an ACL-based PipeSecurity is used).
|
||||
|
options.PipeOptions = PipeOptions.CurrentUserOnly; |
||||
|
} |
||||
|
#endif
|
||||
|
|
||||
|
// Always configure the pipe to use asynchronous operations,
|
||||
|
// even if the flag was not explicitly set by the user.
|
||||
|
options.PipeOptions |= PipeOptions.Asynchronous; |
||||
|
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
// If no explicit pipe security policy was specified, grant the current user
|
||||
|
// full control over the created pipe and allow cross-process communication
|
||||
|
// between elevated and non-elevated processes. Note: if the process executes
|
||||
|
// inside an AppContainer, don't override the default OS pipe security policy
|
||||
|
// to allow all applications with the same identity to access the named pipe.
|
||||
|
if (options.PipeSecurity is null) |
||||
|
{ |
||||
|
using var identity = WindowsIdentity.GetCurrent(TokenAccessLevels.Query); |
||||
|
|
||||
|
if (!IsRunningInAppContainer(identity)) |
||||
|
{ |
||||
|
options.PipeSecurity = new PipeSecurity(); |
||||
|
options.PipeSecurity.SetOwner(identity.User!); |
||||
|
options.PipeSecurity.AddAccessRule(new PipeAccessRule(identity.User!, |
||||
|
PipeAccessRights.FullControl, AccessControlType.Allow)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(MethodImplOptions.NoInlining)] |
||||
|
[SupportedOSPlatform("windows")] |
||||
|
static bool IsRunningInAppContainer(WindowsIdentity identity) |
||||
|
#if SUPPORTS_WINDOWS_RUNTIME
|
||||
|
=> OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported() && |
||||
|
OpenIddictClientSystemIntegrationHelpers.HasAppContainerToken(identity); |
||||
|
#else
|
||||
|
=> false; |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,170 @@ |
|||||
|
/* |
||||
|
* 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.Options; |
||||
|
|
||||
|
namespace OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains a collection of event handler filters commonly used by the system integration handlers.
|
||||
|
/// </summary>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Advanced)] |
||||
|
public static class OpenIddictClientSystemIntegrationHandlerFilters |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a filter that excludes the associated handlers
|
||||
|
/// if no explicit nonce was attached to the authentication context.
|
||||
|
/// </summary>
|
||||
|
public sealed class RequireAuthenticationNonce : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
return new(!string.IsNullOrEmpty(context.Nonce)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a filter that excludes the associated handlers if no HTTP listener context can be found.
|
||||
|
/// </summary>
|
||||
|
public sealed class RequireHttpListenerContext : IOpenIddictClientHandlerFilter<BaseContext> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<bool> IsActiveAsync(BaseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
return new(context.Transaction.GetHttpListenerContext() is not null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a filter that excludes the associated handlers if no interactive user session was detected.
|
||||
|
/// </summary>
|
||||
|
public sealed class RequireInteractiveSession : IOpenIddictClientHandlerFilter<BaseContext> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<bool> IsActiveAsync(BaseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
return new(Environment.UserInteractive); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a filter that excludes the associated handlers if no protocol activation was found.
|
||||
|
/// </summary>
|
||||
|
public sealed class RequireProtocolActivation : IOpenIddictClientHandlerFilter<BaseContext> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<bool> IsActiveAsync(BaseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
return new(context.Transaction.GetProtocolActivation() is not null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a filter that excludes the associated handlers
|
||||
|
/// if the system browser integration was not enabled.
|
||||
|
/// </summary>
|
||||
|
public sealed class RequireSystemBrowser : IOpenIddictClientHandlerFilter<BaseContext> |
||||
|
{ |
||||
|
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options; |
||||
|
|
||||
|
public RequireSystemBrowser(IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options) |
||||
|
=> _options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<bool> IsActiveAsync(BaseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
if (!context.Transaction.Properties.TryGetValue( |
||||
|
typeof(OpenIddictClientSystemIntegrationAuthenticationMode).FullName!, out var result) || |
||||
|
result is not OpenIddictClientSystemIntegrationAuthenticationMode mode) |
||||
|
{ |
||||
|
mode = _options.CurrentValue.AuthenticationMode.GetValueOrDefault(); |
||||
|
} |
||||
|
|
||||
|
return new(mode is OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a filter that excludes the associated handlers if
|
||||
|
/// the web authentication broker integration was not enabled.
|
||||
|
/// </summary>
|
||||
|
public sealed class RequireWebAuthenticationBroker : IOpenIddictClientHandlerFilter<BaseContext> |
||||
|
{ |
||||
|
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options; |
||||
|
|
||||
|
public RequireWebAuthenticationBroker(IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options) |
||||
|
=> _options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<bool> IsActiveAsync(BaseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
if (!context.Transaction.Properties.TryGetValue( |
||||
|
typeof(OpenIddictClientSystemIntegrationAuthenticationMode).FullName!, out var result) || |
||||
|
result is not OpenIddictClientSystemIntegrationAuthenticationMode mode) |
||||
|
{ |
||||
|
mode = _options.CurrentValue.AuthenticationMode.GetValueOrDefault(); |
||||
|
} |
||||
|
|
||||
|
#pragma warning disable CA1416
|
||||
|
return new(mode is OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker); |
||||
|
#pragma warning restore CA1416
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Represents a filter that excludes the associated handlers if no
|
||||
|
/// web authentication operation was triggered during the transaction.
|
||||
|
/// </summary>
|
||||
|
public sealed class RequireWebAuthenticationResult : IOpenIddictClientHandlerFilter<BaseContext> |
||||
|
{ |
||||
|
/// <inheritdoc/>
|
||||
|
public ValueTask<bool> IsActiveAsync(BaseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
#if SUPPORTS_WINDOWS_RUNTIME
|
||||
|
return new(context.Transaction.GetWebAuthenticationResult() is not null); |
||||
|
#else
|
||||
|
return new(false); |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,308 @@ |
|||||
|
/* |
||||
|
* 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.Collections.Immutable; |
||||
|
using System.Diagnostics; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using System.Text; |
||||
|
using Microsoft.Extensions.Primitives; |
||||
|
using OpenIddict.Extensions; |
||||
|
|
||||
|
#if SUPPORTS_WINDOWS_RUNTIME
|
||||
|
using Windows.Security.Authentication.Web; |
||||
|
#endif
|
||||
|
|
||||
|
namespace OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
public static partial class OpenIddictClientSystemIntegrationHandlers |
||||
|
{ |
||||
|
public static class Authentication |
||||
|
{ |
||||
|
public static ImmutableArray<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create( |
||||
|
/* |
||||
|
* Authorization request processing: |
||||
|
*/ |
||||
|
InvokeWebAuthenticationBroker.Descriptor, |
||||
|
LaunchSystemBrowser.Descriptor, |
||||
|
|
||||
|
/* |
||||
|
* Redirection request extraction: |
||||
|
*/ |
||||
|
ExtractGetHttpListenerRequest<ExtractRedirectionRequestContext>.Descriptor, |
||||
|
ExtractProtocolActivationParameters<ExtractRedirectionRequestContext>.Descriptor, |
||||
|
ExtractWebAuthenticationResultData<ExtractRedirectionRequestContext>.Descriptor, |
||||
|
|
||||
|
/* |
||||
|
* Redirection response handling: |
||||
|
*/ |
||||
|
AttachHttpResponseCode<ApplyRedirectionResponseContext>.Descriptor, |
||||
|
AttachCacheControlHeader<ApplyRedirectionResponseContext>.Descriptor, |
||||
|
ProcessEmptyHttpResponse.Descriptor, |
||||
|
ProcessUnactionableResponse<ApplyRedirectionResponseContext>.Descriptor); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for initiating authorization requests using the web authentication broker.
|
||||
|
/// Note: this handler is not used when the user session is not interactive.
|
||||
|
/// </summary>
|
||||
|
public class InvokeWebAuthenticationBroker : IOpenIddictClientHandler<ApplyAuthorizationRequestContext> |
||||
|
{ |
||||
|
private readonly OpenIddictClientSystemIntegrationService _service; |
||||
|
|
||||
|
public InvokeWebAuthenticationBroker(OpenIddictClientSystemIntegrationService service) |
||||
|
=> _service = service ?? throw new ArgumentNullException(nameof(service)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyAuthorizationRequestContext>() |
||||
|
.AddFilter<RequireInteractiveSession>() |
||||
|
.AddFilter<RequireWebAuthenticationBroker>() |
||||
|
.UseSingletonHandler<InvokeWebAuthenticationBroker>() |
||||
|
.SetOrder(100_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
#pragma warning disable CS1998
|
||||
|
public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context) |
||||
|
#pragma warning restore CS1998
|
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008)); |
||||
|
|
||||
|
#if SUPPORTS_WINDOWS_RUNTIME
|
||||
|
if (string.IsNullOrEmpty(context.RedirectUri)) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// OpenIddict represents the complete interactive authentication dance as a two-phase process:
|
||||
|
// - The challenge, during which the user is redirected to the authorization server, either
|
||||
|
// by launching the system browser or, as in this case, using a web-view-like approach.
|
||||
|
//
|
||||
|
// - The authentication that takes place after the authorization server and the user approved
|
||||
|
// the demand and redirected the user agent to the client (using either protocol activation,
|
||||
|
// an embedded web server or by tracking the return URL of the web view created for the process).
|
||||
|
//
|
||||
|
// Unlike OpenIddict, WebAuthenticationBroker materializes this process as a single/one-shot API
|
||||
|
// that opens the system-managed authentication host, navigates to the specified request URI and
|
||||
|
// doesn't return until the specified callback URI is reached or the modal closed by the user.
|
||||
|
// To accomodate OpenIddict's model, successful results are processed as any other callback request.
|
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
// Note: IAsyncOperation<T>.AsTask(context.CancellationToken) is deliberately not used here as
|
||||
|
// the asynchronous operation returned by the web authentication broker is not cancellable.
|
||||
|
switch (await WebAuthenticationBroker.AuthenticateAsync( |
||||
|
options : WebAuthenticationOptions.None, |
||||
|
requestUri : OpenIddictHelpers.AddQueryStringParameters( |
||||
|
uri: new Uri(context.AuthorizationEndpoint, UriKind.Absolute), |
||||
|
parameters: context.Transaction.Request.GetParameters().ToDictionary( |
||||
|
parameter => parameter.Key, |
||||
|
parameter => new StringValues((string?[]?) parameter.Value))), |
||||
|
callbackUri: new Uri(context.RedirectUri, UriKind.Absolute))) |
||||
|
{ |
||||
|
case { ResponseStatus: WebAuthenticationStatus.Success } result: |
||||
|
await _service.HandleWebAuthenticationResultAsync(result, context.CancellationToken); |
||||
|
context.HandleRequest(); |
||||
|
return; |
||||
|
|
||||
|
// Since the result of this operation is known by the time WebAuthenticationBroker.AuthenticateAsync()
|
||||
|
// returns, some errors can directly be handled and surfaced here, as part of the challenge handling.
|
||||
|
|
||||
|
case { ResponseStatus: WebAuthenticationStatus.UserCancel }: |
||||
|
context.Reject( |
||||
|
error: Errors.AccessDenied, |
||||
|
description: SR.GetResourceString(SR.ID2149), |
||||
|
uri: SR.FormatID8000(SR.ID2149)); |
||||
|
|
||||
|
return; |
||||
|
|
||||
|
case { ResponseStatus: WebAuthenticationStatus.ErrorHttp } result: |
||||
|
context.Reject( |
||||
|
error: result.ResponseErrorDetail switch |
||||
|
{ |
||||
|
400 => Errors.InvalidRequest, |
||||
|
401 => Errors.InvalidToken, |
||||
|
403 => Errors.InsufficientAccess, |
||||
|
429 => Errors.SlowDown, |
||||
|
500 => Errors.ServerError, |
||||
|
503 => Errors.TemporarilyUnavailable, |
||||
|
_ => Errors.ServerError |
||||
|
}, |
||||
|
description: SR.FormatID2161(result.ResponseErrorDetail), |
||||
|
uri: SR.FormatID8000(SR.ID2161)); |
||||
|
|
||||
|
return; |
||||
|
|
||||
|
default: |
||||
|
context.Reject( |
||||
|
error: Errors.ServerError, |
||||
|
description: SR.GetResourceString(SR.ID2136), |
||||
|
uri: SR.FormatID8000(SR.ID2136)); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
catch |
||||
|
{ |
||||
|
context.Reject( |
||||
|
error: Errors.ServerError, |
||||
|
description: SR.GetResourceString(SR.ID2136), |
||||
|
uri: SR.FormatID8000(SR.ID2136)); |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
#else
|
||||
|
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for initiating authorization requests using the system browser.
|
||||
|
/// Note: this handler is not used when the user session is not interactive.
|
||||
|
/// </summary>
|
||||
|
public class LaunchSystemBrowser : IOpenIddictClientHandler<ApplyAuthorizationRequestContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyAuthorizationRequestContext>() |
||||
|
.AddFilter<RequireInteractiveSession>() |
||||
|
.AddFilter<RequireSystemBrowser>() |
||||
|
.UseSingletonHandler<LaunchSystemBrowser>() |
||||
|
.SetOrder(InvokeWebAuthenticationBroker.Descriptor.Order + 1_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008)); |
||||
|
|
||||
|
var uri = OpenIddictHelpers.AddQueryStringParameters( |
||||
|
uri: new Uri(context.AuthorizationEndpoint, UriKind.Absolute), |
||||
|
parameters: context.Transaction.Request.GetParameters().ToDictionary( |
||||
|
parameter => parameter.Key, |
||||
|
parameter => new StringValues((string?[]?) parameter.Value))); |
||||
|
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) |
||||
|
{ |
||||
|
// Note: on Windows, multiple application models exist and must be supported to cover most scenarios:
|
||||
|
//
|
||||
|
// - Classical Win32 applications, for which no application-specific restriction is enforced.
|
||||
|
// - Win32 applications running in an AppContainer, that are very similar to UWP applications.
|
||||
|
// - Classical UWP applications, for which strict application restrictions are enforced.
|
||||
|
// - Full-trust UWP applications, that are rare but very similar to classical Win32 applications.
|
||||
|
// - Modern/hybrid Windows applications, that can be sandboxed or run as full-trust applications.
|
||||
|
//
|
||||
|
// Since .NET Standard 2.0 support for UWP was only introduced in Windows 10 1709 (also known
|
||||
|
// as Fall Creators Update) and OpenIddict requires Windows 10 1809 as the minimum supported
|
||||
|
// version, Windows 8/8.1's Metro-style/universal applications are deliberately not supported.
|
||||
|
//
|
||||
|
// While Process.Start()/ShellExecuteEx() can typically be used without any particular restriction
|
||||
|
// by non-sandboxed desktop applications to launch the default system browser, calling these
|
||||
|
// APIs in sandboxed applications will result in an UnauthorizedAccessException being thrown.
|
||||
|
//
|
||||
|
// To avoid that, the OpenIddict host needs to determine whether the platform supports Windows
|
||||
|
// Runtime APIs and favor the Launcher.LaunchUriAsync() API when it's offered by the platform.
|
||||
|
|
||||
|
#if SUPPORTS_WINDOWS_RUNTIME
|
||||
|
if (OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported() && await |
||||
|
OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithWindowsRuntimeAsync(uri)) |
||||
|
{ |
||||
|
context.HandleRequest(); |
||||
|
return; |
||||
|
} |
||||
|
#endif
|
||||
|
if (await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithShellExecuteAsync(uri)) |
||||
|
{ |
||||
|
context.HandleRequest(); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && |
||||
|
await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri)) |
||||
|
{ |
||||
|
context.HandleRequest(); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0385)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic responsible for processing OpenID Connect responses that don't specify any parameter.
|
||||
|
/// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server.
|
||||
|
/// </summary>
|
||||
|
public sealed class ProcessEmptyHttpResponse : IOpenIddictClientHandler<ApplyRedirectionResponseContext> |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets the default descriptor definition assigned to this handler.
|
||||
|
/// </summary>
|
||||
|
public static OpenIddictClientHandlerDescriptor Descriptor { get; } |
||||
|
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyRedirectionResponseContext>() |
||||
|
.AddFilter<RequireHttpListenerContext>() |
||||
|
.UseSingletonHandler<ProcessEmptyHttpResponse>() |
||||
|
.SetOrder(int.MaxValue - 100_000) |
||||
|
.SetType(OpenIddictClientHandlerType.BuiltIn) |
||||
|
.Build(); |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public async ValueTask HandleAsync(ApplyRedirectionResponseContext context) |
||||
|
{ |
||||
|
if (context is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(context)); |
||||
|
} |
||||
|
|
||||
|
Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); |
||||
|
|
||||
|
// This handler only applies to HTTP listener requests. If the HTTP context cannot be resolved,
|
||||
|
// this may indicate that the request was incorrectly processed by another server stack.
|
||||
|
var response = context.Transaction.GetHttpListenerContext()?.Response ?? |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0390)); |
||||
|
|
||||
|
// Always return a 200 status, even for responses indicating that the authentication failed.
|
||||
|
response.StatusCode = 200; |
||||
|
response.ContentType = "text/plain"; |
||||
|
|
||||
|
// Return a message indicating whether the authentication process
|
||||
|
// succeeded or failed and that will be visible by the user.
|
||||
|
var buffer = Encoding.UTF8.GetBytes(context.Transaction.Response.Error switch |
||||
|
{ |
||||
|
null or { Length: 0 } => "Login completed. Please return to the application.", |
||||
|
Errors.AccessDenied => "Authorization denied. Please return to the application.", |
||||
|
_ => "Authentication failed. Please return to the application." |
||||
|
}); |
||||
|
|
||||
|
#if SUPPORTS_STREAM_MEMORY_METHODS
|
||||
|
await response.OutputStream.WriteAsync(buffer); |
||||
|
#else
|
||||
|
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); |
||||
|
#endif
|
||||
|
await response.OutputStream.FlushAsync(); |
||||
|
|
||||
|
context.HandleRequest(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -0,0 +1,190 @@ |
|||||
|
/* |
||||
|
* 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 System.Globalization; |
||||
|
using System.Net; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using OpenIddict.Extensions; |
||||
|
|
||||
|
namespace OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic necessary to handle URI protocol activations that
|
||||
|
/// are redirected by other instances using inter-process communication.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientSystemIntegrationActivationHandler"/>.
|
||||
|
/// </remarks>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public sealed class OpenIddictClientSystemIntegrationHttpListener : BackgroundService |
||||
|
{ |
||||
|
private readonly TaskCompletionSource<int?> _source = new(); |
||||
|
private readonly ILogger<OpenIddictClientSystemIntegrationHttpListener> _logger; |
||||
|
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options; |
||||
|
private readonly OpenIddictClientSystemIntegrationService _service; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationHttpListener"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="logger">The logger.</param>
|
||||
|
/// <param name="options">The OpenIddict client system integration options.</param>
|
||||
|
/// <param name="service">The OpenIddict client system integration service.</param>
|
||||
|
public OpenIddictClientSystemIntegrationHttpListener( |
||||
|
ILogger<OpenIddictClientSystemIntegrationHttpListener> logger, |
||||
|
IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options, |
||||
|
OpenIddictClientSystemIntegrationService service) |
||||
|
{ |
||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
||||
|
_options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
_service = service ?? throw new ArgumentNullException(nameof(service)); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
||||
|
{ |
||||
|
// If the embedded web server instantiation was not enabled, signal the task completion source with a
|
||||
|
// null value to inform the handlers that no HTTP listener is going to be created and return immediately.
|
||||
|
if (_options.CurrentValue.EnableEmbeddedWebServer is not true) |
||||
|
{ |
||||
|
_source.SetResult(result: null); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
// Note: finding a free port in the IANA dynamic port range can take a bit of time on busy systems.
|
||||
|
// To ensure the host initialization is not blocked, the whole process is offloaded to the thread pool.
|
||||
|
await Task.Run(cancellationToken: stoppingToken, function: async () => |
||||
|
{ |
||||
|
var (listener, port) = CreateHttpListener(stoppingToken) ?? |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0391)); |
||||
|
|
||||
|
// Inform the handlers that the HTTP listener was created and can now be accessed via the specified port.
|
||||
|
_source.SetResult(port); |
||||
|
|
||||
|
using (listener) |
||||
|
{ |
||||
|
// Note: while the received load should be minimal, 3 task workers are used
|
||||
|
// to be able to process multiple requests at the same time, if necessary.
|
||||
|
var tasks = new Task[3]; |
||||
|
|
||||
|
for (var index = 0; index < tasks.Length; index++) |
||||
|
{ |
||||
|
tasks[index] = ProcessRequestsAsync(listener, _service, _logger, stoppingToken); |
||||
|
} |
||||
|
|
||||
|
// Wait for all the workers to indicate they finished processing incoming requests.
|
||||
|
await Task.WhenAll(tasks); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Ignore exceptions indicating that the host is shutting down and return immediately.
|
||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
static (HttpListener Listener, int Port)? CreateHttpListener(CancellationToken cancellationToken) |
||||
|
{ |
||||
|
// Note: HttpListener doesn't offer a native way to select a random, non-busy port.
|
||||
|
// To work around this limitation, this local function tries to bind an HttpListener
|
||||
|
// on the first free port in the IANA dynamic ports range (typically: 49152 to 65535).
|
||||
|
//
|
||||
|
// For more information, see
|
||||
|
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml.
|
||||
|
|
||||
|
for (var port = 49152; port < 65535; port++) |
||||
|
{ |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
var listener = new HttpListener |
||||
|
{ |
||||
|
AuthenticationSchemes = AuthenticationSchemes.Anonymous, |
||||
|
IgnoreWriteExceptions = true, |
||||
|
|
||||
|
// Note: the prefix registration is deliberately not configurable to ensure
|
||||
|
// only the "localhost" authority is used, which enforces the built-in host
|
||||
|
// validation performed by HTTP.sys (or the managed .NET implementation on
|
||||
|
// non-Windows operating systems) and doesn't require running the application
|
||||
|
// as an administrator or adding a namespace reservation/ACL rule on Windows.
|
||||
|
Prefixes = { $"http://localhost:{port.ToString(CultureInfo.InvariantCulture)}/" } |
||||
|
}; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
listener.Start(); |
||||
|
|
||||
|
return (listener, port); |
||||
|
} |
||||
|
|
||||
|
catch (HttpListenerException) |
||||
|
{ |
||||
|
listener.Close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
static async Task ProcessRequestsAsync(HttpListener listener, OpenIddictClientSystemIntegrationService service, |
||||
|
ILogger<OpenIddictClientSystemIntegrationHttpListener> logger, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
// Note: HttpListener.GetContextAsync() doesn't support cooperative cancellation. To ensure the host
|
||||
|
// can gracefully shut down without being blocked by an asynchronous call that would never complete,
|
||||
|
// Task.WaitAsync() is used to stop waiting on the task returned by HttpListener.GetContextAsync()
|
||||
|
// when the CancellationToken provided by the host indicates that the application is about to shut down.
|
||||
|
var context = await listener.GetContextAsync().WaitAsync(cancellationToken); |
||||
|
|
||||
|
using (context.Response) |
||||
|
{ |
||||
|
// Only process requests for which the request URL could be decoded/parsed correctly.
|
||||
|
if (context.Request.Url is { IsAbsoluteUri: true }) |
||||
|
{ |
||||
|
await service.HandleHttpRequestAsync(context, cancellationToken); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Surface operation canceled exceptions when the host is shutting down.
|
||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
|
||||
|
// Swallow other exceptions to ensure the worker doesn't exit when encountering an exception.
|
||||
|
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) |
||||
|
{ |
||||
|
logger.LogWarning(exception, SR.GetResourceString(SR.ID6214)); |
||||
|
|
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Resolves the port associated to the <see cref="HttpListener"/> created by this service, or
|
||||
|
/// <see langword="null"/> if the embedded web server instantiation was disabled in the options.
|
||||
|
/// </summary>
|
||||
|
/// <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, whose result
|
||||
|
/// returns the port associated to the <see cref="HttpListener"/> created by this service, or
|
||||
|
/// <see langword="null"/> if the embedded web server instantiation was disabled in the options.
|
||||
|
/// </returns>
|
||||
|
internal Task<int?> GetEmbeddedServerPortAsync(CancellationToken cancellationToken = default) |
||||
|
=> _source.Task.WaitAsync(cancellationToken); |
||||
|
} |
||||
@ -0,0 +1,103 @@ |
|||||
|
/* |
||||
|
* 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.IO.Pipes; |
||||
|
using System.Runtime.Versioning; |
||||
|
|
||||
|
namespace OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Provides various settings needed to configure the OpenIddict client system integration.
|
||||
|
/// </summary>
|
||||
|
public sealed class OpenIddictClientSystemIntegrationOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Gets or sets the authentication mode used to start authentication flows.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If this property is not explicitly set, its value is automatically set by OpenIddict.
|
||||
|
/// </remarks>
|
||||
|
public OpenIddictClientSystemIntegrationAuthenticationMode? AuthenticationMode { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the timeout after which authentication demands
|
||||
|
/// that are not completed are automatically aborted by OpenIddict.
|
||||
|
/// </summary>
|
||||
|
public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a boolean indicating whether protocol activation processing should be enabled.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If this property is not explicitly set, its value is automatically set by OpenIddict
|
||||
|
/// depending on the capabilities of the system on which the application is running.
|
||||
|
/// </remarks>
|
||||
|
public bool? EnableActivationHandling { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a boolean indicating whether protocol activation redirection should be enabled.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If this property is not explicitly set, its value is automatically set by OpenIddict
|
||||
|
/// depending on the capabilities of the system on which the application is running.
|
||||
|
/// </remarks>
|
||||
|
public bool? EnableActivationRedirection { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a boolean indicating whether a local web server
|
||||
|
/// should be started on a random port to handle callbacks.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If this property is not explicitly set, its value is automatically set by OpenIddict
|
||||
|
/// depending on the capabilities of the system on which the application is running.
|
||||
|
/// </remarks>
|
||||
|
public bool? EnableEmbeddedWebServer { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets a boolean indicating whether a pipe server should be started to process notifications
|
||||
|
/// (e.g protocol activation redirections) sent by other instances of the application.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If this property is not explicitly set, its value is automatically set by OpenIddict
|
||||
|
/// depending on the capabilities of the system on which the application is running.
|
||||
|
/// </remarks>
|
||||
|
public bool? EnablePipeServer { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the identifier used to represent the current application
|
||||
|
/// instance and redirect protocol activations when necessary.
|
||||
|
/// </summary>
|
||||
|
public string? InstanceIdentifier { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the base name of the pipe created by OpenIddict to enable
|
||||
|
/// inter-process communication and handle protocol activation redirections.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If no value is explicitly set, a default name is automatically computed.
|
||||
|
/// </remarks>
|
||||
|
public string PipeName { get; set; } = default!; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the pipe options applied to the pipe created by OpenIddict to enable
|
||||
|
/// inter-process communication and handle protocol activation redirections.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If no value is explicitly set, a default combination is automatically used.
|
||||
|
/// </remarks>
|
||||
|
public PipeOptions? PipeOptions { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the security policy applied to the pipe created by OpenIddict
|
||||
|
/// to enable inter-process communication and handle protocol activation redirections.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// If no value is explicitly set, a default policy is automatically created
|
||||
|
/// (unless the application is running inside an AppContainer sandbox).
|
||||
|
/// </remarks>
|
||||
|
[SupportedOSPlatform("windows")] |
||||
|
public PipeSecurity? PipeSecurity { get; set; } |
||||
|
} |
||||
@ -0,0 +1,183 @@ |
|||||
|
/* |
||||
|
* 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 System.IO.Pipes; |
||||
|
using System.Runtime.InteropServices; |
||||
|
using Microsoft.Extensions.Hosting; |
||||
|
using Microsoft.Extensions.Logging; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using OpenIddict.Extensions; |
||||
|
|
||||
|
namespace OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Contains the logic necessary to handle URI protocol activations that
|
||||
|
/// are redirected by other instances using inter-process communication.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientSystemIntegrationActivationHandler"/>.
|
||||
|
/// </remarks>
|
||||
|
[EditorBrowsable(EditorBrowsableState.Never)] |
||||
|
public sealed class OpenIddictClientSystemIntegrationPipeListener : BackgroundService |
||||
|
{ |
||||
|
private readonly ILogger<OpenIddictClientSystemIntegrationPipeListener> _logger; |
||||
|
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options; |
||||
|
private readonly OpenIddictClientSystemIntegrationService _service; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationPipeListener"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="logger">The logger.</param>
|
||||
|
/// <param name="options">The OpenIddict client system integration options.</param>
|
||||
|
/// <param name="service">The OpenIddict client system integration service.</param>
|
||||
|
public OpenIddictClientSystemIntegrationPipeListener( |
||||
|
ILogger<OpenIddictClientSystemIntegrationPipeListener> logger, |
||||
|
IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options, |
||||
|
OpenIddictClientSystemIntegrationService service) |
||||
|
{ |
||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
||||
|
_options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
_service = service ?? throw new ArgumentNullException(nameof(service)); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
||||
|
{ |
||||
|
if (_options.CurrentValue.EnablePipeServer is not true) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
// Offload the whole process to avoid delaying the initialization of the host.
|
||||
|
await Task.Run(cancellationToken: stoppingToken, function: async () => |
||||
|
{ |
||||
|
// Note: while the received load should be minimal, 3 task workers are used
|
||||
|
// to be able to process multiple notifications at the same time, if necessary.
|
||||
|
var tasks = new Task[3]; |
||||
|
|
||||
|
for (var index = 0; index < tasks.Length; index++) |
||||
|
{ |
||||
|
tasks[index] = ProcessNotificationsAsync(_service, _logger, _options.CurrentValue, stoppingToken); |
||||
|
} |
||||
|
|
||||
|
// Wait for all the workers to indicate they finished processing incoming notifications.
|
||||
|
await Task.WhenAll(tasks); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Ignore exceptions indicating that the host is shutting down and return immediately.
|
||||
|
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
static async Task ProcessNotificationsAsync( |
||||
|
OpenIddictClientSystemIntegrationService service, ILogger<OpenIddictClientSystemIntegrationPipeListener> logger, |
||||
|
OpenIddictClientSystemIntegrationOptions options, CancellationToken cancellationToken) |
||||
|
{ |
||||
|
while (true) |
||||
|
{ |
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
using var buffer = new MemoryStream(); |
||||
|
using var reader = new BinaryReader(buffer); |
||||
|
using var stream = CreatePipeServerStream(options); |
||||
|
|
||||
|
// Wait for a writer to connect to the named pipe.
|
||||
|
//
|
||||
|
// Note: NamedPipeServerStream supports cooperative cancellation but it appears that cancellations
|
||||
|
// are not always properly handled in some obscure circumstances. To ensure the application shutdown
|
||||
|
// is not delayed by this issue, the Task.WaitAsync(CancellationToken) API is used to stop waiting
|
||||
|
// for the task returned by WaitForConnectionAsync() to complete when the application shuts down.
|
||||
|
await stream.WaitForConnectionAsync(cancellationToken).WaitAsync(cancellationToken); |
||||
|
|
||||
|
// Copy the content to the memory stream asynchronously and rewind it.
|
||||
|
await stream.CopyToAsync(buffer, bufferSize: 81_920, cancellationToken); |
||||
|
buffer.Seek(0L, SeekOrigin.Begin); |
||||
|
|
||||
|
// Process the inter-process notification based on its declared type.
|
||||
|
await (reader.ReadInt32() switch |
||||
|
{ |
||||
|
0x01 when ReadProtocolActivation(reader) is var activation |
||||
|
=> service.HandleProtocolActivationAsync(activation, cancellationToken), |
||||
|
|
||||
|
var value => throw new InvalidOperationException(SR.FormatID0387(value)) |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Ignore exceptions indicating that the host is shutting down and return immediately.
|
||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
|
||||
|
// Swallow other exceptions to ensure the service doesn't exit when encountering an exception.
|
||||
|
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) |
||||
|
{ |
||||
|
logger.LogWarning(exception, SR.GetResourceString(SR.ID6213)); |
||||
|
|
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
static NamedPipeServerStream CreatePipeServerStream(OpenIddictClientSystemIntegrationOptions options) |
||||
|
// Note: the ACL-based PipeSecurity class is only supported on Windows. On other operating systems,
|
||||
|
// PipeOptions.CurrentUserOnly can be used as an alternative, but only for TFMs that implement it.
|
||||
|
=> RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? |
||||
|
#if SUPPORTS_NAMED_PIPE_CONSTRUCTOR_WITH_ACL
|
||||
|
new NamedPipeServerStream( |
||||
|
#elif SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL
|
||||
|
NamedPipeServerStreamAcl.Create( |
||||
|
#else
|
||||
|
NamedPipeServerStreamConstructors.New( |
||||
|
#endif
|
||||
|
pipeName : $@"{options.PipeName}\{options.InstanceIdentifier}", |
||||
|
direction : PipeDirection.In, |
||||
|
maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances, |
||||
|
transmissionMode : PipeTransmissionMode.Byte, |
||||
|
options : options.PipeOptions.GetValueOrDefault(), |
||||
|
inBufferSize : 0, |
||||
|
outBufferSize : 0, |
||||
|
pipeSecurity : options.PipeSecurity, |
||||
|
inheritability : HandleInheritability.None, |
||||
|
additionalAccessRights : default) : |
||||
|
new NamedPipeServerStream( |
||||
|
pipeName : $@"{options.PipeName}\{options.InstanceIdentifier}", |
||||
|
direction : PipeDirection.In, |
||||
|
maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances, |
||||
|
transmissionMode : PipeTransmissionMode.Byte, |
||||
|
options : options.PipeOptions.GetValueOrDefault(), |
||||
|
inBufferSize : 0, |
||||
|
outBufferSize : 0); |
||||
|
|
||||
|
static OpenIddictClientSystemIntegrationActivation ReadProtocolActivation(BinaryReader reader) |
||||
|
{ |
||||
|
// Ensure the binary serialization format is supported.
|
||||
|
var version = reader.ReadInt32(); |
||||
|
if (version is not 0x01) |
||||
|
{ |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0388)); |
||||
|
} |
||||
|
|
||||
|
var value = reader.ReadString(); |
||||
|
if (string.IsNullOrEmpty(value) || !Uri.TryCreate(value, UriKind.Absolute, out Uri? uri)) |
||||
|
{ |
||||
|
throw new InvalidOperationException(SR.GetResourceString(SR.ID0388)); |
||||
|
} |
||||
|
|
||||
|
return new OpenIddictClientSystemIntegrationActivation(uri) |
||||
|
{ |
||||
|
IsActivationRedirected = true |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,208 @@ |
|||||
|
/* |
||||
|
* 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 System.IO.Pipes; |
||||
|
using System.Net; |
||||
|
using System.Security.Principal; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
|
||||
|
#if SUPPORTS_WINDOWS_RUNTIME
|
||||
|
using Windows.Security.Authentication.Web; |
||||
|
#endif
|
||||
|
|
||||
|
namespace OpenIddict.Client.SystemIntegration; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 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>
|
||||
|
public sealed class OpenIddictClientSystemIntegrationService |
||||
|
{ |
||||
|
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options; |
||||
|
private readonly IServiceProvider _provider; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationService"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="options">The OpenIddict client system integration options.</param>
|
||||
|
/// <param name="provider">The service provider.</param>
|
||||
|
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
|
||||
|
public OpenIddictClientSystemIntegrationService( |
||||
|
IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options, |
||||
|
IServiceProvider provider) |
||||
|
{ |
||||
|
_options = options ?? throw new ArgumentNullException(nameof(options)); |
||||
|
_provider = provider ?? throw new ArgumentNullException(nameof(provider)); |
||||
|
} |
||||
|
|
||||
|
/// <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 Task HandleProtocolActivationAsync(OpenIddictClientSystemIntegrationActivation activation, |
||||
|
CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
if (activation is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(activation)); |
||||
|
} |
||||
|
|
||||
|
return HandleRequestAsync(activation, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Handles the specified HTTP request.
|
||||
|
/// </summary>
|
||||
|
/// <param name="request">The HTTP request received by the embedded web server.</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="request"/> is <see langword="null"/>.</exception>
|
||||
|
internal Task HandleHttpRequestAsync(HttpListenerContext request, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
if (request is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(request)); |
||||
|
} |
||||
|
|
||||
|
return HandleRequestAsync(request, cancellationToken); |
||||
|
} |
||||
|
|
||||
|
#if SUPPORTS_WINDOWS_RUNTIME
|
||||
|
/// <summary>
|
||||
|
/// Handles the specified web authentication result.
|
||||
|
/// </summary>
|
||||
|
/// <param name="result">The web authentication result.</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="result"/> is <see langword="null"/>.</exception>
|
||||
|
internal Task HandleWebAuthenticationResultAsync(WebAuthenticationResult result, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
if (result is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(result)); |
||||
|
} |
||||
|
|
||||
|
return HandleRequestAsync(result, cancellationToken); |
||||
|
} |
||||
|
#endif
|
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Handles the request using the specified property.
|
||||
|
/// </summary>
|
||||
|
/// <param name="property">The property to add to the transaction.</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="property"/> is <see langword="null"/>.</exception>
|
||||
|
private async Task HandleRequestAsync<TProperty>(TProperty property, CancellationToken cancellationToken) where TProperty : class |
||||
|
{ |
||||
|
if (property is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(property)); |
||||
|
} |
||||
|
|
||||
|
cancellationToken.ThrowIfCancellationRequested(); |
||||
|
|
||||
|
var scope = _provider.CreateScope(); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>(); |
||||
|
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>(); |
||||
|
|
||||
|
// 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(); |
||||
|
transaction.SetProperty(typeof(TProperty).FullName!, property); |
||||
|
|
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Redirects a protocol activation to the specified instance.
|
||||
|
/// </summary>
|
||||
|
/// <param name="activation">The protocol activation to redirect.</param>
|
||||
|
/// <param name="identifier">The identifier of the target instance.</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>
|
||||
|
internal async Task RedirectProtocolActivationAsync( |
||||
|
OpenIddictClientSystemIntegrationActivation activation, |
||||
|
string identifier, CancellationToken cancellationToken = default) |
||||
|
{ |
||||
|
if (activation is null) |
||||
|
{ |
||||
|
throw new ArgumentNullException(nameof(activation)); |
||||
|
} |
||||
|
|
||||
|
if (string.IsNullOrEmpty(identifier)) |
||||
|
{ |
||||
|
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier)); |
||||
|
} |
||||
|
|
||||
|
using var buffer = new MemoryStream(); |
||||
|
using var writer = new BinaryWriter(buffer); |
||||
|
using var stream = new NamedPipeClientStream( |
||||
|
serverName : ".", |
||||
|
pipeName : $@"{_options.CurrentValue.PipeName}\{identifier}", |
||||
|
direction : PipeDirection.Out, |
||||
|
options : PipeOptions.Asynchronous, |
||||
|
impersonationLevel: TokenImpersonationLevel.None, |
||||
|
inheritability : HandleInheritability.None); |
||||
|
|
||||
|
// Wait for the target to accept the pipe connection.
|
||||
|
await stream.ConnectAsync(cancellationToken); |
||||
|
|
||||
|
// Write the type of message stored in the shared memory and the
|
||||
|
// version used to identify the binary serialization format.
|
||||
|
writer.Write(0x01); |
||||
|
writer.Write(0x01); |
||||
|
|
||||
|
// Write the protocol activation URI.
|
||||
|
writer.Write(activation.ActivationUri.AbsoluteUri); |
||||
|
|
||||
|
// Transfer the payload to the target.
|
||||
|
buffer.Seek(0L, SeekOrigin.Begin); |
||||
|
await buffer.CopyToAsync(stream, bufferSize: 81_920, cancellationToken); |
||||
|
} |
||||
|
} |
||||
@ -1,139 +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 System.IO.Pipes; |
|
||||
using OpenIddict.Client.Windows; |
|
||||
|
|
||||
namespace Microsoft.Extensions.DependencyInjection; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Exposes the necessary methods required to configure
|
|
||||
/// the OpenIddict client Windows integration.
|
|
||||
/// </summary>
|
|
||||
public sealed class OpenIddictClientWindowsBuilder |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Initializes a new instance of <see cref="OpenIddictClientWindowsBuilder"/>.
|
|
||||
/// </summary>
|
|
||||
/// <param name="services">The services collection.</param>
|
|
||||
public OpenIddictClientWindowsBuilder(IServiceCollection services) |
|
||||
=> Services = services ?? throw new ArgumentNullException(nameof(services)); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the services collection.
|
|
||||
/// </summary>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Never)] |
|
||||
public IServiceCollection Services { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Amends the default OpenIddict client Windows configuration.
|
|
||||
/// </summary>
|
|
||||
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
|
|
||||
/// <remarks>This extension can be safely called multiple times.</remarks>
|
|
||||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|
||||
public OpenIddictClientWindowsBuilder Configure(Action<OpenIddictClientWindowsOptions> configuration) |
|
||||
{ |
|
||||
if (configuration is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(configuration)); |
|
||||
} |
|
||||
|
|
||||
Services.Configure(configuration); |
|
||||
|
|
||||
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.
|
|
||||
/// </summary>
|
|
||||
/// <param name="timeout">The authentication timeout.</param>
|
|
||||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|
||||
public OpenIddictClientWindowsBuilder SetAuthenticationTimeout(TimeSpan timeout) |
|
||||
=> Configure(options => options.AuthenticationTimeout = timeout); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Sets the identifier used to represent the current application
|
|
||||
/// instance and redirect protocol activations when necessary.
|
|
||||
/// </summary>
|
|
||||
/// <param name="identifier">The identifier of the current instance.</param>
|
|
||||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|
||||
public OpenIddictClientWindowsBuilder SetInstanceIdentifier(string identifier) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(identifier)) |
|
||||
{ |
|
||||
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier)); |
|
||||
} |
|
||||
|
|
||||
return Configure(options => options.InstanceIdentifier = identifier); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Sets the base name of the pipe created by OpenIddict to enable
|
|
||||
/// inter-process communication and handle protocol activation redirections.
|
|
||||
/// </summary>
|
|
||||
/// <param name="name">The name of the pipe.</param>
|
|
||||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|
||||
public OpenIddictClientWindowsBuilder SetPipeName(string name) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(name)) |
|
||||
{ |
|
||||
throw new ArgumentException(SR.FormatID0366(nameof(name)), nameof(name)); |
|
||||
} |
|
||||
|
|
||||
return Configure(options => options.PipeName = name); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Sets the security policy applied to the pipe created by OpenIddict to enable
|
|
||||
/// inter-process communication and handle protocol activation redirections.
|
|
||||
/// </summary>
|
|
||||
/// <param name="security">The security policy applied to the pipe.</param>
|
|
||||
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|
||||
public OpenIddictClientWindowsBuilder SetPipeSecurity(PipeSecurity security) |
|
||||
{ |
|
||||
if (security is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(security)); |
|
||||
} |
|
||||
|
|
||||
return Configure(options => options.PipeSecurity = security); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Determines whether the specified object is equal to the current object.
|
|
||||
/// </summary>
|
|
||||
/// <param name="obj">The object to compare with the current object.</param>
|
|
||||
/// <returns><see langword="true"/> if the specified object is equal to the current object; otherwise, false.</returns>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Never)] |
|
||||
public override bool Equals(object? obj) => base.Equals(obj); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Serves as the default hash function.
|
|
||||
/// </summary>
|
|
||||
/// <returns>A hash code for the current object.</returns>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Never)] |
|
||||
public override int GetHashCode() => base.GetHashCode(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Returns a string that represents the current object.
|
|
||||
/// </summary>
|
|
||||
/// <returns>A string that represents the current object.</returns>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Never)] |
|
||||
public override string? ToString() => base.ToString(); |
|
||||
} |
|
||||
@ -1,128 +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 System.IO.Pipes; |
|
||||
using System.Runtime.CompilerServices; |
|
||||
using System.Security.AccessControl; |
|
||||
using System.Security.Principal; |
|
||||
using System.Text; |
|
||||
using Microsoft.Extensions.Hosting; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
using Microsoft.IdentityModel.Tokens; |
|
||||
using OpenIddict.Extensions; |
|
||||
|
|
||||
#if !SUPPORTS_HOST_ENVIRONMENT
|
|
||||
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment; |
|
||||
#endif
|
|
||||
|
|
||||
namespace OpenIddict.Client.Windows; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Contains the methods required to ensure that the OpenIddict client configuration is valid.
|
|
||||
/// </summary>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|
||||
public sealed class OpenIddictClientWindowsConfiguration : IConfigureOptions<OpenIddictClientOptions>, |
|
||||
IPostConfigureOptions<OpenIddictClientOptions>, |
|
||||
IPostConfigureOptions<OpenIddictClientWindowsOptions> |
|
||||
{ |
|
||||
private readonly IHostEnvironment _environment; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a new instance of the <see cref="OpenIddictClientWindowsConfiguration"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <param name="environment">The host environment.</param>
|
|
||||
public OpenIddictClientWindowsConfiguration(IHostEnvironment environment) |
|
||||
=> _environment = environment ?? throw new ArgumentNullException(nameof(environment)); |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void Configure(OpenIddictClientOptions options) |
|
||||
{ |
|
||||
if (options is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(options)); |
|
||||
} |
|
||||
|
|
||||
// Register the built-in event handlers used by the OpenIddict Windows client components.
|
|
||||
options.Handlers.AddRange(OpenIddictClientWindowsHandlers.DefaultHandlers); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PostConfigure(string? name, OpenIddictClientOptions options) |
|
||||
{ |
|
||||
if (options is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(options)); |
|
||||
} |
|
||||
|
|
||||
// Ensure an explicit client URI was set when using the Windows integration.
|
|
||||
if (options.ClientUri is not { IsAbsoluteUri: true }) |
|
||||
{ |
|
||||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0384)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
public void PostConfigure(string? name, OpenIddictClientWindowsOptions options) |
|
||||
{ |
|
||||
if (options is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(options)); |
|
||||
} |
|
||||
|
|
||||
// If no explicit instance identifier was specified, use a random GUID.
|
|
||||
if (string.IsNullOrEmpty(options.InstanceIdentifier)) |
|
||||
{ |
|
||||
options.InstanceIdentifier = Guid.NewGuid().ToString(); |
|
||||
} |
|
||||
|
|
||||
// If no explicit pipe name was specified, compute the SHA-256 hash of the
|
|
||||
// application name resolved from the host and use it as a unique identifier.
|
|
||||
//
|
|
||||
// Note: the pipe name is deliberately prefixed with "LOCAL\" to support
|
|
||||
// partial trust/sandboxed applications that are executed in an AppContainer
|
|
||||
// and cannot communicate with applications outside the sandbox container.
|
|
||||
if (string.IsNullOrEmpty(options.PipeName)) |
|
||||
{ |
|
||||
if (string.IsNullOrEmpty(_environment.ApplicationName)) |
|
||||
{ |
|
||||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0386)); |
|
||||
} |
|
||||
|
|
||||
options.PipeName = $@"LOCAL\OpenIddict.Client.Windows\{
|
|
||||
Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash( |
|
||||
Encoding.UTF8.GetBytes(_environment.ApplicationName))) |
|
||||
}";
|
|
||||
} |
|
||||
|
|
||||
// If no explicit pipe security policy was specified, grant the current user
|
|
||||
// full control over the created pipe and allow cross-process communication
|
|
||||
// between elevated and non-elevated processes. Note: if the process executes
|
|
||||
// inside an AppContainer, don't override the default OS pipe security policy
|
|
||||
// to allow all applications with the same identity to access the named pipe.
|
|
||||
if (options.PipeSecurity is null) |
|
||||
{ |
|
||||
using var identity = WindowsIdentity.GetCurrent(TokenAccessLevels.Query); |
|
||||
|
|
||||
if (!IsRunningInAppContainer(identity)) |
|
||||
{ |
|
||||
options.PipeSecurity = new PipeSecurity(); |
|
||||
options.PipeSecurity.SetOwner(identity.User!); |
|
||||
options.PipeSecurity.AddAccessRule(new PipeAccessRule(identity.User!, |
|
||||
PipeAccessRights.FullControl, AccessControlType.Allow)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
[MethodImpl(MethodImplOptions.NoInlining)] |
|
||||
static bool IsRunningInAppContainer(WindowsIdentity identity) |
|
||||
#if SUPPORTS_WINDOWS_RUNTIME
|
|
||||
=> OpenIddictClientWindowsHelpers.IsWindowsRuntimeSupported() && |
|
||||
OpenIddictClientWindowsHelpers.HasAppContainerToken(identity); |
|
||||
#else
|
|
||||
=> false; |
|
||||
#endif
|
|
||||
} |
|
||||
} |
|
||||
@ -1,68 +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.Windows; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Contains a collection of event handler filters commonly used by the Windows handlers.
|
|
||||
/// </summary>
|
|
||||
[EditorBrowsable(EditorBrowsableState.Advanced)] |
|
||||
public static class OpenIddictClientWindowsHandlerFilters |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Represents a filter that excludes the associated handlers
|
|
||||
/// if no explicit nonce was attached to the authentication context.
|
|
||||
/// </summary>
|
|
||||
public sealed class RequireAuthenticationNonce : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext> |
|
||||
{ |
|
||||
/// <inheritdoc/>
|
|
||||
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context) |
|
||||
{ |
|
||||
if (context is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(context)); |
|
||||
} |
|
||||
|
|
||||
return new(!string.IsNullOrEmpty(context.Nonce)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Represents a filter that excludes the associated handlers if no interactive user session was detected.
|
|
||||
/// </summary>
|
|
||||
public sealed class RequireInteractiveSession : IOpenIddictClientHandlerFilter<BaseContext> |
|
||||
{ |
|
||||
/// <inheritdoc/>
|
|
||||
public ValueTask<bool> IsActiveAsync(BaseContext context) |
|
||||
{ |
|
||||
if (context is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(context)); |
|
||||
} |
|
||||
|
|
||||
return new(Environment.UserInteractive); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Represents a filter that excludes the associated handlers if no Windows activation was found.
|
|
||||
/// </summary>
|
|
||||
public sealed class RequireWindowsActivation : IOpenIddictClientHandlerFilter<BaseContext> |
|
||||
{ |
|
||||
/// <inheritdoc/>
|
|
||||
public ValueTask<bool> IsActiveAsync(BaseContext context) |
|
||||
{ |
|
||||
if (context is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(context)); |
|
||||
} |
|
||||
|
|
||||
return new(context.Transaction.GetWindowsActivation() is not null); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,125 +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 System.IO.Pipes; |
|
||||
using Microsoft.Extensions.Hosting; |
|
||||
using Microsoft.Extensions.Logging; |
|
||||
using Microsoft.Extensions.Options; |
|
||||
|
|
||||
namespace OpenIddict.Client.Windows; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Contains the logic necessary to handle URI protocol activations that
|
|
||||
/// are redirected by other instances using inter-process communication.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// 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 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, |
|
||||
OpenIddictClientWindowsService service) |
|
||||
{ |
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); |
|
||||
_options = options ?? throw new ArgumentNullException(nameof(options)); |
|
||||
_service = service ?? throw new ArgumentNullException(nameof(service)); |
|
||||
} |
|
||||
|
|
||||
/// <inheritdoc/>
|
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) |
|
||||
{ |
|
||||
do |
|
||||
{ |
|
||||
try |
|
||||
{ |
|
||||
using var buffer = new MemoryStream(); |
|
||||
using var reader = new BinaryReader(buffer); |
|
||||
|
|
||||
#if SUPPORTS_NAMED_PIPE_CONSTRUCTOR_WITH_ACL
|
|
||||
using var stream = new NamedPipeServerStream( |
|
||||
#elif SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL
|
|
||||
using var stream = NamedPipeServerStreamAcl.Create( |
|
||||
#else
|
|
||||
using var stream = NamedPipeServerStreamConstructors.New( |
|
||||
#endif
|
|
||||
pipeName : $@"{_options.CurrentValue.PipeName}\{_options.CurrentValue.InstanceIdentifier}", |
|
||||
direction : PipeDirection.In, |
|
||||
maxNumberOfServerInstances: 1, |
|
||||
transmissionMode : PipeTransmissionMode.Message, |
|
||||
options : PipeOptions.Asynchronous, |
|
||||
inBufferSize : 0, |
|
||||
outBufferSize : 0, |
|
||||
pipeSecurity : _options.CurrentValue.PipeSecurity, |
|
||||
inheritability : HandleInheritability.None, |
|
||||
additionalAccessRights : default); |
|
||||
|
|
||||
// Wait for a writer to connect to the named pipe.
|
|
||||
await stream.WaitForConnectionAsync(stoppingToken); |
|
||||
|
|
||||
// Copy the content to the memory stream asynchronously and rewind it.
|
|
||||
await stream.CopyToAsync(buffer, bufferSize: 81_920, stoppingToken); |
|
||||
buffer.Seek(0L, SeekOrigin.Begin); |
|
||||
|
|
||||
// Process the inter-process notification based on its declared type.
|
|
||||
await (reader.ReadInt32() switch |
|
||||
{ |
|
||||
0x01 when GetProtocolActivation(reader) is OpenIddictClientWindowsActivation activation |
|
||||
=> _service.HandleProtocolActivationAsync(activation, stoppingToken), |
|
||||
|
|
||||
var value => throw new InvalidOperationException(SR.FormatID0387(value)) |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
// Ignore operation canceled exceptions when the host is shutting down.
|
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
// Swallow all exceptions to ensure the service doesn't exit when encountering an exception.
|
|
||||
catch (Exception exception) |
|
||||
{ |
|
||||
_logger.LogWarning(exception, SR.GetResourceString(SR.ID6213)); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
while (!stoppingToken.IsCancellationRequested); |
|
||||
|
|
||||
static OpenIddictClientWindowsActivation GetProtocolActivation(BinaryReader reader) |
|
||||
{ |
|
||||
// Ensure the binary serialization format is supported.
|
|
||||
var version = reader.ReadInt32(); |
|
||||
if (version is not 0x01) |
|
||||
{ |
|
||||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0388)); |
|
||||
} |
|
||||
|
|
||||
var value = reader.ReadString(); |
|
||||
if (string.IsNullOrEmpty(value) || !Uri.TryCreate(value, UriKind.Absolute, out Uri? uri)) |
|
||||
{ |
|
||||
throw new InvalidOperationException(SR.GetResourceString(SR.ID0388)); |
|
||||
} |
|
||||
|
|
||||
return new OpenIddictClientWindowsActivation(uri) |
|
||||
{ |
|
||||
IsActivationRedirected = true |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,56 +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.IO.Pipes; |
|
||||
|
|
||||
#if !SUPPORTS_HOST_ENVIRONMENT
|
|
||||
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment; |
|
||||
#endif
|
|
||||
|
|
||||
namespace OpenIddict.Client.Windows; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Provides various settings needed to configure the OpenIddict Windows client integration.
|
|
||||
/// </summary>
|
|
||||
public sealed class OpenIddictClientWindowsOptions |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Gets or sets the timeout after which authentication demands
|
|
||||
/// that are not completed are automatically aborted by OpenIddict.
|
|
||||
/// </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.
|
|
||||
/// </summary>
|
|
||||
public string? InstanceIdentifier { get; set; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the base name of the pipe created by OpenIddict to enable
|
|
||||
/// inter-process communication and handle protocol activation redirections.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// If no value is explicitly set, a default name is automatically computed.
|
|
||||
/// </remarks>
|
|
||||
public string PipeName { get; set; } = default!; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets or sets the security policy applied to the pipe created by OpenIddict
|
|
||||
/// to enable inter-process communication and handle protocol activation redirections.
|
|
||||
/// </summary>
|
|
||||
/// <remarks>
|
|
||||
/// If no value is explicitly set, a default policy is automatically created
|
|
||||
/// (unless the application is running inside an AppContainer sandbox).
|
|
||||
/// </remarks>
|
|
||||
public PipeSecurity PipeSecurity { get; set; } = default!; |
|
||||
} |
|
||||
@ -1,92 +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.DependencyInjection; |
|
||||
|
|
||||
namespace OpenIddict.Client.Windows; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// 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>
|
|
||||
public sealed class OpenIddictClientWindowsService |
|
||||
{ |
|
||||
private readonly IServiceProvider _provider; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Creates a new instance of the <see cref="OpenIddictClientWindowsService"/> class.
|
|
||||
/// </summary>
|
|
||||
/// <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)); |
|
||||
|
|
||||
/// <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) |
|
||||
{ |
|
||||
if (activation is null) |
|
||||
{ |
|
||||
throw new ArgumentNullException(nameof(activation)); |
|
||||
} |
|
||||
|
|
||||
cancellationToken.ThrowIfCancellationRequested(); |
|
||||
|
|
||||
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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue