diff --git a/Directory.Build.targets b/Directory.Build.targets index 03ab2b05..a79b954e 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -55,7 +55,9 @@ Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.1'))) Or ('$(TargetFrameworkIdentifier)' == '.NETStandard' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.1'))) "> $(DefineConstants);SUPPORTS_BROTLI_COMPRESSION + $(DefineConstants);SUPPORTS_CURRENT_USER_ONLY_PIPE_OPTION $(DefineConstants);SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS + $(DefineConstants);SUPPORTS_STREAM_MEMORY_METHODS $(DefineConstants);SUPPORTS_TIME_CONSTANT_COMPARISONS @@ -97,6 +99,7 @@ $(DefineConstants);SUPPORTS_DIRECT_JSON_ELEMENT_SERIALIZATION $(DefineConstants);SUPPORTS_JSON_NODES $(DefineConstants);SUPPORTS_ONE_SHOT_RANDOM_NUMBER_GENERATOR_METHODS + $(DefineConstants);SUPPORTS_TASK_WAIT_ASYNC $(DefineConstants);SUPPORTS_ZLIB_COMPRESSION diff --git a/OpenIddict.sln b/OpenIddict.sln index f16fd052..8139285f 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -147,7 +147,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.Owin.Inte EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Server.DataProtection.Tests", "test\OpenIddict.Server.DataProtection.Tests\OpenIddict.Server.DataProtection.Tests.csproj", "{C92838AB-3923-49A1-B23E-FA01306CAC9D}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.Windows", "src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj", "{058BF159-F3DB-4BCD-99E5-2E5C62D8F588}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Client.SystemIntegration", "src\OpenIddict.Client.SystemIntegration\OpenIddict.Client.SystemIntegration.csproj", "{058BF159-F3DB-4BCD-99E5-2E5C62D8F588}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenIddict.Sandbox.Wpf.Client", "sandbox\OpenIddict.Sandbox.Wpf.Client\OpenIddict.Sandbox.Wpf.Client.csproj", "{D0C56832-6557-4CC1-91EA-7B31B18BEE7A}" EndProject diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config b/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config index c01219fd..913c4bd7 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config +++ b/sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config @@ -82,6 +82,12 @@ + + + + + + @@ -148,6 +154,12 @@ + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config b/sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config index 8a8c9968..944a71c0 100644 --- a/sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config +++ b/sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config @@ -106,6 +106,12 @@ + + + + + + @@ -172,6 +178,12 @@ + + + + + + diff --git a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs index 9c4e3903..21eebe48 100644 --- a/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs +++ b/sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs @@ -39,7 +39,17 @@ public class Worker : IHostedService }, RedirectUris = { - new Uri("openiddict-sandbox-console-client://localhost/callback/login/local") + new Uri("http://localhost:49152/callback/login/local"), + new Uri("http://localhost:49153/callback/login/local"), + new Uri("http://localhost:49154/callback/login/local"), + new Uri("http://localhost:49155/callback/login/local"), + new Uri("http://localhost:49156/callback/login/local"), + new Uri("http://localhost:49157/callback/login/local"), + new Uri("http://localhost:49158/callback/login/local"), + new Uri("http://localhost:49159/callback/login/local"), + new Uri("http://localhost:49160/callback/login/local"), + new Uri("http://localhost:49161/callback/login/local"), + new Uri("http://localhost:49162/callback/login/local") }, Permissions = { diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs index a403b6f7..e89b2af5 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs @@ -2,10 +2,11 @@ using OpenIddict.Client; using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictExceptions; -using static System.Console; namespace OpenIddict.Sandbox.Console.Client; +using Console = System.Console; + public class InteractiveService : BackgroundService { private readonly IHostApplicationLifetime _lifetime; @@ -28,54 +29,49 @@ public class InteractiveService : BackgroundService await source.Task; } - string? provider; - while (!stoppingToken.IsCancellationRequested) { + string? provider; + do { - await Out.WriteLineAsync("Type '1' + ENTER to log in using the local server or '2' + ENTER to log in using Twitter"); + Console.WriteLine("Type '1' + ENTER to log in using the local server or '2' + ENTER to log in using Twitter"); - provider = await In.ReadLineAsync(stoppingToken) switch + provider = await Task.Run(Console.ReadLine).WaitAsync(stoppingToken) switch { "1" => "Local", "2" => "Twitter", - _ => null + _ => null }; } while (string.IsNullOrEmpty(provider)); - await Out.WriteLineAsync("Launching the system browser."); + Console.WriteLine("Launching the system browser."); try { - // Ask OpenIddict to initiate the challenge and launch the system browser - // to allow the user to complete the interactive authentication dance. - var nonce = await _service.ChallengeWithBrowserAsync( + // Ask OpenIddict to initiate the authentication flow (typically, by + // starting the system browser) and wait for the user to complete it. + var (_, _, principal) = await _service.AuthenticateInteractivelyAsync( provider, cancellationToken: stoppingToken); - // Wait until the user approved or rejected the authorization - // demand and retrieve the resulting claims-based principal. - var (_, _, principal) = await _service.AuthenticateWithBrowserAsync( - nonce, cancellationToken: stoppingToken); - - await Out.WriteLineAsync($"Welcome, {principal.FindFirst(Claims.Name)!.Value}."); + Console.WriteLine($"Welcome, {principal.FindFirst(Claims.Name)!.Value}."); } catch (OperationCanceledException) { - await Error.WriteLineAsync("The authentication process was aborted."); + Console.WriteLine("The authentication process was aborted."); } catch (ProtocolException exception) when (exception.Error is Errors.AccessDenied) { - await Error.WriteLineAsync("The authorization was denied by the end user."); + Console.WriteLine("The authorization was denied by the end user."); } catch { - await Error.WriteLineAsync("An error occurred while trying to authenticate the user."); + Console.WriteLine("An error occurred while trying to authenticate the user."); } } } diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj b/sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj index a57b4b55..fadc4aff 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj @@ -2,16 +2,16 @@ Exe - net7.0-windows + net7.0 true false false + - diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs b/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs index 3aca489f..594685b4 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/Program.cs @@ -1,19 +1,19 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenIddict.Client; using OpenIddict.Sandbox.Console.Client; using static OpenIddict.Abstractions.OpenIddictConstants; -var host = Host.CreateDefaultBuilder(args) +var host = new HostBuilder() // Note: applications for which a single instance is preferred can reference // the Dapplo.Microsoft.Extensions.Hosting.AppServices package and call this // method to automatically close extra instances based on the specified identifier: // // .ConfigureSingleInstance(options => options.MutexId = "{802A478D-00E8-4DAE-9A27-27B31A47CB39}") // + .ConfigureLogging(options => options.AddDebug()) .ConfigureServices(services => { services.AddDbContext(options => @@ -46,11 +46,16 @@ var host = Host.CreateDefaultBuilder(args) options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); - // Register the Windows host. - options.UseWindows(); + // Add the operating system integration. + options.UseSystemIntegration() + .DisableActivationHandling() + .DisableActivationRedirection() + .DisablePipeServer() + .EnableEmbeddedWebServer() + .UseSystemBrowser(); // Set the client URI that will uniquely identify this application. - options.SetClientUri(new Uri("openiddict-sandbox-console-client://localhost/", UriKind.Absolute)); + options.SetClientUri(new Uri("http://localhost/", UriKind.Absolute)); // Register the System.Net.Http integration and use the identity of the current // assembly as a more specific user agent, which can be useful when dealing with @@ -90,8 +95,6 @@ var host = Host.CreateDefaultBuilder(args) // Register the background service responsible for handling the console interactions. services.AddHostedService(); - - services.RemoveAll(); }) .UseConsoleLifetime() .Build(); diff --git a/sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs b/sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs index 982dffae..304deab8 100644 --- a/sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs +++ b/sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Win32; namespace OpenIddict.Sandbox.Console.Client; @@ -18,39 +17,6 @@ public class Worker : IHostedService var context = scope.ServiceProvider.GetRequiredService(); await context.Database.EnsureCreatedAsync(); - - RegistryKey? root = null; - - // Create the registry entries necessary to handle URI protocol activations. - // Note: the application MUST be run once as an administrator for this to work, - // so this should typically be done by a dedicated installer or a setup script. - // Alternatively, the application can be packaged and use windows.protocol to - // register the protocol handler/custom URI scheme with the operation system. - try - { - root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-console-client"); - - if (root is null) - { - root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-console-client"); - root.SetValue(string.Empty, "URL:openiddict-sandbox-console-client"); - root.SetValue("URL Protocol", string.Empty); - - using var command = root.CreateSubKey("shell\\open\\command"); - command.SetValue(string.Empty, string.Format("\"{0}\" \"%1\"", -#if SUPPORTS_ENVIRONMENT_PROCESS_PATH - Environment.ProcessPath -#else - Process.GetCurrentProcess().MainModule.FileName -#endif - )); - } - } - - finally - { - root?.Dispose(); - } } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; diff --git a/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs b/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs index e1a6210d..140dc62b 100644 --- a/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs +++ b/sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs @@ -29,16 +29,11 @@ namespace OpenIddict.Sandbox.WinForms.Client try { - // Ask OpenIddict to initiate the challenge and launch the system browser - // to allow the user to complete the interactive authentication dance. - var nonce = await _service.ChallengeWithBrowserAsync( + // Ask OpenIddict to initiate the authentication flow (typically, by + // starting the system browser) and wait for the user to complete it. + var (_, _, principal) = await _service.AuthenticateInteractivelyAsync( provider, cancellationToken: source.Token); - // Wait until the user approved or rejected the authorization - // demand and retrieve the resulting claims-based principal. - var (_, _, principal) = await _service.AuthenticateWithBrowserAsync( - nonce, cancellationToken: source.Token); - #if SUPPORTS_WINFORMS_TASK_DIALOG TaskDialog.ShowDialog(new TaskDialogPage { diff --git a/sandbox/OpenIddict.Sandbox.WinForms.Client/OpenIddict.Sandbox.WinForms.Client.csproj b/sandbox/OpenIddict.Sandbox.WinForms.Client/OpenIddict.Sandbox.WinForms.Client.csproj index 802e1f8f..516d4e5d 100644 --- a/sandbox/OpenIddict.Sandbox.WinForms.Client/OpenIddict.Sandbox.WinForms.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.WinForms.Client/OpenIddict.Sandbox.WinForms.Client.csproj @@ -11,9 +11,9 @@ + - diff --git a/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs b/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs index a828c27f..fff24bd6 100644 --- a/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs @@ -2,17 +2,19 @@ using Dapplo.Microsoft.Extensions.Hosting.WinForms; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using OpenIddict.Client; using OpenIddict.Sandbox.WinForms.Client; using static OpenIddict.Abstractions.OpenIddictConstants; -var host = Host.CreateDefaultBuilder(args) +var host = new HostBuilder() // Note: applications for which a single instance is preferred can reference // the Dapplo.Microsoft.Extensions.Hosting.AppServices package and call this // method to automatically close extra instances based on the specified identifier: // // .ConfigureSingleInstance(options => options.MutexId = "{D6FEAFC8-3079-4881-B9F2-0B78EAF38B85}") // + .ConfigureLogging(options => options.AddDebug()) .ConfigureServices(services => { services.AddDbContext(options => @@ -45,8 +47,8 @@ var host = Host.CreateDefaultBuilder(args) options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); - // Register the Windows host. - options.UseWindows(); + // Add the operating system integration. + options.UseSystemIntegration(); // Set the client URI that will uniquely identify this application. options.SetClientUri(new Uri("openiddict-sandbox-winforms-client://localhost/", UriKind.Absolute)); diff --git a/sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs b/sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs index 6c17863b..a550dc18 100644 --- a/sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs +++ b/sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs @@ -30,16 +30,11 @@ namespace OpenIddict.Sandbox.Wpf.Client try { - // Ask OpenIddict to initiate the challenge and launch the system browser - // to allow the user to complete the interactive authentication dance. - var nonce = await _service.ChallengeWithBrowserAsync( + // Ask OpenIddict to initiate the authentication flow (typically, by + // starting the system browser) and wait for the user to complete it. + var (_, _, principal) = await _service.AuthenticateInteractivelyAsync( provider, cancellationToken: source.Token); - // Wait until the user approved or rejected the authorization - // demand and retrieve the resulting claims-based principal. - var (_, _, principal) = await _service.AuthenticateWithBrowserAsync( - nonce, cancellationToken: source.Token); - MessageBox.Show($"Welcome, {principal.FindFirst(Claims.Name)!.Value}.", "Authentication successful", MessageBoxButton.OK, MessageBoxImage.Information); } diff --git a/sandbox/OpenIddict.Sandbox.Wpf.Client/OpenIddict.Sandbox.Wpf.Client.csproj b/sandbox/OpenIddict.Sandbox.Wpf.Client/OpenIddict.Sandbox.Wpf.Client.csproj index 81dcf221..67baa904 100644 --- a/sandbox/OpenIddict.Sandbox.Wpf.Client/OpenIddict.Sandbox.Wpf.Client.csproj +++ b/sandbox/OpenIddict.Sandbox.Wpf.Client/OpenIddict.Sandbox.Wpf.Client.csproj @@ -12,9 +12,9 @@ + - diff --git a/sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs b/sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs index 336bba81..b3ec45e4 100644 --- a/sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs +++ b/sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs @@ -3,17 +3,19 @@ using Dapplo.Microsoft.Extensions.Hosting.Wpf; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using OpenIddict.Client; using OpenIddict.Sandbox.Wpf.Client; using static OpenIddict.Abstractions.OpenIddictConstants; -var host = Host.CreateDefaultBuilder(args) +var host = new HostBuilder() // Note: applications for which a single instance is preferred can reference // the Dapplo.Microsoft.Extensions.Hosting.AppServices package and call this // method to automatically close extra instances based on the specified identifier: // // .ConfigureSingleInstance(options => options.MutexId = "{C587B9EA-A870-4CF3-8B00-33DF67FCA143}") // + .ConfigureLogging(options => options.AddDebug()) .ConfigureServices(services => { services.AddDbContext(options => @@ -46,8 +48,8 @@ var host = Host.CreateDefaultBuilder(args) options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate(); - // Register the Windows host. - options.UseWindows(); + // Add the operating system integration. + options.UseSystemIntegration(); // Set the client URI that will uniquely identify this application. options.SetClientUri(new Uri("openiddict-sandbox-wpf-client://localhost/", UriKind.Absolute)); diff --git a/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs b/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs index 4f0118ed..ca70d965 100644 --- a/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs +++ b/shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using System.Data; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Security.Claims; @@ -79,6 +80,77 @@ internal static class OpenIddictHelpers } } +#if !SUPPORTS_TASK_WAIT_ASYNC + /// + /// Waits until the specified task returns a result or the cancellation token is signaled. + /// + /// The task. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// The specified is signaled. + public static async Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + var source = new TaskCompletionSource(TaskCreationOptions.None); + + using (cancellationToken.Register(static state => ((TaskCompletionSource) state!).SetResult(true), source)) + { + if (await Task.WhenAny(task, source.Task) == source.Task) + { + throw new OperationCanceledException(cancellationToken); + } + + await task; + } + } + + /// + /// Waits until the specified task returns a result or the cancellation token is signaled. + /// + /// The task. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// The specified is signaled. + public static async Task WaitAsync(this Task task, CancellationToken cancellationToken) + { + var source = new TaskCompletionSource(TaskCreationOptions.None); + + using (cancellationToken.Register(static state => ((TaskCompletionSource) state!).SetResult(true), source)) + { + if (await Task.WhenAny(task, source.Task) == source.Task) + { + throw new OperationCanceledException(cancellationToken); + } + + return await task; + } + } +#endif + + /// + /// Determines whether the specified is considered fatal. + /// + /// The exception. + /// + /// if the exception is considered fatal, otherwise. + /// + public static bool IsFatal(Exception exception) + { + RuntimeHelpers.EnsureSufficientExecutionStack(); + + return exception switch + { + ThreadAbortException => true, + OutOfMemoryException and not InsufficientMemoryException => true, + + AggregateException { InnerExceptions: var exceptions } => exceptions.Any(IsFatal), + Exception { InnerException: Exception inner } => IsFatal(inner), + + _ => false + }; + } + #if !SUPPORTS_TOHASHSET_LINQ_EXTENSION /// /// Creates a new instance and imports the elements present in the specified source. diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 073a7aba..0df308ed 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1424,7 +1424,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId An error occurred while authenticating the user. - The Windows protocol activation cannot be resolved from the client transaction. + The protocol activation cannot be resolved or contains invalid data. The identifier of the application instance that initiated the authentication process cannot be resolved from the state token. @@ -1451,13 +1451,13 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId An error occurred while waiting for the authentication operation to complete, which may indicate a nonce collision. Make sure nonces are unique, contain enough entropy and are generated using a crypto-secure random number generator. - An explicit client URI must be set when using the OpenIddict client Windows integration. To set the client URI, use 'services.AddOpenIddict().AddClient().SetClientUri()'. + An explicit client URI must be set when using the OpenIddict client system integration. To set the client URI, use 'services.AddOpenIddict().AddClient().SetClientUri()'. The default system browser couldn't be started. If the application executes inside a sandbox, make sure it is allowed to launch URIs or spawn new processes. - A pipe name must be manually set in the OpenIddict Windows integration options when no application name was configured in the .NET generic host options. To set the pipe name, call 'services.AddOpenIddict().AddClient().UseWindows().SetPipeName()'. + A pipe name must be manually set in the OpenIddict client system integration options when no application name was configured in the .NET generic host options. To set the pipe name, call 'services.AddOpenIddict().AddClient().UseOperatingSystemIntegration().SetPipeName()'. The type extracted from the inter-process notification ({0}) is unknown or not valid, which may indicate that different versions of the OpenIddict client are used for the same application. @@ -1466,7 +1466,19 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId The payload extracted from the inter-process notification is malformed, incomplete or was created by a different version of the OpenIddict client library. - The OpenIddict client Windows integration is not supported on this platform. + The OpenIddict client system integration is not supported on this platform. + + + The HTTP listener context cannot be resolved or contains invalid data. + + + An error occurred while instantiating the embedded web server, which may indicate a permission issue preventing the ports in the IANA dynamic ports range from being allocated. + + + The web authentication broker is not supported on this platform. + + + The web authentication result cannot be resolved or contains invalid data. The security token is missing. @@ -1963,6 +1975,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId The issuer returned in the server configuration doesn't match the value set in the client registration options. + + The received authorization response is not valid for this instance of the application. + The '{0}' parameter shouldn't be null or empty at this point. @@ -2626,6 +2641,12 @@ This may indicate that the hashed entry is corrupted or malformed. An error occurred while handling an inter-process message. + + An error occurred while handling an HTTP listener request. + + + An error occurred while redirecting a protocol activation to the '{Identifier}' instance. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Client.Windows/OpenIddict.Client.Windows.csproj b/src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj similarity index 76% rename from src/OpenIddict.Client.Windows/OpenIddict.Client.Windows.csproj rename to src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj index 868626e5..fc62d164 100644 --- a/src/OpenIddict.Client.Windows/OpenIddict.Client.Windows.csproj +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj @@ -4,8 +4,10 @@ net461; netcoreapp3.1; + net6.0; net6.0-windows7.0; net6.0-windows10.0.17763; + net7.0; net7.0-windows7.0; net7.0-windows10.0.17763; netstandard2.0 @@ -16,8 +18,8 @@ - Windows integration package for the OpenIddict client services. - $(PackageTags);client;windows + Operating system integration package for the OpenIddict client. + $(PackageTags);client;linux;windows @@ -52,8 +54,14 @@ - - + + + + + + + + diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivation.cs similarity index 83% rename from src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs rename to src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivation.cs index 920e0478..9174a76d 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivation.cs @@ -6,20 +6,20 @@ using System.ComponentModel; -namespace OpenIddict.Client.Windows; +namespace OpenIddict.Client.SystemIntegration; /// -/// Represents a Windows protocol activation. +/// Represents a protocol activation. /// [EditorBrowsable(EditorBrowsableState.Advanced)] -public sealed class OpenIddictClientWindowsActivation +public sealed class OpenIddictClientSystemIntegrationActivation { /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// /// The protocol activation URI. /// is . - public OpenIddictClientWindowsActivation(Uri uri) + public OpenIddictClientSystemIntegrationActivation(Uri uri) { if (uri is null) { diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivationHandler.cs similarity index 64% rename from src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs rename to src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivationHandler.cs index 427aad67..ab070ce0 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivationHandler.cs @@ -4,34 +4,33 @@ * the license and the contributors participating to this project. */ -using System; using System.ComponentModel; using System.Runtime.CompilerServices; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; -namespace OpenIddict.Client.Windows; +namespace OpenIddict.Client.SystemIntegration; /// /// Contains the logic necessary to handle initial URI protocol activations. /// /// -/// Note: redirected URI protocol activations are handled by . +/// Note: redirected URI protocol activations are handled by . /// [EditorBrowsable(EditorBrowsableState.Never)] -public sealed class OpenIddictClientWindowsHandler : IHostedService +public sealed class OpenIddictClientSystemIntegrationActivationHandler : IHostedService { - private readonly IOptionsMonitor _options; - private readonly OpenIddictClientWindowsService _service; + private readonly IOptionsMonitor _options; + private readonly OpenIddictClientSystemIntegrationService _service; /// - /// Creates a new instance of the class. + /// Creates a new instance of the class. /// - /// The OpenIddict client Windows integration options. - /// The OpenIddict client Windows service. - public OpenIddictClientWindowsHandler( - IOptionsMonitor options, - OpenIddictClientWindowsService service) + /// The OpenIddict client system integration integration options. + /// The OpenIddict client system integration service. + public OpenIddictClientSystemIntegrationActivationHandler( + IOptionsMonitor options, + OpenIddictClientSystemIntegrationService service) { _options = options ?? throw new ArgumentNullException(nameof(options)); _service = service ?? throw new ArgumentNullException(nameof(service)); @@ -53,15 +52,15 @@ public sealed class OpenIddictClientWindowsHandler : IHostedService return Task.FromCanceled(cancellationToken); } - // If the default activation processing logic was disabled in the options, ignore the activation. - if (_options.CurrentValue.DisableProtocolActivationProcessing) + // If the protocol activation processing logic was not enabled, ignore the activation. + if (_options.CurrentValue.EnableActivationHandling is not true) { return Task.CompletedTask; } // Determine whether the current instance is initialized to react to a protocol activation. // If it's not, return immediately to avoid adding latency to the application startup process. - if (GetProtocolActivation() is not OpenIddictClientWindowsActivation activation) + if (GetProtocolActivation() is not OpenIddictClientSystemIntegrationActivation activation) { return Task.CompletedTask; } @@ -69,21 +68,21 @@ public sealed class OpenIddictClientWindowsHandler : IHostedService return _service.HandleProtocolActivationAsync(activation, cancellationToken); [MethodImpl(MethodImplOptions.NoInlining)] - static OpenIddictClientWindowsActivation? GetProtocolActivation() + static OpenIddictClientSystemIntegrationActivation? GetProtocolActivation() { #if SUPPORTS_WINDOWS_RUNTIME // On platforms that support WinRT, always favor the AppInstance.GetActivatedEventArgs() API. - if (OpenIddictClientWindowsHelpers.IsWindowsRuntimeSupported() && - OpenIddictClientWindowsHelpers.GetProtocolActivationUriWithWindowsRuntime() is Uri uri) + if (OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported() && + OpenIddictClientSystemIntegrationHelpers.GetProtocolActivationUriWithWindowsRuntime() is Uri uri) { - return new OpenIddictClientWindowsActivation(uri); + return new OpenIddictClientSystemIntegrationActivation(uri); } #endif // Otherwise, try to extract the protocol activation from the command line arguments. - if (OpenIddictClientWindowsHelpers.GetProtocolActivationUriFromCommandLineArguments( + if (OpenIddictClientSystemIntegrationHelpers.GetProtocolActivationUriFromCommandLineArguments( Environment.GetCommandLineArgs()) is Uri value) { - return new OpenIddictClientWindowsActivation(value); + return new OpenIddictClientSystemIntegrationActivation(value); } return null; diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs new file mode 100644 index 00000000..edb9de49 --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs @@ -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; + +/// +/// Provides various settings needed to configure the OpenIddict client system integration. +/// +public enum OpenIddictClientSystemIntegrationAuthenticationMode +{ + /// + /// Browser-based authentication. + /// + SystemBrowser = 0, + + /// + /// Windows web authentication broker-based authentication. + /// + /// + /// Note: the web authentication broker is only supported in UWP applications + /// and its use is generally not recommended due to its inherent limitations. + /// + [SupportedOSPlatform("windows10.0.17763")] + WebAuthenticationBroker = 1 +} diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs new file mode 100644 index 00000000..9c66be39 --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs @@ -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; + +/// +/// Exposes the necessary methods required to configure +/// the OpenIddict client system integration. +/// +public sealed class OpenIddictClientSystemIntegrationBuilder +{ + /// + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictClientSystemIntegrationBuilder(IServiceCollection services) + => Services = services ?? throw new ArgumentNullException(nameof(services)); + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict client system integration configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public OpenIddictClientSystemIntegrationBuilder Configure(Action configuration) + { + if (configuration is null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Uses the Windows web authentication broker to start authentication flows. + /// + /// + /// Note: the web authentication broker is only supported in UWP applications + /// and its use is generally not recommended due to its inherent limitations. + /// + /// The . + [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 + } + + /// + /// Uses the system browser to start authentication flows. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder UseSystemBrowser() + => Configure(options => options.AuthenticationMode = + OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser); + + /// + /// Sets the timeout after which authentication demands that + /// are not completed are automatically aborted by OpenIddict. + /// + /// The authentication timeout. + /// The . + public OpenIddictClientSystemIntegrationBuilder SetAuthenticationTimeout(TimeSpan timeout) + => Configure(options => options.AuthenticationTimeout = timeout); + + /// + /// Disables the built-in protocol activation processing logic. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder DisableActivationHandling() + => Configure(options => options.EnableActivationHandling = false); + + /// + /// Enables the built-in protocol activation processing logic. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder EnableActivationHandling() + => Configure(options => options.EnableActivationHandling = true); + + /// + /// Disables the built-in protocol activation redirection logic. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder DisableActivationRedirection() + => Configure(options => options.EnableActivationRedirection = false); + + /// + /// Enables the built-in protocol activation redirection logic. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder EnableActivationRedirection() + => Configure(options => options.EnableActivationRedirection = true); + + /// + /// Disables the built-in web server used to handle callbacks. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder DisableEmbeddedWebServer() + => Configure(options => options.EnableEmbeddedWebServer = false); + + /// + /// Enables the built-in web server used to handle callbacks. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder EnableEmbeddedWebServer() + => Configure(options => options.EnableEmbeddedWebServer = true); + + /// + /// Disables the pipe server used to process notifications (e.g protocol + /// activation redirections) sent by other instances of the application. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder DisablePipeServer() + => Configure(options => options.EnablePipeServer = false); + + /// + /// Enables the pipe server used to process protocol + /// activations redirected by other instances of the application. + /// + /// The . + public OpenIddictClientSystemIntegrationBuilder EnablePipeServer() + => Configure(options => options.EnablePipeServer = true); + + /// + /// Sets the identifier used to represent the current application + /// instance and redirect protocol activations when necessary. + /// + /// The identifier of the current instance. + /// The . + [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); + } + + /// + /// Sets the base name of the pipe created by OpenIddict to enable + /// inter-process communication and handle protocol activation redirections. + /// + /// The name of the pipe. + /// The . + [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); + } + + /// + /// Sets the options applied to the pipe created by OpenIddict to enable + /// inter-process communication and handle protocol activation redirections. + /// + /// The options flags applied to the pipe. + /// The . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public OpenIddictClientSystemIntegrationBuilder SetPipeOptions(PipeOptions flags) + => Configure(options => options.PipeOptions = flags); + + /// + /// Sets the security policy applied to the pipe created by OpenIddict to enable + /// inter-process communication and handle protocol activation redirections. + /// + /// The security policy applied to the pipe. + /// The . + [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); + } + + /// + /// Determines whether the specified object is equal to the current object. + /// + /// The object to compare with the current object. + /// if the specified object is equal to the current object; otherwise, false. + [EditorBrowsable(EditorBrowsableState.Never)] + public override bool Equals(object? obj) => base.Equals(obj); + + /// + /// Serves as the default hash function. + /// + /// A hash code for the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override int GetHashCode() => base.GetHashCode(); + + /// + /// Returns a string that represents the current object. + /// + /// A string that represents the current object. + [EditorBrowsable(EditorBrowsableState.Never)] + public override string? ToString() => base.ToString(); +} diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs new file mode 100644 index 00000000..b6e60a79 --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs @@ -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; + +/// +/// 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) + { + 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); + } + + /// + 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)); + } + } + + /// + 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 + } + } +} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsConstants.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConstants.cs similarity index 66% rename from src/OpenIddict.Client.Windows/OpenIddictClientWindowsConstants.cs rename to src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConstants.cs index b607b814..e8006726 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsConstants.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConstants.cs @@ -4,13 +4,21 @@ * the license and the contributors participating to this project. */ -namespace OpenIddict.Client.Windows; +namespace OpenIddict.Client.SystemIntegration; /// -/// Exposes common constants used by the OpenIddict Windows host. +/// Exposes common constants used by the OpenIddict client system integration. /// -public static class OpenIddictClientWindowsConstants +public static class OpenIddictClientSystemIntegrationConstants { + public static class Headers + { + public const string CacheControl = "Cache-Control"; + public const string ContentType = "Content-Type"; + public const string Expires = "Expires"; + public const string Pragma = "Pragma"; + } + public static class Tokens { public const string AuthorizationCode = "authorization_code"; diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs similarity index 53% rename from src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs rename to src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs index bcf60bfa..a86ca4ce 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs @@ -9,81 +9,92 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using OpenIddict.Client; -using OpenIddict.Client.Windows; +using OpenIddict.Client.SystemIntegration; namespace Microsoft.Extensions.DependencyInjection; /// /// Exposes extensions allowing to register the OpenIddict client services. /// -public static class OpenIddictClientWindowsExtensions +public static class OpenIddictClientSystemIntegrationExtensions { /// - /// Registers the OpenIddict client services for Windows in the DI container. + /// Registers the OpenIddict client system integration services in the DI container. /// /// The services builder used by OpenIddict to register new services. /// This extension can be safely called multiple times. - /// The . - public static OpenIddictClientWindowsBuilder UseWindows(this OpenIddictClientBuilder builder) + /// The . + public static OpenIddictClientSystemIntegrationBuilder UseSystemIntegration(this OpenIddictClientBuilder builder) { if (builder is null) { throw new ArgumentNullException(nameof(builder)); } - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); } - // Note: the OpenIddict IHostedService implementation is deliberately registered as early as possible to + // Note: the OpenIddict activation handler service is deliberately registered as early as possible to // ensure protocol activations can be handled before another service can stop the initialization of the // application (e.g Dapplo.Microsoft.Extensions.Hosting.AppServices relies on an IHostedService to implement // single instantiation, which would prevent the OpenIddict service from handling the protocol activation - // if the OpenIddict IHostedService implementation was not registered before the Dapplo IHostedService). - if (!builder.Services.Any(static descriptor => descriptor.ServiceType == typeof(IHostedService) && - descriptor.ImplementationType == typeof(OpenIddictClientWindowsHandler))) + // if the OpenIddict activation handler service was not registered before the Dapplo IHostedService). + if (!builder.Services.Any(static descriptor => + descriptor.ServiceType == typeof(IHostedService) && + descriptor.ImplementationType == typeof(OpenIddictClientSystemIntegrationActivationHandler))) { - builder.Services.Insert(0, ServiceDescriptor.Singleton()); + builder.Services.Insert(0, ServiceDescriptor.Singleton()); } // Register the services responsible for coordinating and managing authentication operations. - builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); - // Register the built-in filters used by the default OpenIddict Windows client event handlers. + builder.Services.TryAddSingleton(static provider => provider.GetServices() + .OfType() + .Single()); + + // Register the built-in filters used by the default OpenIddict client system integration event handlers. builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); - builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); - // Register the built-in event handlers used by the OpenIddict Windows client components. + // Register the built-in event handlers used by the OpenIddict client system integration components. // Note: the order used here is not important, as the actual order is set in the options. - builder.Services.TryAdd(OpenIddictClientWindowsHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); + builder.Services.TryAdd(OpenIddictClientSystemIntegrationHandlers.DefaultHandlers.Select(descriptor => descriptor.ServiceDescriptor)); - // Register the option initializer and the background service used by the OpenIddict Windows client integration services. + // Register the option initializer and the background service used by the OpenIddict client system integration services. // Note: TryAddEnumerable() is used here to ensure the initializers and the background service are only registered once. builder.Services.TryAddEnumerable(new[] { - ServiceDescriptor.Singleton(), + ServiceDescriptor.Singleton(), + ServiceDescriptor.Singleton(), - ServiceDescriptor.Singleton, OpenIddictClientWindowsConfiguration>(), - ServiceDescriptor.Singleton, OpenIddictClientWindowsConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictClientSystemIntegrationConfiguration>(), + ServiceDescriptor.Singleton, OpenIddictClientSystemIntegrationConfiguration>(), - ServiceDescriptor.Singleton, OpenIddictClientWindowsConfiguration>() + ServiceDescriptor.Singleton, OpenIddictClientSystemIntegrationConfiguration>() }); - return new OpenIddictClientWindowsBuilder(builder.Services); + return new OpenIddictClientSystemIntegrationBuilder(builder.Services); } /// - /// Registers the OpenIddict client services for Windows in the DI container. + /// Registers the OpenIddict client system integration services in the DI container. /// /// The services builder used by OpenIddict to register new services. /// The configuration delegate used to configure the client services. /// This extension can be safely called multiple times. /// The . - public static OpenIddictClientBuilder UseWindows( - this OpenIddictClientBuilder builder, Action configuration) + public static OpenIddictClientBuilder UseSystemIntegration( + this OpenIddictClientBuilder builder, Action configuration) { if (builder is null) { @@ -95,7 +106,7 @@ public static class OpenIddictClientWindowsExtensions throw new ArgumentNullException(nameof(configuration)); } - configuration(builder.UseWindows()); + configuration(builder.UseSystemIntegration()); return builder; } diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs new file mode 100644 index 00000000..f1c953a4 --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs @@ -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; + +/// +/// Contains a collection of event handler filters commonly used by the system integration handlers. +/// +[EditorBrowsable(EditorBrowsableState.Advanced)] +public static class OpenIddictClientSystemIntegrationHandlerFilters +{ + /// + /// Represents a filter that excludes the associated handlers + /// if no explicit nonce was attached to the authentication context. + /// + public sealed class RequireAuthenticationNonce : IOpenIddictClientHandlerFilter + { + /// + public ValueTask IsActiveAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(!string.IsNullOrEmpty(context.Nonce)); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no HTTP listener context can be found. + /// + public sealed class RequireHttpListenerContext : IOpenIddictClientHandlerFilter + { + /// + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.Transaction.GetHttpListenerContext() is not null); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no interactive user session was detected. + /// + public sealed class RequireInteractiveSession : IOpenIddictClientHandlerFilter + { + /// + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(Environment.UserInteractive); + } + } + + /// + /// Represents a filter that excludes the associated handlers if no protocol activation was found. + /// + public sealed class RequireProtocolActivation : IOpenIddictClientHandlerFilter + { + /// + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + return new(context.Transaction.GetProtocolActivation() is not null); + } + } + + /// + /// Represents a filter that excludes the associated handlers + /// if the system browser integration was not enabled. + /// + public sealed class RequireSystemBrowser : IOpenIddictClientHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireSystemBrowser(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + /// + public ValueTask 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); + } + } + + /// + /// Represents a filter that excludes the associated handlers if + /// the web authentication broker integration was not enabled. + /// + public sealed class RequireWebAuthenticationBroker : IOpenIddictClientHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireWebAuthenticationBroker(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + /// + public ValueTask 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 + } + } + + /// + /// Represents a filter that excludes the associated handlers if no + /// web authentication operation was triggered during the transaction. + /// + public sealed class RequireWebAuthenticationResult : IOpenIddictClientHandlerFilter + { + /// + public ValueTask 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 + } + } +} diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs new file mode 100644 index 00000000..7e692087 --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs @@ -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 DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Authorization request processing: + */ + InvokeWebAuthenticationBroker.Descriptor, + LaunchSystemBrowser.Descriptor, + + /* + * Redirection request extraction: + */ + ExtractGetHttpListenerRequest.Descriptor, + ExtractProtocolActivationParameters.Descriptor, + ExtractWebAuthenticationResultData.Descriptor, + + /* + * Redirection response handling: + */ + AttachHttpResponseCode.Descriptor, + AttachCacheControlHeader.Descriptor, + ProcessEmptyHttpResponse.Descriptor, + ProcessUnactionableResponse.Descriptor); + + /// + /// 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. + /// + public class InvokeWebAuthenticationBroker : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationService _service; + + public InvokeWebAuthenticationBroker(OpenIddictClientSystemIntegrationService service) + => _service = service ?? throw new ArgumentNullException(nameof(service)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// +#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.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 + } + } + + /// + /// 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. + /// + public class LaunchSystemBrowser : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(InvokeWebAuthenticationBroker.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + 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)); + } + } + + /// + /// 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. + /// + public sealed class ProcessEmptyHttpResponse : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + 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(); + } + } + } +} diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs new file mode 100644 index 00000000..8ac93934 --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs @@ -0,0 +1,1723 @@ +/* + * 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.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Net; +using System.Security.Claims; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using OpenIddict.Extensions; +using static OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrationConstants; + +#if !SUPPORTS_HOST_APPLICATION_LIFETIME +using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime; +#endif + +#if SUPPORTS_WINDOWS_RUNTIME +using Windows.Security.Authentication.Web; +#endif + +namespace OpenIddict.Client.SystemIntegration; + +[EditorBrowsable(EditorBrowsableState.Never)] +public static partial class OpenIddictClientSystemIntegrationHandlers +{ + public static ImmutableArray DefaultHandlers { get; } = ImmutableArray.Create( + /* + * Top-level request processing: + */ + ResolveRequestUriFromHttpListenerRequest.Descriptor, + ResolveRequestUriFromProtocolActivation.Descriptor, + ResolveRequestUriFromWebAuthenticationResult.Descriptor, + RejectUnknownHttpRequests.Descriptor, + + /* + * Authentication processing: + */ + WaitMarshalledAuthentication.Descriptor, + RestoreStateTokenFromMarshalledAuthentication.Descriptor, + RestoreStateTokenPrincipalFromMarshalledAuthentication.Descriptor, + RestoreClientRegistrationFromMarshalledContext.Descriptor, + RedirectProtocolActivation.Descriptor, + ResolveRequestForgeryProtection.Descriptor, + RestoreFrontchannelTokensFromMarshalledAuthentication.Descriptor, + RestoreFrontchannelIdentityTokenPrincipalFromMarshalledAuthentication.Descriptor, + RestoreFrontchannelAccessTokenPrincipalFromMarshalledAuthentication.Descriptor, + RestoreAuthorizationCodePrincipalFromMarshalledAuthentication.Descriptor, + RestoreBackchannelTokensFromMarshalledAuthentication.Descriptor, + RestoreBackchannelIdentityTokenPrincipalFromMarshalledAuthentication.Descriptor, + RestoreBackchannelAccessTokenPrincipalFromMarshalledAuthentication.Descriptor, + RestoreRefreshTokenPrincipalFromMarshalledAuthentication.Descriptor, + RestoreUserinfoDetailsFromMarshalledAuthentication.Descriptor, + CompleteAuthenticationOperation.Descriptor, + UntrackMarshalledAuthenticationOperation.Descriptor, + + /* + * Challenge processing: + */ + InferBaseUriFromClientUri.Descriptor, + AttachDynamicPortToRedirectUri.Descriptor, + AttachInstanceIdentifier.Descriptor, + TrackAuthenticationOperation.Descriptor, + + /* + * Error processing: + */ + AbortAuthenticationDemand.Descriptor) + .AddRange(Authentication.DefaultHandlers); + + /// + /// Contains the logic responsible for resolving the request URI from the HTTP listener request. + /// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server. + /// + public sealed class ResolveRequestUriFromHttpListenerRequest : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MinValue + 50_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // When using the OpenIddict client system integration, requests can originate from multiple sources: + // + // - A proper HTTP GET request handled by the embedded web server, when the authorization server + // returns an HTTP 302 response pointing to the local machine (e.g an authorization response). + // In this case, the handling is very similar to what's performed by the web-based OWIN or + // ASP.NET Core hosts and a proper HTTP response can be returned and rendered by the browser. + // + // - A protocol activation triggered when the authorization server returns a HTTP 302 response + // with a redirection address associated with the client application (e.g using a manifest + // or a registry entry). In this case, the redirection is handled by the operating system + // that instantiates the application process and no response can be returned to the browser. + // + // - A protocol activation redirected by another instance of the application using inter-process + // communication. The handling of such activations is similar to direct protocol activations + // and no response can be returned to the browser (that typically stays on the same page). + // + // - A redirection handled transparently by a web-view component (e.g the web authentication + // broker on Windows). In this case, the modal window created by the application or the + // operating system is automatically closed when the specified callback URI is reached + // and there is no way to return a response that would be visible by the user. + // + // OpenIddict unifies these request models by sharing the same request processing pipeline and + // by adapting the logic based on the request type (e.g only protocol activations are redirected + // to other instances and can result in the current instance being terminated by OpenIddict). + + (context.BaseUri, context.RequestUri) = context.Transaction.GetHttpListenerContext() switch + { + // Note: unlike the equivalent handler in the ASP.NET Core and OWIN hosts, the URI is + // expected to be always present and absolute, as the embedded web server is configured + // to use "localhost" as the registered prefix, which forces HTTP.sys (or the managed + // .NET implementation on non-Windows operating systems) to automatically reject requests + // that don't include a Host header (e.g HTTP/1.0 requests) or specify an invalid value. + + { Request.Url: { IsAbsoluteUri: true } uri } => ( + BaseUri: new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), + RequestUri: uri), + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0390)) + }; + + return default; + } + } + + /// + /// Contains the logic responsible for resolving the request URI from the protocol activation details. + /// Note: this handler is not used when the OpenID Connect request is not a protocol activation. + /// + public sealed class ResolveRequestUriFromProtocolActivation : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveRequestUriFromHttpListenerRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + (context.BaseUri, context.RequestUri) = context.Transaction.GetProtocolActivation() switch + { + { ActivationUri: Uri uri } => ( + BaseUri: new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), + RequestUri: uri), + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)) + }; + + return default; + } + } + + /// + /// Contains the logic responsible for resolving the request URI from the web authentication result. + /// Note: this handler is not used when the OpenID Connect request is not a web authentication result. + /// + public sealed class ResolveRequestUriFromWebAuthenticationResult : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveRequestUriFromProtocolActivation.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + +#if SUPPORTS_WINDOWS_RUNTIME + (context.BaseUri, context.RequestUri) = context.Transaction.GetWebAuthenticationResult() switch + { + { ResponseStatus: WebAuthenticationStatus.Success, ResponseData: string data } when + Uri.TryCreate(data, UriKind.Absolute, out Uri? uri) => ( + BaseUri: new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), + RequestUri: uri), + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0393)) + }; + + return default; +#else + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); +#endif + } + } + + /// + /// Contains the logic responsible for rejecting unknown requests handled by the embedded web server, if applicable. + /// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server. + /// + public sealed class RejectUnknownHttpRequests : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(InferEndpointType.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // 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)); + + // Unlike the ASP.NET Core or OWIN hosts, the embedded server instantiated by the system + // integration is not meant to handle requests pointing to user-defined HTTP endpoints. + // At such, reject all HTTP requests whose address doesn't match an OpenIddict endpoint. + if (context.EndpointType is OpenIddictClientEndpointType.Unknown) + { + response.StatusCode = (int) HttpStatusCode.NotFound; + + context.HandleRequest(); + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible for extracting OpenID Connect requests from the HTTP listener request. + /// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server. + /// + public sealed class ExtractGetHttpListenerRequest : IOpenIddictClientHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MinValue + 100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // 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 request = context.Transaction.GetHttpListenerContext()?.Request ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0390)); + + // If the incoming request doesn't use GET, reject it. + if (!string.Equals(request.HttpMethod, "GET", StringComparison.OrdinalIgnoreCase)) + { + context.Logger.LogInformation(SR.GetResourceString(SR.ID6137), request.HttpMethod); + + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2084), + uri: SR.FormatID8000(SR.ID2084)); + + return default; + } + + context.Transaction.Request = request.QueryString.AllKeys.Length switch + { + 0 => new OpenIddictRequest(), + _ => new OpenIddictRequest(AsEnumerable(request.QueryString)) + }; + + return default; + + static IEnumerable> AsEnumerable(NameValueCollection collection) + { + for (var index = 0; index < collection.AllKeys.Length; index++) + { + var name = collection.AllKeys[index]; + if (!string.IsNullOrEmpty(name)) + { + yield return new(name, collection.GetValues(name)); + } + } + } + } + } + + /// + /// Contains the logic responsible for extracting OpenID Connect requests + /// from the URI of an initial or redirected protocol activation. + /// Note: this handler is not used when the OpenID Connect request is not a protocol activation. + /// + public sealed class ExtractProtocolActivationParameters : IOpenIddictClientHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractGetHttpListenerRequest.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.Transaction.Request = context.Transaction.GetProtocolActivation() switch + { + { ActivationUri: Uri uri } => new OpenIddictRequest(OpenIddictHelpers.ParseQuery(uri.Query)), + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)) + }; + + return default; + } + } + + /// + /// Contains the logic responsible for extracting OpenID Connect + /// requests from the response data of a web authentication result. + /// Note: this handler is not used when the OpenID Connect request is not a web authentication result. + /// + public sealed class ExtractWebAuthenticationResultData : IOpenIddictClientHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractProtocolActivationParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + +#if SUPPORTS_WINDOWS_RUNTIME + context.Transaction.Request = context.Transaction.GetWebAuthenticationResult() switch + { + { ResponseStatus: WebAuthenticationStatus.Success, ResponseData: string data } when + Uri.TryCreate(data, UriKind.Absolute, out Uri? uri) + => new OpenIddictRequest(OpenIddictHelpers.ParseQuery(uri.Query)), + + _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0393)) + }; + + return default; +#else + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); +#endif + } + } + + /// + /// Contains the logic responsible for waiting for the marshalled authentication operation to complete, if applicable. + /// + public sealed class WaitMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + private readonly IOptionsMonitor _options; + + public WaitMarshalledAuthentication( + OpenIddictClientSystemIntegrationMarshal marshal, + IOptionsMonitor options) + { + _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateAuthenticationDemand.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + // Skip the marshalling logic entirely if the operation is not tracked. + if (!_marshal.IsTracked(context.Nonce)) + { + return; + } + + // Allow a single authentication operation at the same time with the same nonce. + if (!_marshal.TryAcquireLock(context.Nonce)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0379)); + } + + // At this point, user authentication demands cannot complete until the authorization response has been + // returned to the redirection endpoint (materialized as a registered protocol activation URI) and handled + // by OpenIddict via the ProcessRequest event. Since it is asynchronous by nature, this process requires + // using a signal mechanism to unblock the authentication operation once it is complete. For that, the + // marshal uses a TaskCompletionSource (one per authentication) that will be automatically completed + // or aborted by a specialized event handler as part of the ProcessRequest/ProcessError events processing. + + try + { + // To ensure pending authentication operations for which no response is received are not tracked + // indefinitely, a CancellationTokenSource with a static timeout is used even if the cancellation + // token specified by the user is never marked as canceled: if the authentication is not completed + // when the timeout is reached, the operation will be considered canceled and removed from the list. + using var source = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken); + source.CancelAfter(_options.CurrentValue.AuthenticationTimeout); + + if (!await _marshal.TryWaitForCompletionAsync(context.Nonce, source.Token) || + !_marshal.TryGetResult(context.Nonce, out ProcessAuthenticationContext? notification)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0383)); + } + + if (notification.IsRequestHandled) + { + context.HandleRequest(); + return; + } + + else if (notification.IsRequestSkipped) + { + context.SkipRequest(); + return; + } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } + } + + // If the operation failed due to the timeout, it's likely the TryRemove() method + // won't be called, so the tracked context is manually removed before re-throwing. + catch (OperationCanceledException) when (_marshal.TryRemove(context.Nonce)) + { + throw; + } + } + } + + /// + /// Contains the logic responsible for restoring the state token + /// from the marshalled authentication context, if applicable. + /// + public sealed class RestoreStateTokenFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreStateTokenFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveValidatedStateToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.StateToken = context.EndpointType switch + { + // When the authentication context is marshalled, restore the state token from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.StateToken, + + // Otherwise, don't alter the current context. + _ => context.StateToken + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the state token + /// principal from the marshalled authentication context, if applicable. + /// + public sealed class RestoreStateTokenPrincipalFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreStateTokenPrincipalFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateStateToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.StateTokenPrincipal = context.EndpointType switch + { + // When the authentication context is marshalled, restore + // the state token principal from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.StateTokenPrincipal, + + // Otherwise, don't alter the current context. + _ => context.StateTokenPrincipal + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the client registration and + /// configuration from the marshalled authentication context, if applicable. + /// + public sealed class RestoreClientRegistrationFromMarshalledContext : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreClientRegistrationFromMarshalledContext(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveClientRegistrationFromStateToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + (context.Issuer, context.Configuration, context.Registration) = context.EndpointType switch + { + // When the authentication context is marshalled, restore the + // issuer registration and configuration from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => (notification.Issuer, notification.Configuration, notification.Registration), + + _ => (context.Issuer, context.Configuration, context.Registration) + }; + + return default; + } + } + + /// + /// Contains the logic responsible for redirecting the Windows protocol activation + /// to the instance that initially started the authentication demand, if applicable. + /// Note: this handler is not used when the OpenID Connect request is not a protocol activation. + /// + public sealed class RedirectProtocolActivation : IOpenIddictClientHandler + { + private readonly IHostApplicationLifetime _lifetime; + private readonly IOptionsMonitor _options; + private readonly OpenIddictClientSystemIntegrationService _service; + + public RedirectProtocolActivation( + IHostApplicationLifetime lifetime, + IOptionsMonitor options, + OpenIddictClientSystemIntegrationService service) + { + _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _service = service ?? throw new ArgumentNullException(nameof(service)); + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveNonceFromStateToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + var activation = context.Transaction.GetProtocolActivation() ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)); + + var identifier = context.StateTokenPrincipal.GetClaim(Claims.Private.InstanceId); + if (string.IsNullOrEmpty(identifier)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0376)); + } + + // If the identifier stored in the state token doesn't match the identifier of the + // current instance, stop processing the authentication demand in this process and + // redirect the protocol activation to the correct instance. Once the redirection + // has been received by the other instance, ask the host to stop the application. + + if (string.Equals(identifier, _options.CurrentValue.InstanceIdentifier, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + // If protocol activation redirection was not enabled, reject the request + // as there's no additional processing that can be made at this stage. + if (_options.CurrentValue.EnableActivationRedirection is not true) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2166), + uri: SR.FormatID8000(SR.ID2166)); + + return; + } + + // Try to redirect the protocol activation to the correct instance. + try + { + using var source = new CancellationTokenSource(delay: TimeSpan.FromSeconds(10)); + await _service.RedirectProtocolActivationAsync(activation, identifier, source.Token); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + context.Logger.LogWarning(SR.GetResourceString(SR.ID6215), identifier); + } + + // Inform the host that the application should stop and mark the authentication context as handled + // to prevent the other event handlers from being invoked while the application is shutting down. + _lifetime.StopApplication(); + context.HandleRequest(); + } + } + + /// + /// Contains the logic responsible for resolving the request forgery protection that serves as a + /// protection against state token injection, forged requests and session fixation attacks. + /// + public sealed class ResolveRequestForgeryProtection : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public ResolveRequestForgeryProtection(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateRequestForgeryProtection.Descriptor.Order - 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + // Ensure the authentication demand is tracked by the OpenIddict client Windows marshal + // and resolve the corresponding request forgery protection. If it can't be found, this may + // indicate a session fixation attack: in this case, reject the authentication demand. + if (!_marshal.TryGetRequestForgeryProtection(context.Nonce, out string? protection)) + { + context.Reject( + error: Errors.InvalidRequest, + description: SR.GetResourceString(SR.ID2139), + uri: SR.FormatID8000(SR.ID2139)); + + return default; + } + + context.RequestForgeryProtection = protection; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the frontchannel tokens + /// from the marshalled authentication context, if applicable. + /// + public sealed class RestoreFrontchannelTokensFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreFrontchannelTokensFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveValidatedFrontchannelTokens.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + (context.AuthorizationCode, + context.FrontchannelAccessToken, + context.FrontchannelIdentityToken) = context.EndpointType switch + { + // When the authentication context is marshalled, restore the tokens from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => (notification.AuthorizationCode, notification.FrontchannelAccessToken, notification.FrontchannelIdentityToken), + + // Otherwise, don't alter the current context. + _ => (context.AuthorizationCode, context.FrontchannelAccessToken, context.FrontchannelIdentityToken) + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the frontchannel identity + /// token principal from the marshalled authentication context, if applicable. + /// + public sealed class RestoreFrontchannelIdentityTokenPrincipalFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreFrontchannelIdentityTokenPrincipalFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelIdentityToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.FrontchannelIdentityTokenPrincipal = context.EndpointType switch + { + // When the authentication context is marshalled, restore the + // frontchannel identity token principal from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.FrontchannelIdentityTokenPrincipal, + + // Otherwise, don't alter the current context. + _ => context.FrontchannelIdentityTokenPrincipal + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the frontchannel access + /// token principal from the marshalled authentication context, if applicable. + /// + public sealed class RestoreFrontchannelAccessTokenPrincipalFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreFrontchannelAccessTokenPrincipalFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateFrontchannelAccessToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.FrontchannelAccessTokenPrincipal = context.EndpointType switch + { + // When the authentication context is marshalled, restore the + // frontchannel access token principal from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.FrontchannelAccessTokenPrincipal, + + // Otherwise, don't alter the current context. + _ => context.FrontchannelAccessTokenPrincipal + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the authorization code + /// principal from the marshalled authentication context, if applicable. + /// + public sealed class RestoreAuthorizationCodePrincipalFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreAuthorizationCodePrincipalFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateAuthorizationCode.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.AuthorizationCodePrincipal = context.EndpointType switch + { + // When the authentication context is marshalled, restore the + // authorization code principal from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.AuthorizationCodePrincipal, + + // Otherwise, don't alter the current context. + _ => context.AuthorizationCodePrincipal + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the token response + /// from the marshalled authentication context, if applicable. + /// + public sealed class RestoreTokenResponseFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreTokenResponseFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(SendTokenRequest.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.TokenResponse = context.EndpointType switch + { + // When the authentication context is marshalled, restore the token response from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.TokenResponse, + + // Otherwise, don't alter the current context. + _ => context.TokenResponse + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the backchannel tokens + /// from the marshalled authentication context, if applicable. + /// + public sealed class RestoreBackchannelTokensFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreBackchannelTokensFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveValidatedBackchannelTokens.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + (context.BackchannelAccessToken, + context.BackchannelIdentityToken, + context.RefreshToken) = context.EndpointType switch + { + // When the authentication context is marshalled, restore the tokens from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => (notification.BackchannelAccessToken, notification.BackchannelIdentityToken, notification.RefreshToken), + + // Otherwise, don't alter the current context. + _ => (context.BackchannelAccessToken, context.BackchannelIdentityToken, context.RefreshToken) + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the backchannel identity + /// token principal from the marshalled authentication context, if applicable. + /// + public sealed class RestoreBackchannelIdentityTokenPrincipalFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreBackchannelIdentityTokenPrincipalFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateBackchannelIdentityToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.BackchannelIdentityTokenPrincipal = context.EndpointType switch + { + // When the authentication context is marshalled, restore the + // frontchannel identity token principal from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.BackchannelIdentityTokenPrincipal, + + // Otherwise, don't alter the current context. + _ => context.BackchannelIdentityTokenPrincipal + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the frontchannel access + /// token principal from the marshalled authentication context, if applicable. + /// + public sealed class RestoreBackchannelAccessTokenPrincipalFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreBackchannelAccessTokenPrincipalFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateBackchannelAccessToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.BackchannelAccessTokenPrincipal = context.EndpointType switch + { + // When the authentication context is marshalled, restore the + // frontchannel access token principal from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.BackchannelAccessTokenPrincipal, + + // Otherwise, don't alter the current context. + _ => context.BackchannelAccessTokenPrincipal + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the refresh token + /// principal from the marshalled authentication context, if applicable. + /// + public sealed class RestoreRefreshTokenPrincipalFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreRefreshTokenPrincipalFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateRefreshToken.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + context.RefreshTokenPrincipal = context.EndpointType switch + { + // When the authentication context is marshalled, restore + // the refresh token principal from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => notification.RefreshTokenPrincipal, + + // Otherwise, don't alter the current context. + _ => context.RefreshTokenPrincipal + }; + + return default; + } + } + + /// + /// Contains the logic responsible for restoring the userinfo details + /// from the marshalled authentication context, if applicable. + /// + public sealed class RestoreUserinfoDetailsFromMarshalledAuthentication : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public RestoreUserinfoDetailsFromMarshalledAuthentication(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(SendUserinfoRequest.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + (context.UserinfoResponse, context.UserinfoTokenPrincipal, context.UserinfoToken) = context.EndpointType switch + { + // When the authentication context is marshalled, restore the userinfo details from the other instance. + OpenIddictClientEndpointType.Unknown when _marshal.TryGetResult(context.Nonce, out var notification) + => (notification.UserinfoResponse, notification.UserinfoTokenPrincipal, notification.UserinfoToken), + + // Otherwise, don't alter the current context. + _ => (context.UserinfoResponse, context.UserinfoTokenPrincipal, context.UserinfoToken) + }; + + return default; + } + } + + /// + /// Contains the logic responsible for informing the authentication service the operation is complete. + /// + public sealed class CompleteAuthenticationOperation : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public CompleteAuthenticationOperation(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue - 50_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + // Inform the marshal that the authentication demand is complete. + if (!_marshal.TryComplete(context.Nonce, context)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0380)); + } + + return default; + } + } + + /// + /// Contains the logic responsible for informing the marshal that the context + /// associated with the authentication operation can be discarded, if applicable. + /// + public sealed class UntrackMarshalledAuthenticationOperation : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public UntrackMarshalledAuthenticationOperation(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(int.MaxValue) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessAuthenticationContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(!string.IsNullOrEmpty(context.Nonce), SR.GetResourceString(SR.ID4019)); + + // If applicable, inform the marshal that the authentication demand can be discarded. + if (context.EndpointType is OpenIddictClientEndpointType.Unknown && + _marshal.IsTracked(context.Nonce) && !_marshal.TryRemove(context.Nonce)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0381)); + } + + return default; + } + } + + /// + /// Contains the logic responsible for inferring the base URI from the client URI set in the options. + /// Note: this handler is not used when the user session is not interactive. + /// + public sealed class InferBaseUriFromClientUri : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ValidateChallengeDemand.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + context.BaseUri ??= context.Options.ClientUri; + + return default; + } + } + + /// + /// Contains the logic responsible for attaching the listening port + /// of the embedded web server to the redirect_uri, if applicable. + /// Note: this handler is not used when the user session is not interactive. + /// + public sealed class AttachDynamicPortToRedirectUri : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationHttpListener _listener; + + public AttachDynamicPortToRedirectUri(OpenIddictClientSystemIntegrationHttpListener listener) + => _listener = listener ?? throw new ArgumentNullException(nameof(listener)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(AttachRedirectUri.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public async ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If the redirect_uri uses "localhost" as the authority and doesn't include a non-default port, + // determine whether the embedded web server is running: if so, override the port in the redirect_uri + // by the port used by the embedded web server (guaranteed to be running if a value is returned). + if (!string.IsNullOrEmpty(context.RedirectUri) && + Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) && + string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && + string.Equals(uri.Authority, "localhost", StringComparison.OrdinalIgnoreCase) && uri.IsDefaultPort && + await _listener.GetEmbeddedServerPortAsync(context.CancellationToken) is int port) + { + var builder = new UriBuilder(context.RedirectUri) + { + Port = port + }; + + context.RedirectUri = builder.Uri.AbsoluteUri; + } + } + } + + /// + /// Contains the logic responsible for storing the identifier of the current instance in the state token. + /// Note: this handler is not used when the user session is not interactive. + /// + public sealed class AttachInstanceIdentifier : IOpenIddictClientHandler + { + private readonly IOptionsMonitor _options; + + public AttachInstanceIdentifier(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(PrepareLoginStateTokenPrincipal.Descriptor.Order + 500) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + Debug.Assert(context.StateTokenPrincipal is { Identity: ClaimsIdentity }, SR.GetResourceString(SR.ID4006)); + + // Most applications (except Windows UWP applications) are multi-instanced. As such, any protocol activation + // triggered by launching one of the URI schemes associated with the application will create a new instance, + // different from the one that initially started the authentication flow. To deal with that without having to + // share persistent state between instances, OpenIddict stores the identifier of the instance that starts the + // authentication process and uses it when handling the callback to determine whether the protocol activation + // should be redirected to a different instance using inter-process communication. + context.StateTokenPrincipal.SetClaim(Claims.Private.InstanceId, _options.CurrentValue.InstanceIdentifier); + + return default; + } + } + + /// + /// Contains the logic responsible for asking the marshal to track the authentication operation. + /// Note: this handler is not used when the user session is not interactive. + /// + public sealed class TrackAuthenticationOperation : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + + public TrackAuthenticationOperation(OpenIddictClientSystemIntegrationMarshal marshal) + => _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessChallengeContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (string.IsNullOrEmpty(context.Nonce)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0352)); + } + + if (string.IsNullOrEmpty(context.RequestForgeryProtection)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0343)); + } + + if (!_marshal.TryAdd(context.Nonce, context.RequestForgeryProtection)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0378)); + } + + return default; + } + } + + /// + /// Contains the logic responsible for informing the authentication service the demand is aborted. + /// + public sealed class AbortAuthenticationDemand : IOpenIddictClientHandler + { + private readonly OpenIddictClientSystemIntegrationMarshal _marshal; + private readonly IHostApplicationLifetime _lifetime; + + public AbortAuthenticationDemand( + OpenIddictClientSystemIntegrationMarshal marshal, + IHostApplicationLifetime lifetime) + { + _marshal = marshal ?? throw new ArgumentNullException(nameof(marshal)); + _lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime)); + } + + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(ProcessErrorContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // Try to resolve the authentication context from the transaction, if available. + var notification = context.Transaction.GetProperty( + typeof(ProcessAuthenticationContext).FullName!); + + // If the context is available, resolve the nonce used to track the marshalled authentication + // and inform the marshal so that the context can be marshalled back to the initiator. + if (!string.IsNullOrEmpty(notification?.Nonce) && !_marshal.TryComplete(notification.Nonce, notification)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0382)); + } + + // If the current application instance was created to react to a protocol activation (assumed to be + // managed by OpenIddict at this stage), terminate it to prevent the UI thread from being started. + // By doing that, unsolicited requests will be discarded without the user seeing flashing windows. + if (context.Transaction.GetProtocolActivation() is { IsActivationRedirected: false }) + { + _lifetime.StopApplication(); + + context.HandleRequest(); + return default; + } + + return default; + } + } + + /// + /// Contains the logic responsible for attaching an appropriate HTTP status code. + /// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server. + /// + public sealed class AttachHttpResponseCode : IOpenIddictClientHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // 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)); + + Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007)); + + response.StatusCode = context.Transaction.Response.Error switch + { + null => 200, // Note: the default code may be replaced by another handler (e.g when doing redirects). + + _ => 400 + }; + + return default; + } + } + + /// + /// Contains the logic responsible for attaching the appropriate HTTP response cache headers. + /// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server. + /// + public sealed class AttachCacheControlHeader : IOpenIddictClientHandler where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(AttachHttpResponseCode.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // 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)); + + // Prevent the response from being cached. + response.Headers[Headers.CacheControl] = "no-store"; + response.Headers[Headers.Pragma] = "no-cache"; + response.Headers[Headers.Expires] = "Thu, 01 Jan 1970 00:00:00 GMT"; + + return default; + } + } + + /// + /// Contains the logic responsible for marking OpenID Connect responses + /// returned via protocol activations or web authentication results as processed. + /// + public sealed class ProcessUnactionableResponse : IOpenIddictClientHandler + where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .UseSingletonHandler>() + .SetOrder(int.MaxValue) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + // For both protocol activations (initial or redirected) and web-view-like results, + // no proper response can be generated and eventually displayed to the user. In this + // case, simply stop processing the response and mark the request as fully handled. + // + // Note: this logic applies to both successful and errored responses. + + if (context.Transaction.GetProtocolActivation() is not null) + { + context.HandleRequest(); + return default; + } + +#if SUPPORTS_WINDOWS_RUNTIME + if (context.Transaction.GetWebAuthenticationResult() is not null) + { + context.HandleRequest(); + return default; + } +#endif + return default; + } + } +} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs similarity index 73% rename from src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs rename to src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs index 9c0d275e..61d828d1 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs @@ -6,6 +6,7 @@ using System.ComponentModel; using System.Diagnostics; +using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; @@ -15,30 +16,49 @@ using System.Security.Principal; using Windows.ApplicationModel.Activation; using Windows.ApplicationModel; using Windows.Foundation.Metadata; +using Windows.Security.Authentication.Web; using Windows.System; #endif -namespace OpenIddict.Client.Windows; +namespace OpenIddict.Client.SystemIntegration; /// /// Exposes companion extensions for the OpenIddict/Windows integration. /// -public static class OpenIddictClientWindowsHelpers +public static class OpenIddictClientSystemIntegrationHelpers { /// - /// Gets the associated with the current context. + /// Gets the associated with the current context. /// /// The transaction instance. - /// The instance or if it couldn't be found. - public static OpenIddictClientWindowsActivation? GetWindowsActivation(this OpenIddictClientTransaction transaction) - => transaction.GetProperty(typeof(OpenIddictClientWindowsActivation).FullName!); + /// The instance or if it couldn't be found. + public static OpenIddictClientSystemIntegrationActivation? GetProtocolActivation(this OpenIddictClientTransaction transaction) + => transaction.GetProperty(typeof(OpenIddictClientSystemIntegrationActivation).FullName!); + + /// + /// Gets the associated with the current context. + /// + /// The transaction instance. + /// The instance or if it couldn't be found. + public static HttpListenerContext? GetHttpListenerContext(this OpenIddictClientTransaction transaction) + => transaction.GetProperty(typeof(HttpListenerContext).FullName!); #if SUPPORTS_WINDOWS_RUNTIME + /// + /// Gets the associated with the current context. + /// + /// The transaction instance. + /// The instance or if it couldn't be found. + public static WebAuthenticationResult? GetWebAuthenticationResult(this OpenIddictClientTransaction transaction) + => transaction.GetProperty(typeof(WebAuthenticationResult).FullName!); + /// /// Determines whether the Windows Runtime APIs are supported on this platform. /// /// if the Windows Runtime APIs are supported, otherwise. - [MethodImpl(MethodImplOptions.AggressiveInlining), SupportedOSPlatformGuard("windows10.0.17763")] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SupportedOSPlatform("windows")] + [SupportedOSPlatformGuard("windows10.0.17763")] internal static bool IsWindowsRuntimeSupported() { // Note: as WinRT is only supported on Windows 8 and higher, trying to call any of the @@ -222,4 +242,37 @@ public static class OpenIddictClientWindowsHelpers return false; } } + + /// + /// Starts the system browser using xdg-open. + /// + /// The to use. + /// if the browser could be started, otherwise. + [SupportedOSPlatform("linux")] + internal static async Task TryLaunchBrowserWithXdgOpenAsync(Uri uri) + { + try + { + await Task.Run(() => Process.Start(new ProcessStartInfo + { + FileName = "xdg-open", + Arguments = uri.AbsoluteUri, + UseShellExecute = false, + + // Note: on some Linux distributions, xdg-open is known to propagate errors + // and warnings written to the standard error stream to the parent process. + // To avoid that, the streams are redirected to this instance and ignored. + RedirectStandardError = true, + RedirectStandardInput = true, + RedirectStandardOutput = true + })); + + return true; + } + + catch (UnauthorizedAccessException) + { + return false; + } + } } diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs new file mode 100644 index 00000000..2adc8e68 --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs @@ -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; + +/// +/// Contains the logic necessary to handle URI protocol activations that +/// are redirected by other instances using inter-process communication. +/// +/// +/// Note: initial URI protocol activations are handled by . +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class OpenIddictClientSystemIntegrationHttpListener : BackgroundService +{ + private readonly TaskCompletionSource _source = new(); + private readonly ILogger _logger; + private readonly IOptionsMonitor _options; + private readonly OpenIddictClientSystemIntegrationService _service; + + /// + /// Creates a new instance of the class. + /// + /// The logger. + /// The OpenIddict client system integration options. + /// The OpenIddict client system integration service. + public OpenIddictClientSystemIntegrationHttpListener( + ILogger logger, + IOptionsMonitor options, + OpenIddictClientSystemIntegrationService service) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _service = service ?? throw new ArgumentNullException(nameof(service)); + } + + /// + 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 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; + } + } + } + } + + /// + /// Resolves the port associated to the created by this service, or + /// if the embedded web server instantiation was disabled in the options. + /// + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, whose result + /// returns the port associated to the created by this service, or + /// if the embedded web server instantiation was disabled in the options. + /// + internal Task GetEmbeddedServerPortAsync(CancellationToken cancellationToken = default) + => _source.Task.WaitAsync(cancellationToken); +} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsMarshal.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs similarity index 87% rename from src/OpenIddict.Client.Windows/OpenIddictClientWindowsMarshal.cs rename to src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs index 54a0758f..b84faaa1 100644 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsMarshal.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs @@ -7,14 +7,15 @@ using System.Collections.Concurrent; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using OpenIddict.Extensions; -namespace OpenIddict.Client.Windows; +namespace OpenIddict.Client.SystemIntegration; /// /// Contains the APIs needed to coordinate authentication operations that happen in a different context. /// [EditorBrowsable(EditorBrowsableState.Never)] -public sealed class OpenIddictClientWindowsMarshal +public sealed class OpenIddictClientSystemIntegrationMarshal { private readonly ConcurrentDictionaryThe nonce, used as a unique identifier. /// The request forgery protection associated with the specified authentication demand. /// if the operation could be added, otherwise. - internal bool TryAdd(string nonce, string protection) - => _operations.TryAdd(nonce, new(() => (protection, new SemaphoreSlim(initialCount: 1, maxCount: 1), new()))); + internal bool TryAdd(string nonce, string protection) => _operations.TryAdd(nonce, new(() => ( + RequestForgeryProtection: protection, + Semaphore: new SemaphoreSlim(initialCount: 1, maxCount: 1), + TaskCompletionSource: new(TaskCreationOptions.RunContinuationsAsynchronously)))); /// /// Tries to acquire a lock on the authentication demand corresponding to the specified nonce. @@ -83,17 +86,8 @@ public sealed class OpenIddictClientWindowsMarshal return false; } - var source = new TaskCompletionSource(TaskCreationOptions.None); - using (cancellationToken.Register(static state => ((TaskCompletionSource) state!).SetResult(true), source)) - { - if (await Task.WhenAny(operation.Value.TaskCompletionSource.Task, source.Task) == source.Task) - { - throw new OperationCanceledException(cancellationToken); - } - - await operation.Value.TaskCompletionSource.Task; - return true; - } + await operation.Value.TaskCompletionSource.Task.WaitAsync(cancellationToken); + return true; } /// diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationOptions.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationOptions.cs new file mode 100644 index 00000000..a2dd36ab --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationOptions.cs @@ -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; + +/// +/// Provides various settings needed to configure the OpenIddict client system integration. +/// +public sealed class OpenIddictClientSystemIntegrationOptions +{ + /// + /// Gets or sets the authentication mode used to start authentication flows. + /// + /// + /// If this property is not explicitly set, its value is automatically set by OpenIddict. + /// + public OpenIddictClientSystemIntegrationAuthenticationMode? AuthenticationMode { get; set; } + + /// + /// Gets or sets the timeout after which authentication demands + /// that are not completed are automatically aborted by OpenIddict. + /// + public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10); + + /// + /// Gets or sets a boolean indicating whether protocol activation processing should be enabled. + /// + /// + /// 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. + /// + public bool? EnableActivationHandling { get; set; } + + /// + /// Gets or sets a boolean indicating whether protocol activation redirection should be enabled. + /// + /// + /// 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. + /// + public bool? EnableActivationRedirection { get; set; } + + /// + /// Gets or sets a boolean indicating whether a local web server + /// should be started on a random port to handle callbacks. + /// + /// + /// 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. + /// + public bool? EnableEmbeddedWebServer { get; set; } + + /// + /// 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. + /// + /// + /// 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. + /// + public bool? EnablePipeServer { get; set; } + + /// + /// Gets or sets the identifier used to represent the current application + /// instance and redirect protocol activations when necessary. + /// + public string? InstanceIdentifier { get; set; } + + /// + /// Gets or sets the base name of the pipe created by OpenIddict to enable + /// inter-process communication and handle protocol activation redirections. + /// + /// + /// If no value is explicitly set, a default name is automatically computed. + /// + public string PipeName { get; set; } = default!; + + /// + /// Gets or sets the pipe options applied to the pipe created by OpenIddict to enable + /// inter-process communication and handle protocol activation redirections. + /// + /// + /// If no value is explicitly set, a default combination is automatically used. + /// + public PipeOptions? PipeOptions { get; set; } + + /// + /// Gets or sets the security policy applied to the pipe created by OpenIddict + /// to enable inter-process communication and handle protocol activation redirections. + /// + /// + /// If no value is explicitly set, a default policy is automatically created + /// (unless the application is running inside an AppContainer sandbox). + /// + [SupportedOSPlatform("windows")] + public PipeSecurity? PipeSecurity { get; set; } +} diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationPipeListener.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationPipeListener.cs new file mode 100644 index 00000000..54213a0d --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationPipeListener.cs @@ -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; + +/// +/// Contains the logic necessary to handle URI protocol activations that +/// are redirected by other instances using inter-process communication. +/// +/// +/// Note: initial URI protocol activations are handled by . +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class OpenIddictClientSystemIntegrationPipeListener : BackgroundService +{ + private readonly ILogger _logger; + private readonly IOptionsMonitor _options; + private readonly OpenIddictClientSystemIntegrationService _service; + + /// + /// Creates a new instance of the class. + /// + /// The logger. + /// The OpenIddict client system integration options. + /// The OpenIddict client system integration service. + public OpenIddictClientSystemIntegrationPipeListener( + ILogger logger, + IOptionsMonitor options, + OpenIddictClientSystemIntegrationService service) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _service = service ?? throw new ArgumentNullException(nameof(service)); + } + + /// + 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 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 + }; + } + } +} diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs new file mode 100644 index 00000000..c015fe45 --- /dev/null +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs @@ -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; + +/// +/// 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). +/// +public sealed class OpenIddictClientSystemIntegrationService +{ + private readonly IOptionsMonitor _options; + private readonly IServiceProvider _provider; + + /// + /// Creates a new instance of the class. + /// + /// The OpenIddict client system integration options. + /// The service provider. + /// is . + public OpenIddictClientSystemIntegrationService( + IOptionsMonitor options, + IServiceProvider provider) + { + _options = options ?? throw new ArgumentNullException(nameof(options)); + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + /// + /// Handles the specified protocol activation. + /// + /// The protocol activation details. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// is . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Task HandleProtocolActivationAsync(OpenIddictClientSystemIntegrationActivation activation, + CancellationToken cancellationToken = default) + { + if (activation is null) + { + throw new ArgumentNullException(nameof(activation)); + } + + return HandleRequestAsync(activation, cancellationToken); + } + + /// + /// Handles the specified HTTP request. + /// + /// The HTTP request received by the embedded web server. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// is . + 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 + /// + /// Handles the specified web authentication result. + /// + /// The web authentication result. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// is . + internal Task HandleWebAuthenticationResultAsync(WebAuthenticationResult result, CancellationToken cancellationToken = default) + { + if (result is null) + { + throw new ArgumentNullException(nameof(result)); + } + + return HandleRequestAsync(result, cancellationToken); + } +#endif + + /// + /// Handles the request using the specified property. + /// + /// The property to add to the transaction. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// is . + private async Task HandleRequestAsync(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(); + var factory = scope.ServiceProvider.GetRequiredService(); + + // 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(); + } + } + } + + /// + /// Redirects a protocol activation to the specified instance. + /// + /// The protocol activation to redirect. + /// The identifier of the target instance. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// is . + 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); + } +} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs deleted file mode 100644 index a9bf3d6d..00000000 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs +++ /dev/null @@ -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; - -/// -/// Exposes the necessary methods required to configure -/// the OpenIddict client Windows integration. -/// -public sealed class OpenIddictClientWindowsBuilder -{ - /// - /// Initializes a new instance of . - /// - /// The services collection. - public OpenIddictClientWindowsBuilder(IServiceCollection services) - => Services = services ?? throw new ArgumentNullException(nameof(services)); - - /// - /// Gets the services collection. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public IServiceCollection Services { get; } - - /// - /// Amends the default OpenIddict client Windows configuration. - /// - /// The delegate used to configure the OpenIddict options. - /// This extension can be safely called multiple times. - /// The . - public OpenIddictClientWindowsBuilder Configure(Action configuration) - { - if (configuration is null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - Services.Configure(configuration); - - return this; - } - - /// - /// Disables the built-in protocol activation processing logic, which - /// can be used to offload this task a separate dedicated executable. - /// - /// The . - public OpenIddictClientWindowsBuilder DisableProtocolActivationProcessing() - => Configure(options => options.DisableProtocolActivationProcessing = true); - - /// - /// Sets the timeout after which authentication demands that - /// are not completed are automatically aborted by OpenIddict. - /// - /// The authentication timeout. - /// The . - public OpenIddictClientWindowsBuilder SetAuthenticationTimeout(TimeSpan timeout) - => Configure(options => options.AuthenticationTimeout = timeout); - - /// - /// Sets the identifier used to represent the current application - /// instance and redirect protocol activations when necessary. - /// - /// The identifier of the current instance. - /// The . - [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); - } - - /// - /// Sets the base name of the pipe created by OpenIddict to enable - /// inter-process communication and handle protocol activation redirections. - /// - /// The name of the pipe. - /// The . - [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); - } - - /// - /// Sets the security policy applied to the pipe created by OpenIddict to enable - /// inter-process communication and handle protocol activation redirections. - /// - /// The security policy applied to the pipe. - /// The . - [EditorBrowsable(EditorBrowsableState.Advanced)] - public OpenIddictClientWindowsBuilder SetPipeSecurity(PipeSecurity security) - { - if (security is null) - { - throw new ArgumentNullException(nameof(security)); - } - - return Configure(options => options.PipeSecurity = security); - } - - /// - /// Determines whether the specified object is equal to the current object. - /// - /// The object to compare with the current object. - /// if the specified object is equal to the current object; otherwise, false. - [EditorBrowsable(EditorBrowsableState.Never)] - public override bool Equals(object? obj) => base.Equals(obj); - - /// - /// Serves as the default hash function. - /// - /// A hash code for the current object. - [EditorBrowsable(EditorBrowsableState.Never)] - public override int GetHashCode() => base.GetHashCode(); - - /// - /// Returns a string that represents the current object. - /// - /// A string that represents the current object. - [EditorBrowsable(EditorBrowsableState.Never)] - public override string? ToString() => base.ToString(); -} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsConfiguration.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsConfiguration.cs deleted file mode 100644 index c4c11dbf..00000000 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsConfiguration.cs +++ /dev/null @@ -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; - -/// -/// Contains the methods required to ensure that the OpenIddict client configuration is valid. -/// -[EditorBrowsable(EditorBrowsableState.Advanced)] -public sealed class OpenIddictClientWindowsConfiguration : IConfigureOptions, - IPostConfigureOptions, - IPostConfigureOptions -{ - private readonly IHostEnvironment _environment; - - /// - /// Creates a new instance of the class. - /// - /// The host environment. - public OpenIddictClientWindowsConfiguration(IHostEnvironment environment) - => _environment = environment ?? throw new ArgumentNullException(nameof(environment)); - - /// - 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); - } - - /// - 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)); - } - } - - /// - 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 - } -} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlerFilters.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlerFilters.cs deleted file mode 100644 index 592026e1..00000000 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlerFilters.cs +++ /dev/null @@ -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; - -/// -/// Contains a collection of event handler filters commonly used by the Windows handlers. -/// -[EditorBrowsable(EditorBrowsableState.Advanced)] -public static class OpenIddictClientWindowsHandlerFilters -{ - /// - /// Represents a filter that excludes the associated handlers - /// if no explicit nonce was attached to the authentication context. - /// - public sealed class RequireAuthenticationNonce : IOpenIddictClientHandlerFilter - { - /// - public ValueTask IsActiveAsync(ProcessAuthenticationContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new(!string.IsNullOrEmpty(context.Nonce)); - } - } - - /// - /// Represents a filter that excludes the associated handlers if no interactive user session was detected. - /// - public sealed class RequireInteractiveSession : IOpenIddictClientHandlerFilter - { - /// - public ValueTask IsActiveAsync(BaseContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new(Environment.UserInteractive); - } - } - - /// - /// Represents a filter that excludes the associated handlers if no Windows activation was found. - /// - public sealed class RequireWindowsActivation : IOpenIddictClientHandlerFilter - { - /// - public ValueTask IsActiveAsync(BaseContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - return new(context.Transaction.GetWindowsActivation() is not null); - } - } -} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs deleted file mode 100644 index 9495c7c3..00000000 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs +++ /dev/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; - -/// -/// Contains the logic necessary to handle URI protocol activations that -/// are redirected by other instances using inter-process communication. -/// -/// -/// Note: initial URI protocol activations are handled by . -/// -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed class OpenIddictClientWindowsListener : BackgroundService -{ - private readonly ILogger _logger; - private readonly IOptionsMonitor _options; - private readonly OpenIddictClientWindowsService _service; - - /// - /// Creates a new instance of the class. - /// - /// The logger. - /// The OpenIddict client Windows integration options. - /// The OpenIddict client Windows service. - public OpenIddictClientWindowsListener( - ILogger logger, - IOptionsMonitor options, - OpenIddictClientWindowsService service) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options ?? throw new ArgumentNullException(nameof(options)); - _service = service ?? throw new ArgumentNullException(nameof(service)); - } - - /// - 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 - }; - } - } -} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs deleted file mode 100644 index a4b79676..00000000 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs +++ /dev/null @@ -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; - -/// -/// Provides various settings needed to configure the OpenIddict Windows client integration. -/// -public sealed class OpenIddictClientWindowsOptions -{ - /// - /// Gets or sets the timeout after which authentication demands - /// that are not completed are automatically aborted by OpenIddict. - /// - public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10); - - /// - /// Gets or sets a boolean indicating whether protocol activation processing should be - /// disabled, which can be used to offload this task to a separate dedicated executable. - /// - public bool DisableProtocolActivationProcessing { get; set; } - - /// - /// Gets or sets the identifier used to represent the current application - /// instance and redirect protocol activations when necessary. - /// - public string? InstanceIdentifier { get; set; } - - /// - /// Gets or sets the base name of the pipe created by OpenIddict to enable - /// inter-process communication and handle protocol activation redirections. - /// - /// - /// If no value is explicitly set, a default name is automatically computed. - /// - public string PipeName { get; set; } = default!; - - /// - /// Gets or sets the security policy applied to the pipe created by OpenIddict - /// to enable inter-process communication and handle protocol activation redirections. - /// - /// - /// If no value is explicitly set, a default policy is automatically created - /// (unless the application is running inside an AppContainer sandbox). - /// - public PipeSecurity PipeSecurity { get; set; } = default!; -} diff --git a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs b/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs deleted file mode 100644 index 9be0b337..00000000 --- a/src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs +++ /dev/null @@ -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; - -/// -/// 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). -/// -public sealed class OpenIddictClientWindowsService -{ - private readonly IServiceProvider _provider; - - /// - /// Creates a new instance of the class. - /// - /// The service provider. - /// is . - public OpenIddictClientWindowsService(IServiceProvider provider) - => _provider = provider ?? throw new ArgumentNullException(nameof(provider)); - - /// - /// Handles the specified protocol activation. - /// - /// The protocol activation details. - /// The that can be used to abort the operation. - /// A that can be used to monitor the asynchronous operation. - /// is . - [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(); - var factory = scope.ServiceProvider.GetRequiredService(); - - // Create a client transaction and store the protocol activation details so they can be - // retrieved by the Windows-specific client event handlers that need to access them. - var transaction = await factory.CreateTransactionAsync(); - transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!, activation); - - var context = new ProcessRequestContext(transaction) - { - CancellationToken = cancellationToken - }; - - await dispatcher.DispatchAsync(context); - - if (context.IsRejected) - { - await dispatcher.DispatchAsync(new ProcessErrorContext(transaction) - { - CancellationToken = cancellationToken, - Error = context.Error ?? Errors.InvalidRequest, - ErrorDescription = context.ErrorDescription, - ErrorUri = context.ErrorUri, - Response = new OpenIddictResponse() - }); - } - } - - finally - { - if (scope is IAsyncDisposable disposable) - { - await disposable.DisposeAsync(); - } - - else - { - scope.Dispose(); - } - } - } -} diff --git a/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs b/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs index 46a2faad..5599f07d 100644 --- a/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs +++ b/src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs @@ -62,6 +62,19 @@ public static partial class OpenIddictClientEvents set => Transaction.Request = value; } + /// + /// Gets or sets the nonce that is used as the unique identifier of the operation, if available. + /// + public string? Nonce { get; set; } + + /// + /// Gets or sets the redirect URI that was selected during the challenge, if available. + /// + public string? RedirectUri { get; set; } + + /// + /// Gets or sets the URI of the remote authorization endpoint. + /// public string AuthorizationEndpoint { get; set; } = null!; } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs index 9bb25d32..10e75e8b 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs @@ -117,7 +117,12 @@ public static partial class OpenIddictClientHandlers throw new ArgumentNullException(nameof(context)); } - var notification = new ApplyAuthorizationRequestContext(context.Transaction); + var notification = new ApplyAuthorizationRequestContext(context.Transaction) + { + Nonce = context.Nonce, + RedirectUri = context.RedirectUri + }; + await _dispatcher.DispatchAsync(notification); if (notification.IsRequestHandled) @@ -131,6 +136,15 @@ public static partial class OpenIddictClientHandlers context.SkipRequest(); return; } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } } } @@ -339,7 +353,7 @@ public static partial class OpenIddictClientHandlers return; } - throw new InvalidOperationException(SR.GetResourceString(SR.ID0368)); + context.Transaction.Response = new OpenIddictResponse(); } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs index 042f7b16..dbfe2267 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs @@ -124,6 +124,15 @@ public static partial class OpenIddictClientHandlers context.SkipRequest(); return; } + + else if (notification.IsRejected) + { + context.Reject( + error: notification.Error ?? Errors.InvalidRequest, + description: notification.ErrorDescription, + uri: notification.ErrorUri); + return; + } } } diff --git a/src/OpenIddict.Client/OpenIddictClientHandlers.cs b/src/OpenIddict.Client/OpenIddictClientHandlers.cs index b35e68d5..ec55cfb2 100644 --- a/src/OpenIddict.Client/OpenIddictClientHandlers.cs +++ b/src/OpenIddict.Client/OpenIddictClientHandlers.cs @@ -3964,12 +3964,12 @@ public static partial class OpenIddictClientHandlers // Otherwise, always default to the response types that have been enabled globally. ResponseTypes: context.Registration.ResponseTypes.Count switch { - 0 => context.Options.ResponseTypes.Select(types => types + 0 => context.Options.ResponseTypes.Select(static types => types .Split(Separators.Space, StringSplitOptions.None) .ToHashSet(StringComparer.Ordinal)) .ToList(), - _ => context.Options.ResponseTypes.Select(types => types + _ => context.Options.ResponseTypes.Select(static types => types .Split(Separators.Space, StringSplitOptions.None) .ToHashSet(StringComparer.Ordinal)) .Where(types => context.Registration.ResponseTypes.Any(value => value @@ -3983,7 +3983,7 @@ public static partial class OpenIddictClientHandlers GrantTypes: context.Configuration.GrantTypesSupported, ResponseTypes: context.Configuration.ResponseTypesSupported - .Select(types => types + .Select(static types => types .Split(Separators.Space, StringSplitOptions.None) .ToHashSet(StringComparer.Ordinal)) .ToList())) switch @@ -4014,7 +4014,7 @@ public static partial class OpenIddictClientHandlers client.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.Code)) && server.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.Code)) - => (GrantTypes.AuthorizationCode, ResponseTypes.Code), + => (GrantTypes.AuthorizationCode, ResponseTypes.Code), // Hybrid flow with grant_type=authorization_code/implicit and response_type=code id_token: (var client, var server) when @@ -4042,7 +4042,7 @@ public static partial class OpenIddictClientHandlers client.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.IdToken)) && server.ResponseTypes.Exists(static types => types.Count is 1 && types.Contains(ResponseTypes.IdToken)) - => (GrantTypes.Implicit, ResponseTypes.IdToken), + => (GrantTypes.Implicit, ResponseTypes.IdToken), // Note: response types combinations containing "token" are always tested last as some // authorization servers - like OpenIddict - are known to block authorization requests diff --git a/src/OpenIddict.Client/OpenIddictClientService.cs b/src/OpenIddict.Client/OpenIddictClientService.cs index a67756c4..56c9a241 100644 --- a/src/OpenIddict.Client/OpenIddictClientService.cs +++ b/src/OpenIddict.Client/OpenIddictClientService.cs @@ -37,7 +37,7 @@ public sealed class OpenIddictClientService /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. [RequiresPreviewFeatures] - public ValueTask ChallengeWithBrowserAsync( + public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateInteractivelyAsync( Uri issuer, string[]? scopes = null, Dictionary? parameters = null, Dictionary? properties = null, CancellationToken cancellationToken = default) @@ -51,7 +51,8 @@ public sealed class OpenIddictClientService var registration = options.CurrentValue.Registrations.Find(registration => registration.Issuer == issuer) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0292)); - return ChallengeWithBrowserAsync(registration, scopes, parameters, properties, cancellationToken); + return await AuthenticateInteractivelyAsync(nonce: + await ChallengeInteractivelyAsync(registration, scopes, parameters, properties, cancellationToken), cancellationToken); } /// @@ -64,7 +65,7 @@ public sealed class OpenIddictClientService /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. [RequiresPreviewFeatures] - public ValueTask ChallengeWithBrowserAsync( + public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateInteractivelyAsync( string provider, string[]? scopes = null, Dictionary? parameters = null, Dictionary? properties = null, CancellationToken cancellationToken = default) @@ -79,37 +80,20 @@ public sealed class OpenIddictClientService registration.ProviderName, provider, StringComparison.Ordinal)) ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0348)); - return ChallengeWithBrowserAsync(registration, scopes, parameters, properties, cancellationToken); + return await AuthenticateInteractivelyAsync(nonce: + await ChallengeInteractivelyAsync(registration, scopes, parameters, properties, cancellationToken), cancellationToken); } /// - /// Initiates an interactive user authentication demand. + /// Completes the interactive authentication demand corresponding to the specified nonce. /// - /// The client registration. - /// The scopes to request to the authorization server. - /// The additional parameters to send as part of the token request. - /// The application-specific properties that will be added to the authentication context. + /// The nonce obtained after a challenge operation. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. [RequiresPreviewFeatures] - private async ValueTask ChallengeWithBrowserAsync( - OpenIddictClientRegistration registration, string[]? scopes = null, - Dictionary? parameters = null, - Dictionary? properties = null, CancellationToken cancellationToken = default) + private async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateInteractivelyAsync( + string nonce, CancellationToken cancellationToken = default) { - if (registration is null) - { - throw new ArgumentNullException(nameof(registration)); - } - - if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty)) - { - throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); - } - - var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); - cancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot @@ -126,48 +110,38 @@ public sealed class OpenIddictClientService var transaction = await factory.CreateTransactionAsync(); - var context = new ProcessChallengeContext(transaction) + var context = new ProcessAuthenticationContext(transaction) { CancellationToken = cancellationToken, - Configuration = configuration, - Issuer = registration.Issuer, - Principal = new ClaimsPrincipal(new ClaimsIdentity()), - Registration = registration, - Request = parameters is not null ? new(parameters) : new(), + Nonce = nonce }; - if (scopes is { Length: > 0 }) - { - context.Scopes.UnionWith(scopes); - } - - if (properties is { Count: > 0 }) - { - foreach (var property in properties) - { - context.Properties[property.Key] = property.Value; - } - } - await dispatcher.DispatchAsync(context); if (context.IsRejected) { - await dispatcher.DispatchAsync(new ProcessErrorContext(transaction) - { - Error = context.Error ?? Errors.InvalidRequest, - ErrorDescription = context.ErrorDescription, - ErrorUri = context.ErrorUri, - Response = new OpenIddictResponse() - }); + throw new ProtocolException( + message: SR.GetResourceString(SR.ID0374), + context.Error, context.ErrorDescription, context.ErrorUri); } - if (string.IsNullOrEmpty(context.Nonce)) + else { - throw new InvalidOperationException(SR.GetResourceString(SR.ID0352)); - } + var principal = OpenIddictHelpers.CreateMergedPrincipal( + context.FrontchannelIdentityTokenPrincipal, + context.BackchannelIdentityTokenPrincipal, + context.UserinfoTokenPrincipal) ?? new ClaimsPrincipal(new ClaimsIdentity()); - return context.Nonce; + // Attach the identity of the authorization to the returned principal to allow resolving it even if no other + // claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available). + principal.SetClaim(Claims.AuthorizationServer, context.StateTokenPrincipal?.GetClaim(Claims.AuthorizationServer)) + .SetClaim(Claims.Private.ProviderName, context.StateTokenPrincipal?.GetClaim(Claims.Private.ProviderName)); + + return ( + AuthorizationResponse: context.Request is not null ? new(context.Request.GetParameters()) : new(), + TokenResponse : context.TokenResponse ?? new(), + Principal : principal); + } } finally @@ -185,15 +159,33 @@ public sealed class OpenIddictClientService } /// - /// Completes the interactive authentication demand corresponding to the specified nonce. + /// Initiates an interactive user authentication demand. /// - /// The nonce obtained after a challenge operation. + /// The client registration. + /// The scopes to request to the authorization server. + /// The additional parameters to send as part of the token request. + /// The application-specific properties that will be added to the authentication context. /// The that can be used to abort the operation. /// The response and a merged principal containing the claims extracted from the tokens and userinfo response. [RequiresPreviewFeatures] - public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateWithBrowserAsync( - string nonce, CancellationToken cancellationToken = default) + private async ValueTask ChallengeInteractivelyAsync( + OpenIddictClientRegistration registration, string[]? scopes = null, + Dictionary? parameters = null, + Dictionary? properties = null, CancellationToken cancellationToken = default) { + if (registration is null) + { + throw new ArgumentNullException(nameof(registration)); + } + + if (scopes is not null && Array.Exists(scopes, string.IsNullOrEmpty)) + { + throw new ArgumentException(SR.GetResourceString(SR.ID0074), nameof(scopes)); + } + + var configuration = await registration.ConfigurationManager.GetConfigurationAsync(default) ?? + throw new InvalidOperationException(SR.GetResourceString(SR.ID0140)); + cancellationToken.ThrowIfCancellationRequested(); // Note: this service is registered as a singleton service. As such, it cannot @@ -210,12 +202,29 @@ public sealed class OpenIddictClientService var transaction = await factory.CreateTransactionAsync(); - var context = new ProcessAuthenticationContext(transaction) + var context = new ProcessChallengeContext(transaction) { CancellationToken = cancellationToken, - Nonce = nonce + Configuration = configuration, + Issuer = registration.Issuer, + Principal = new ClaimsPrincipal(new ClaimsIdentity()), + Registration = registration, + Request = parameters is not null ? new(parameters) : new(), }; + if (scopes is { Length: > 0 }) + { + context.Scopes.UnionWith(scopes); + } + + if (properties is { Count: > 0 }) + { + foreach (var property in properties) + { + context.Properties[property.Key] = property.Value; + } + } + await dispatcher.DispatchAsync(context); if (context.IsRejected) @@ -225,23 +234,12 @@ public sealed class OpenIddictClientService context.Error, context.ErrorDescription, context.ErrorUri); } - else + if (string.IsNullOrEmpty(context.Nonce)) { - var principal = OpenIddictHelpers.CreateMergedPrincipal( - context.FrontchannelIdentityTokenPrincipal, - context.BackchannelIdentityTokenPrincipal, - context.UserinfoTokenPrincipal) ?? new ClaimsPrincipal(new ClaimsIdentity()); - - // Attach the identity of the authorization to the returned principal to allow resolving it even if no other - // claim was added to the principal (e.g when no id_token was returned and no userinfo endpoint is available). - principal.SetClaim(Claims.AuthorizationServer, context.StateTokenPrincipal?.GetClaim(Claims.AuthorizationServer)) - .SetClaim(Claims.Private.ProviderName, context.StateTokenPrincipal?.GetClaim(Claims.Private.ProviderName)); - - return ( - AuthorizationResponse: context.Request is not null ? new(context.Request.GetParameters()) : new(), - TokenResponse : context.TokenResponse ?? new(), - Principal : principal); + throw new InvalidOperationException(SR.GetResourceString(SR.ID0352)); } + + return context.Nonce; } finally diff --git a/src/OpenIddict/OpenIddict.csproj b/src/OpenIddict/OpenIddict.csproj index 18062f9c..db0a9e72 100644 --- a/src/OpenIddict/OpenIddict.csproj +++ b/src/OpenIddict/OpenIddict.csproj @@ -23,6 +23,7 @@ To use these features on ASP.NET Core or OWIN/Katana/ASP.NET 4.x, reference the +