/* * 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.AccessControl; using System.Security.Principal; using System.Text; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using static OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrationAuthenticationMode; namespace OpenIddict.Client.SystemIntegration; /// /// Contains the methods required to ensure that the OpenIddict client system integration configuration is valid. /// [EditorBrowsable(EditorBrowsableState.Advanced)] public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureOptions, IPostConfigureOptions, IPostConfigureOptions { private readonly IHostEnvironment _environment; /// /// Creates a new instance of the class. /// /// The host environment. public OpenIddictClientSystemIntegrationConfiguration(IHostEnvironment environment) => _environment = environment ?? throw new ArgumentNullException(nameof(environment)); /// public void Configure(OpenIddictClientOptions options) { ArgumentNullException.ThrowIfNull(options); // Register the built-in event handlers used by the OpenIddict client system integration components. options.Handlers.AddRange(OpenIddictClientSystemIntegrationHandlers.DefaultHandlers); // Enable response_mode=fragment support by default. options.ResponseModes.Add(ResponseModes.Fragment); } /// public void PostConfigure(string? name, OpenIddictClientOptions options) { ArgumentNullException.ThrowIfNull(options); // If no explicit client URI was set, default to the static "http://localhost/" address, which is // adequate for a native/mobile client and points to the embedded web server when it is enabled. options.ClientUri ??= new Uri("http://localhost/", UriKind.Absolute); } /// public void PostConfigure(string? name, OpenIddictClientSystemIntegrationOptions options) { ArgumentNullException.ThrowIfNull(options); // Ensure the operating system version is supported. if ((OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(21)) || (OperatingSystem.IsIOS() && !OperatingSystem.IsIOSVersionAtLeast(12)) || OperatingSystem.IsLinux() || (OperatingSystem.IsMacCatalyst() && !OperatingSystem.IsMacCatalystVersionAtLeast(13, 1)) || (OperatingSystem.IsMacOS() && !OperatingSystem.IsMacOSVersionAtLeast(10, 15)) || (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(7))) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); } #if !SUPPORTS_ANDROID // When running on Android, iOS or Mac Catalyst, ensure the version compiled for these platforms // is used to prevent the generic/non-OS specific TFM from being used as launching the system // browser cannot be done using Process.Start() and requires using OS-specific APIs that are // not available on the portable version of the OpenIddict.Client.SystemIntegration package. if (OperatingSystem.IsAndroid()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); } #endif #if !SUPPORTS_UIKIT if (OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); } #endif #pragma warning disable CA1416 // If explicitly set, ensure the specified authentication mode is supported. if (options.AuthenticationMode is ASWebAuthenticationSession && !IsASWebAuthenticationSessionSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446)); } else if (options.AuthenticationMode is CustomTabsIntent && !IsCustomTabsIntentSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0452)); } else if (options.AuthenticationMode is WebAuthenticationBroker && !IsWebAuthenticationBrokerSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); } #pragma warning restore CA1416 // When possible, always prefer OS-managed modes. Otherwise, fall back to the system browser. options.AuthenticationMode ??= IsASWebAuthenticationSessionSupported() ? ASWebAuthenticationSession : IsCustomTabsIntentSupported() ? CustomTabsIntent : SystemBrowser; if (!OperatingSystem.IsAndroid() && !OperatingSystem.IsIOS() && !OperatingSystem.IsMacCatalyst() && !OperatingSystem.IsMacOS()) { options.EnableActivationHandling ??= true; options.EnableActivationRedirection ??= true; options.EnablePipeServer ??= true; options.EnableEmbeddedWebServer ??= HttpListener.IsSupported; } else { options.EnableActivationHandling ??= false; options.EnableActivationRedirection ??= false; options.EnablePipeServer ??= false; options.EnableEmbeddedWebServer ??= false; } // If no explicit application discriminator 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.ApplicationDiscriminator)) { if (string.IsNullOrEmpty(_environment.ApplicationName)) { throw new InvalidOperationException(SR.GetResourceString(SR.ID0386)); } var digest = OpenIddictHelpers.ComputeSha256Hash(Encoding.UTF8.GetBytes(_environment.ApplicationName)); // Note: only the left-most half of the hash is used to limit the length of the resulting discriminator, // which is required on platforms like macOS, where the name of pipes is always prefixed with a static part // (e.g /var/folders/5j/jjxtct5j1gvg35z6sdh2fz0w0000gn/T/CoreFxPipe_) and must not exceed 104 characters. options.ApplicationDiscriminator = Base64UrlEncoder.Encode(digest, 0, digest.Length / 2); } // If no explicit instance identifier was specified, use a 96-bit random identifier. if (string.IsNullOrEmpty(options.InstanceIdentifier)) { options.InstanceIdentifier = Base64UrlEncoder.Encode(OpenIddictHelpers.CreateRandomArray(size: 96)); } // If no explicit pipe name was specified, build one using the application discriminator. if (string.IsNullOrEmpty(options.PipeName)) { // 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. options.PipeName = OperatingSystem.IsWindows() ? @$"LOCAL\{options.ApplicationDiscriminator}" : options.ApplicationDiscriminator; } #if SUPPORTS_CURRENT_USER_ONLY_PIPE_OPTION if (options.PipeOptions is null && !OperatingSystem.IsWindows()) { // 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; // On 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 (OperatingSystem.IsWindows() && options.PipeSecurity is null) { using var identity = WindowsIdentity.GetCurrent(TokenAccessLevels.Query); if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 10240) || !HasAppContainerToken(identity)) { options.PipeSecurity = new PipeSecurity(); options.PipeSecurity.SetOwner(identity.User!); options.PipeSecurity.AddAccessRule(new PipeAccessRule(identity.User!, PipeAccessRights.FullControl, AccessControlType.Allow)); } } } }