Browse Source

Replace the client Windows integration by a cross-platform package and add HttpListener and WebAuthenticationBroker support

pull/1677/head
Kévin Chalet 3 years ago
parent
commit
9b5075e656
  1. 3
      Directory.Build.targets
  2. 2
      OpenIddict.sln
  3. 12
      sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config
  4. 12
      sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config
  5. 12
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  6. 34
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  7. 4
      sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj
  8. 17
      sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
  9. 34
      sandbox/OpenIddict.Sandbox.Console.Client/Worker.cs
  10. 11
      sandbox/OpenIddict.Sandbox.WinForms.Client/MainForm.cs
  11. 2
      sandbox/OpenIddict.Sandbox.WinForms.Client/OpenIddict.Sandbox.WinForms.Client.csproj
  12. 8
      sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs
  13. 11
      sandbox/OpenIddict.Sandbox.Wpf.Client/MainWindow.xaml.cs
  14. 2
      sandbox/OpenIddict.Sandbox.Wpf.Client/OpenIddict.Sandbox.Wpf.Client.csproj
  15. 8
      sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs
  16. 74
      shared/OpenIddict.Extensions/Helpers/OpenIddictHelpers.cs
  17. 29
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  18. 16
      src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj
  19. 10
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivation.cs
  20. 41
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivationHandler.cs
  21. 30
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs
  22. 238
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs
  23. 172
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs
  24. 14
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConstants.cs
  25. 65
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs
  26. 170
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs
  27. 308
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs
  28. 1723
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
  29. 67
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs
  30. 190
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs
  31. 24
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationMarshal.cs
  32. 103
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationOptions.cs
  33. 183
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationPipeListener.cs
  34. 208
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs
  35. 139
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsBuilder.cs
  36. 128
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsConfiguration.cs
  37. 68
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlerFilters.cs
  38. 125
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs
  39. 56
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs
  40. 92
      src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs
  41. 13
      src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs
  42. 18
      src/OpenIddict.Client/OpenIddictClientHandlers.Authentication.cs
  43. 9
      src/OpenIddict.Client/OpenIddictClientHandlers.Session.cs
  44. 10
      src/OpenIddict.Client/OpenIddictClientHandlers.cs
  45. 152
      src/OpenIddict.Client/OpenIddictClientService.cs
  46. 1
      src/OpenIddict/OpenIddict.csproj

3
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>$(DefineConstants);SUPPORTS_BROTLI_COMPRESSION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_CURRENT_USER_ONLY_PIPE_OPTION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_STATIC_RANDOM_NUMBER_GENERATOR_METHODS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_STREAM_MEMORY_METHODS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_TIME_CONSTANT_COMPARISONS</DefineConstants>
</PropertyGroup>
@ -97,6 +99,7 @@
<DefineConstants>$(DefineConstants);SUPPORTS_DIRECT_JSON_ELEMENT_SERIALIZATION</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_JSON_NODES</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_ONE_SHOT_RANDOM_NUMBER_GENERATOR_METHODS</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_TASK_WAIT_ASYNC</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_ZLIB_COMPRESSION</DefineConstants>
</PropertyGroup>

2
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

12
sandbox/OpenIddict.Sandbox.AspNet.Client/Web.config

@ -82,6 +82,12 @@
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.WindowsRuntime" publicKeyToken="b77a5c561934e089" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.14.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Text.Encodings.Web" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
@ -148,6 +154,12 @@
<bindingRedirect oldVersion="0.0.0.0-2.0.1.0" newVersion="2.0.1.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<system.codedom>
<compilers>

12
sandbox/OpenIddict.Sandbox.AspNet.Server/Web.config

@ -106,6 +106,12 @@
<bindingRedirect oldVersion="0.0.0.0-4.0.6.0" newVersion="4.0.6.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Runtime.WindowsRuntime" publicKeyToken="b77a5c561934e089" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.14.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Text.Encodings.Web" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
@ -172,6 +178,12 @@
<bindingRedirect oldVersion="0.0.0.0-2.0.1.0" newVersion="2.0.1.0" />
</dependentAssembly>
</assemblyBinding>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Numerics.Vectors" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.4.0" newVersion="4.1.4.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<entityFramework>
<providers>

12
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 =
{

34
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.");
}
}
}

4
sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj

@ -2,16 +2,16 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework>
<TargetFramework>net7.0</TargetFramework>
<EnablePreviewFeatures>true</EnablePreviewFeatures>
<IsShipping>false</IsShipping>
<SignAssembly>false</SignAssembly>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemIntegration\OpenIddict.Client.SystemIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
</ItemGroup>

17
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<DbContext>(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<InteractiveService>();
services.RemoveAll<ILoggerProvider>();
})
.UseConsoleLifetime()
.Build();

34
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<DbContext>();
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;

11
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
{

2
sandbox/OpenIddict.Sandbox.WinForms.Client/OpenIddict.Sandbox.WinForms.Client.csproj

@ -11,9 +11,9 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemIntegration\OpenIddict.Client.SystemIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
</ItemGroup>

8
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<DbContext>(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));

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

2
sandbox/OpenIddict.Sandbox.Wpf.Client/OpenIddict.Sandbox.Wpf.Client.csproj

@ -12,9 +12,9 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemIntegration\OpenIddict.Client.SystemIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.Client.Windows\OpenIddict.Client.Windows.csproj" />
<ProjectReference Include="..\..\src\OpenIddict.EntityFrameworkCore\OpenIddict.EntityFrameworkCore.csproj" />
</ItemGroup>

8
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<DbContext>(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));

74
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
/// <summary>
/// Waits until the specified task returns a result or the cancellation token is signaled.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="OperationCanceledException">The specified <paramref name="cancellationToken"/> is signaled.</exception>
public static async Task WaitAsync(this Task task, CancellationToken cancellationToken)
{
var source = new TaskCompletionSource<bool>(TaskCreationOptions.None);
using (cancellationToken.Register(static state => ((TaskCompletionSource<bool>) state!).SetResult(true), source))
{
if (await Task.WhenAny(task, source.Task) == source.Task)
{
throw new OperationCanceledException(cancellationToken);
}
await task;
}
}
/// <summary>
/// Waits until the specified task returns a result or the cancellation token is signaled.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="OperationCanceledException">The specified <paramref name="cancellationToken"/> is signaled.</exception>
public static async Task<T> WaitAsync<T>(this Task<T> task, CancellationToken cancellationToken)
{
var source = new TaskCompletionSource<bool>(TaskCreationOptions.None);
using (cancellationToken.Register(static state => ((TaskCompletionSource<bool>) state!).SetResult(true), source))
{
if (await Task.WhenAny(task, source.Task) == source.Task)
{
throw new OperationCanceledException(cancellationToken);
}
return await task;
}
}
#endif
/// <summary>
/// Determines whether the specified <paramref name="exception"/> is considered fatal.
/// </summary>
/// <param name="exception">The exception.</param>
/// <returns>
/// <see langword="true"/> if the exception is considered fatal, <see langword="false"/> otherwise.
/// </returns>
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
/// <summary>
/// Creates a new <see cref="HashSet{T}"/> instance and imports the elements present in the specified source.

29
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1424,7 +1424,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>An error occurred while authenticating the user.</value>
</data>
<data name="ID0375" xml:space="preserve">
<value>The Windows protocol activation cannot be resolved from the client transaction.</value>
<value>The protocol activation cannot be resolved or contains invalid data.</value>
</data>
<data name="ID0376" xml:space="preserve">
<value>The identifier of the application instance that initiated the authentication process cannot be resolved from the state token.</value>
@ -1451,13 +1451,13 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>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.</value>
</data>
<data name="ID0384" xml:space="preserve">
<value>An explicit client URI must be set when using the OpenIddict client Windows integration. To set the client URI, use 'services.AddOpenIddict().AddClient().SetClientUri()'.</value>
<value>An explicit client URI must be set when using the OpenIddict client system integration. To set the client URI, use 'services.AddOpenIddict().AddClient().SetClientUri()'.</value>
</data>
<data name="ID0385" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID0386" xml:space="preserve">
<value>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()'.</value>
<value>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()'.</value>
</data>
<data name="ID0387" xml:space="preserve">
<value>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.</value>
@ -1466,7 +1466,19 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>The payload extracted from the inter-process notification is malformed, incomplete or was created by a different version of the OpenIddict client library.</value>
</data>
<data name="ID0389" xml:space="preserve">
<value>The OpenIddict client Windows integration is not supported on this platform.</value>
<value>The OpenIddict client system integration is not supported on this platform.</value>
</data>
<data name="ID0390" xml:space="preserve">
<value>The HTTP listener context cannot be resolved or contains invalid data.</value>
</data>
<data name="ID0391" xml:space="preserve">
<value>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.</value>
</data>
<data name="ID0392" xml:space="preserve">
<value>The web authentication broker is not supported on this platform.</value>
</data>
<data name="ID0393" xml:space="preserve">
<value>The web authentication result cannot be resolved or contains invalid data.</value>
</data>
<data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value>
@ -1963,6 +1975,9 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID2165" xml:space="preserve">
<value>The issuer returned in the server configuration doesn't match the value set in the client registration options.</value>
</data>
<data name="ID2166" xml:space="preserve">
<value>The received authorization response is not valid for this instance of the application.</value>
</data>
<data name="ID4000" xml:space="preserve">
<value>The '{0}' parameter shouldn't be null or empty at this point.</value>
</data>
@ -2626,6 +2641,12 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6213" xml:space="preserve">
<value>An error occurred while handling an inter-process message.</value>
</data>
<data name="ID6214" xml:space="preserve">
<value>An error occurred while handling an HTTP listener request.</value>
</data>
<data name="ID6215" xml:space="preserve">
<value>An error occurred while redirecting a protocol activation to the '{Identifier}' instance.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

16
src/OpenIddict.Client.Windows/OpenIddict.Client.Windows.csproj → src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj

@ -4,8 +4,10 @@
<TargetFrameworks>
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 @@
</PropertyGroup>
<PropertyGroup>
<Description>Windows integration package for the OpenIddict client services.</Description>
<PackageTags>$(PackageTags);client;windows</PackageTags>
<Description>Operating system integration package for the OpenIddict client.</Description>
<PackageTags>$(PackageTags);client;linux;windows</PackageTags>
</PropertyGroup>
<ItemGroup>
@ -52,8 +54,14 @@
<Using Include="OpenIddict.Client.OpenIddictClientEvents" Static="true" />
<Using Include="OpenIddict.Client.OpenIddictClientHandlers" Static="true" />
<Using Include="OpenIddict.Client.OpenIddictClientHandlerFilters" Static="true" />
<Using Include="OpenIddict.Client.Windows.OpenIddictClientWindowsHandlers" Static="true" />
<Using Include="OpenIddict.Client.Windows.OpenIddictClientWindowsHandlerFilters" Static="true" />
<Using Include="OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrationHandlers" Static="true" />
<Using Include="OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrationHandlerFilters" Static="true" />
</ItemGroup>
<ItemGroup>
<SupportedPlatform Remove="@(SupportedPlatform)" />
<SupportedPlatform Include="linux" />
<SupportedPlatform Include="windows" />
</ItemGroup>
</Project>

10
src/OpenIddict.Client.Windows/OpenIddictClientWindowsActivation.cs → src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivation.cs

@ -6,20 +6,20 @@
using System.ComponentModel;
namespace OpenIddict.Client.Windows;
namespace OpenIddict.Client.SystemIntegration;
/// <summary>
/// Represents a Windows protocol activation.
/// Represents a protocol activation.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientWindowsActivation
public sealed class OpenIddictClientSystemIntegrationActivation
{
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsActivation"/> class.
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationActivation"/> class.
/// </summary>
/// <param name="uri">The protocol activation URI.</param>
/// <exception cref="ArgumentNullException"><paramref name="uri"/> is <see langword="null"/>.</exception>
public OpenIddictClientWindowsActivation(Uri uri)
public OpenIddictClientSystemIntegrationActivation(Uri uri)
{
if (uri is null)
{

41
src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandler.cs → 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;
/// <summary>
/// Contains the logic necessary to handle initial URI protocol activations.
/// </summary>
/// <remarks>
/// Note: redirected URI protocol activations are handled by <see cref="OpenIddictClientWindowsListener"/>.
/// Note: redirected URI protocol activations are handled by <see cref="OpenIddictClientSystemIntegrationPipeListener"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientWindowsHandler : IHostedService
public sealed class OpenIddictClientSystemIntegrationActivationHandler : IHostedService
{
private readonly IOptionsMonitor<OpenIddictClientWindowsOptions> _options;
private readonly OpenIddictClientWindowsService _service;
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
private readonly OpenIddictClientSystemIntegrationService _service;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsHandler"/> class.
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationActivationHandler"/> class.
/// </summary>
/// <param name="options">The OpenIddict client Windows integration options.</param>
/// <param name="service">The OpenIddict client Windows service.</param>
public OpenIddictClientWindowsHandler(
IOptionsMonitor<OpenIddictClientWindowsOptions> options,
OpenIddictClientWindowsService service)
/// <param name="options">The OpenIddict client system integration integration options.</param>
/// <param name="service">The OpenIddict client system integration service.</param>
public OpenIddictClientSystemIntegrationActivationHandler(
IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> 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;

30
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;
/// <summary>
/// Provides various settings needed to configure the OpenIddict client system integration.
/// </summary>
public enum OpenIddictClientSystemIntegrationAuthenticationMode
{
/// <summary>
/// Browser-based authentication.
/// </summary>
SystemBrowser = 0,
/// <summary>
/// Windows web authentication broker-based authentication.
/// </summary>
/// <remarks>
/// Note: the web authentication broker is only supported in UWP applications
/// and its use is generally not recommended due to its inherent limitations.
/// </remarks>
[SupportedOSPlatform("windows10.0.17763")]
WebAuthenticationBroker = 1
}

238
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;
/// <summary>
/// Exposes the necessary methods required to configure
/// the OpenIddict client system integration.
/// </summary>
public sealed class OpenIddictClientSystemIntegrationBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictClientSystemIntegrationBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictClientSystemIntegrationBuilder(IServiceCollection services)
=> Services = services ?? throw new ArgumentNullException(nameof(services));
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict client system integration configuration.
/// </summary>
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder Configure(Action<OpenIddictClientSystemIntegrationOptions> configuration)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Uses the Windows web authentication broker to start authentication flows.
/// </summary>
/// <remarks>
/// Note: the web authentication broker is only supported in UWP applications
/// and its use is generally not recommended due to its inherent limitations.
/// </remarks>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
[SupportedOSPlatform("windows10.0.17763")]
public OpenIddictClientSystemIntegrationBuilder UseWebAuthenticationBroker()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
}
#if SUPPORTS_WINDOWS_RUNTIME
if (!OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported())
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
}
return Configure(options => options.AuthenticationMode =
OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker);
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
#endif
}
/// <summary>
/// Uses the system browser to start authentication flows.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder UseSystemBrowser()
=> Configure(options => options.AuthenticationMode =
OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser);
/// <summary>
/// Sets the timeout after which authentication demands that
/// are not completed are automatically aborted by OpenIddict.
/// </summary>
/// <param name="timeout">The authentication timeout.</param>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder SetAuthenticationTimeout(TimeSpan timeout)
=> Configure(options => options.AuthenticationTimeout = timeout);
/// <summary>
/// Disables the built-in protocol activation processing logic.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder DisableActivationHandling()
=> Configure(options => options.EnableActivationHandling = false);
/// <summary>
/// Enables the built-in protocol activation processing logic.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder EnableActivationHandling()
=> Configure(options => options.EnableActivationHandling = true);
/// <summary>
/// Disables the built-in protocol activation redirection logic.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder DisableActivationRedirection()
=> Configure(options => options.EnableActivationRedirection = false);
/// <summary>
/// Enables the built-in protocol activation redirection logic.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder EnableActivationRedirection()
=> Configure(options => options.EnableActivationRedirection = true);
/// <summary>
/// Disables the built-in web server used to handle callbacks.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder DisableEmbeddedWebServer()
=> Configure(options => options.EnableEmbeddedWebServer = false);
/// <summary>
/// Enables the built-in web server used to handle callbacks.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder EnableEmbeddedWebServer()
=> Configure(options => options.EnableEmbeddedWebServer = true);
/// <summary>
/// Disables the pipe server used to process notifications (e.g protocol
/// activation redirections) sent by other instances of the application.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder DisablePipeServer()
=> Configure(options => options.EnablePipeServer = false);
/// <summary>
/// Enables the pipe server used to process protocol
/// activations redirected by other instances of the application.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder EnablePipeServer()
=> Configure(options => options.EnablePipeServer = true);
/// <summary>
/// Sets the identifier used to represent the current application
/// instance and redirect protocol activations when necessary.
/// </summary>
/// <param name="identifier">The identifier of the current instance.</param>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientSystemIntegrationBuilder SetInstanceIdentifier(string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier));
}
return Configure(options => options.InstanceIdentifier = identifier);
}
/// <summary>
/// Sets the base name of the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <param name="name">The name of the pipe.</param>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientSystemIntegrationBuilder SetPipeName(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(SR.FormatID0366(nameof(name)), nameof(name));
}
return Configure(options => options.PipeName = name);
}
/// <summary>
/// Sets the options applied to the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <param name="flags">The options flags applied to the pipe.</param>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientSystemIntegrationBuilder SetPipeOptions(PipeOptions flags)
=> Configure(options => options.PipeOptions = flags);
/// <summary>
/// Sets the security policy applied to the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <param name="security">The security policy applied to the pipe.</param>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced), SupportedOSPlatform("windows")]
public OpenIddictClientSystemIntegrationBuilder SetPipeSecurity(PipeSecurity security)
{
if (security is null)
{
throw new ArgumentNullException(nameof(security));
}
return Configure(options => options.PipeSecurity = security);
}
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns><see langword="true"/> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
/// <summary>
/// Serves as the default hash function.
/// </summary>
/// <returns>A hash code for the current object.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => base.GetHashCode();
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override string? ToString() => base.ToString();
}

172
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;
/// <summary>
/// Contains the methods required to ensure that the OpenIddict client system integration configuration is valid.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<OpenIddictClientSystemIntegrationOptions>
{
private readonly IHostEnvironment _environment;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationConfiguration"/> class.
/// </summary>
/// <param name="environment">The host environment.</param>
public OpenIddictClientSystemIntegrationConfiguration(IHostEnvironment environment)
=> _environment = environment ?? throw new ArgumentNullException(nameof(environment));
/// <inheritdoc/>
public void Configure(OpenIddictClientOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// Register the built-in event handlers used by the OpenIddict client system integration components.
options.Handlers.AddRange(OpenIddictClientSystemIntegrationHandlers.DefaultHandlers);
}
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// Ensure an explicit client URI was set when using the system integration.
if (options.ClientUri is not { IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0384));
}
}
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientSystemIntegrationOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389));
}
// Note: the OpenIddict client system integration is currently only supported on Windows
// and Linux. As such, using the system browser as the default authentication method in
// conjunction with the embedded web server and activation handling should be always supported.
options.AuthenticationMode ??= OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser;
options.EnableActivationHandling ??= true;
options.EnableActivationRedirection ??= true;
options.EnablePipeServer ??= true;
options.EnableEmbeddedWebServer ??= HttpListener.IsSupported;
// If no explicit instance identifier was specified, use a random GUID.
if (string.IsNullOrEmpty(options.InstanceIdentifier))
{
options.InstanceIdentifier = Guid.NewGuid().ToString();
}
// If no explicit pipe name was specified, compute the SHA-256 hash of the
// application name resolved from the host and use it as a unique identifier.
if (string.IsNullOrEmpty(options.PipeName))
{
if (string.IsNullOrEmpty(_environment.ApplicationName))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0386));
}
var builder = new StringBuilder();
// Note: on Windows, the name is deliberately prefixed with "LOCAL\" to support
// partial trust/sandboxed applications that are executed in an AppContainer
// and cannot communicate with applications outside the sandbox container.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
builder.Append(@"LOCAL\");
}
builder.Append(@"OpenIddict.Client.SystemIntegration-");
builder.Append(Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash(
Encoding.UTF8.GetBytes(_environment.ApplicationName))));
options.PipeName = builder.ToString();
}
#if SUPPORTS_CURRENT_USER_ONLY_PIPE_OPTION
if (options.PipeOptions is null && !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Note: the CurrentUserOnly option is also supported on Windows, but is less
// flexible than using a PipeSecurity object (e.g cross-process communication
// between elevated and non-elevated processes is not possible with this option).
// As such, it's not used on Windows (instead, an ACL-based PipeSecurity is used).
options.PipeOptions = PipeOptions.CurrentUserOnly;
}
#endif
// Always configure the pipe to use asynchronous operations,
// even if the flag was not explicitly set by the user.
options.PipeOptions |= PipeOptions.Asynchronous;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// If no explicit pipe security policy was specified, grant the current user
// full control over the created pipe and allow cross-process communication
// between elevated and non-elevated processes. Note: if the process executes
// inside an AppContainer, don't override the default OS pipe security policy
// to allow all applications with the same identity to access the named pipe.
if (options.PipeSecurity is null)
{
using var identity = WindowsIdentity.GetCurrent(TokenAccessLevels.Query);
if (!IsRunningInAppContainer(identity))
{
options.PipeSecurity = new PipeSecurity();
options.PipeSecurity.SetOwner(identity.User!);
options.PipeSecurity.AddAccessRule(new PipeAccessRule(identity.User!,
PipeAccessRights.FullControl, AccessControlType.Allow));
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
[SupportedOSPlatform("windows")]
static bool IsRunningInAppContainer(WindowsIdentity identity)
#if SUPPORTS_WINDOWS_RUNTIME
=> OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported() &&
OpenIddictClientSystemIntegrationHelpers.HasAppContainerToken(identity);
#else
=> false;
#endif
}
}
}

14
src/OpenIddict.Client.Windows/OpenIddictClientWindowsConstants.cs → 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;
/// <summary>
/// Exposes common constants used by the OpenIddict Windows host.
/// Exposes common constants used by the OpenIddict client system integration.
/// </summary>
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";

65
src/OpenIddict.Client.Windows/OpenIddictClientWindowsExtensions.cs → 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;
/// <summary>
/// Exposes extensions allowing to register the OpenIddict client services.
/// </summary>
public static class OpenIddictClientWindowsExtensions
public static class OpenIddictClientSystemIntegrationExtensions
{
/// <summary>
/// Registers the OpenIddict client services for Windows in the DI container.
/// Registers the OpenIddict client system integration services in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
public static OpenIddictClientWindowsBuilder UseWindows(this OpenIddictClientBuilder builder)
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
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<IHostedService, OpenIddictClientWindowsHandler>());
builder.Services.Insert(0, ServiceDescriptor.Singleton<IHostedService, OpenIddictClientSystemIntegrationActivationHandler>());
}
// Register the services responsible for coordinating and managing authentication operations.
builder.Services.TryAddSingleton<OpenIddictClientWindowsMarshal>();
builder.Services.TryAddSingleton<OpenIddictClientWindowsService>();
builder.Services.TryAddSingleton<OpenIddictClientSystemIntegrationMarshal>();
builder.Services.TryAddSingleton<OpenIddictClientSystemIntegrationService>();
// Register the built-in filters used by the default OpenIddict Windows client event handlers.
builder.Services.TryAddSingleton(static provider => provider.GetServices<IHostedService>()
.OfType<OpenIddictClientSystemIntegrationHttpListener>()
.Single());
// Register the built-in filters used by the default OpenIddict client system integration event handlers.
builder.Services.TryAddSingleton<RequireAuthenticationNonce>();
builder.Services.TryAddSingleton<RequireHttpListenerContext>();
builder.Services.TryAddSingleton<RequireInteractiveSession>();
builder.Services.TryAddSingleton<RequireWindowsActivation>();
builder.Services.TryAddSingleton<RequireProtocolActivation>();
builder.Services.TryAddSingleton<RequireSystemBrowser>();
builder.Services.TryAddSingleton<RequireWebAuthenticationBroker>();
builder.Services.TryAddSingleton<RequireWebAuthenticationResult>();
// 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<IHostedService, OpenIddictClientWindowsListener>(),
ServiceDescriptor.Singleton<IHostedService, OpenIddictClientSystemIntegrationHttpListener>(),
ServiceDescriptor.Singleton<IHostedService, OpenIddictClientSystemIntegrationPipeListener>(),
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWindowsConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientOptions>, OpenIddictClientWindowsConfiguration>(),
ServiceDescriptor.Singleton<IConfigureOptions<OpenIddictClientOptions>, OpenIddictClientSystemIntegrationConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientOptions>, OpenIddictClientSystemIntegrationConfiguration>(),
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientWindowsOptions>, OpenIddictClientWindowsConfiguration>()
ServiceDescriptor.Singleton<IPostConfigureOptions<OpenIddictClientSystemIntegrationOptions>, OpenIddictClientSystemIntegrationConfiguration>()
});
return new OpenIddictClientWindowsBuilder(builder.Services);
return new OpenIddictClientSystemIntegrationBuilder(builder.Services);
}
/// <summary>
/// Registers the OpenIddict client services for Windows in the DI container.
/// Registers the OpenIddict client system integration services in the DI container.
/// </summary>
/// <param name="builder">The services builder used by OpenIddict to register new services.</param>
/// <param name="configuration">The configuration delegate used to configure the client services.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientBuilder"/>.</returns>
public static OpenIddictClientBuilder UseWindows(
this OpenIddictClientBuilder builder, Action<OpenIddictClientWindowsBuilder> configuration)
public static OpenIddictClientBuilder UseSystemIntegration(
this OpenIddictClientBuilder builder, Action<OpenIddictClientSystemIntegrationBuilder> 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;
}

170
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;
/// <summary>
/// Contains a collection of event handler filters commonly used by the system integration handlers.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientSystemIntegrationHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers
/// if no explicit nonce was attached to the authentication context.
/// </summary>
public sealed class RequireAuthenticationNonce : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.Nonce));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no HTTP listener context can be found.
/// </summary>
public sealed class RequireHttpListenerContext : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.Transaction.GetHttpListenerContext() is not null);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no interactive user session was detected.
/// </summary>
public sealed class RequireInteractiveSession : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(Environment.UserInteractive);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no protocol activation was found.
/// </summary>
public sealed class RequireProtocolActivation : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.Transaction.GetProtocolActivation() is not null);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers
/// if the system browser integration was not enabled.
/// </summary>
public sealed class RequireSystemBrowser : IOpenIddictClientHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
public RequireSystemBrowser(IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.Transaction.Properties.TryGetValue(
typeof(OpenIddictClientSystemIntegrationAuthenticationMode).FullName!, out var result) ||
result is not OpenIddictClientSystemIntegrationAuthenticationMode mode)
{
mode = _options.CurrentValue.AuthenticationMode.GetValueOrDefault();
}
return new(mode is OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if
/// the web authentication broker integration was not enabled.
/// </summary>
public sealed class RequireWebAuthenticationBroker : IOpenIddictClientHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
public RequireWebAuthenticationBroker(IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options)
=> _options = options ?? throw new ArgumentNullException(nameof(options));
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (!context.Transaction.Properties.TryGetValue(
typeof(OpenIddictClientSystemIntegrationAuthenticationMode).FullName!, out var result) ||
result is not OpenIddictClientSystemIntegrationAuthenticationMode mode)
{
mode = _options.CurrentValue.AuthenticationMode.GetValueOrDefault();
}
#pragma warning disable CA1416
return new(mode is OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker);
#pragma warning restore CA1416
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no
/// web authentication operation was triggered during the transaction.
/// </summary>
public sealed class RequireWebAuthenticationResult : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
#if SUPPORTS_WINDOWS_RUNTIME
return new(context.Transaction.GetWebAuthenticationResult() is not null);
#else
return new(false);
#endif
}
}
}

308
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<OpenIddictClientHandlerDescriptor> DefaultHandlers { get; } = ImmutableArray.Create(
/*
* Authorization request processing:
*/
InvokeWebAuthenticationBroker.Descriptor,
LaunchSystemBrowser.Descriptor,
/*
* Redirection request extraction:
*/
ExtractGetHttpListenerRequest<ExtractRedirectionRequestContext>.Descriptor,
ExtractProtocolActivationParameters<ExtractRedirectionRequestContext>.Descriptor,
ExtractWebAuthenticationResultData<ExtractRedirectionRequestContext>.Descriptor,
/*
* Redirection response handling:
*/
AttachHttpResponseCode<ApplyRedirectionResponseContext>.Descriptor,
AttachCacheControlHeader<ApplyRedirectionResponseContext>.Descriptor,
ProcessEmptyHttpResponse.Descriptor,
ProcessUnactionableResponse<ApplyRedirectionResponseContext>.Descriptor);
/// <summary>
/// Contains the logic responsible for initiating authorization requests using the web authentication broker.
/// Note: this handler is not used when the user session is not interactive.
/// </summary>
public class InvokeWebAuthenticationBroker : IOpenIddictClientHandler<ApplyAuthorizationRequestContext>
{
private readonly OpenIddictClientSystemIntegrationService _service;
public InvokeWebAuthenticationBroker(OpenIddictClientSystemIntegrationService service)
=> _service = service ?? throw new ArgumentNullException(nameof(service));
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyAuthorizationRequestContext>()
.AddFilter<RequireInteractiveSession>()
.AddFilter<RequireWebAuthenticationBroker>()
.UseSingletonHandler<InvokeWebAuthenticationBroker>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
#pragma warning disable CS1998
public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context)
#pragma warning restore CS1998
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
#if SUPPORTS_WINDOWS_RUNTIME
if (string.IsNullOrEmpty(context.RedirectUri))
{
return;
}
// OpenIddict represents the complete interactive authentication dance as a two-phase process:
// - The challenge, during which the user is redirected to the authorization server, either
// by launching the system browser or, as in this case, using a web-view-like approach.
//
// - The authentication that takes place after the authorization server and the user approved
// the demand and redirected the user agent to the client (using either protocol activation,
// an embedded web server or by tracking the return URL of the web view created for the process).
//
// Unlike OpenIddict, WebAuthenticationBroker materializes this process as a single/one-shot API
// that opens the system-managed authentication host, navigates to the specified request URI and
// doesn't return until the specified callback URI is reached or the modal closed by the user.
// To accomodate OpenIddict's model, successful results are processed as any other callback request.
try
{
// Note: IAsyncOperation<T>.AsTask(context.CancellationToken) is deliberately not used here as
// the asynchronous operation returned by the web authentication broker is not cancellable.
switch (await WebAuthenticationBroker.AuthenticateAsync(
options : WebAuthenticationOptions.None,
requestUri : OpenIddictHelpers.AddQueryStringParameters(
uri: new Uri(context.AuthorizationEndpoint, UriKind.Absolute),
parameters: context.Transaction.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => new StringValues((string?[]?) parameter.Value))),
callbackUri: new Uri(context.RedirectUri, UriKind.Absolute)))
{
case { ResponseStatus: WebAuthenticationStatus.Success } result:
await _service.HandleWebAuthenticationResultAsync(result, context.CancellationToken);
context.HandleRequest();
return;
// Since the result of this operation is known by the time WebAuthenticationBroker.AuthenticateAsync()
// returns, some errors can directly be handled and surfaced here, as part of the challenge handling.
case { ResponseStatus: WebAuthenticationStatus.UserCancel }:
context.Reject(
error: Errors.AccessDenied,
description: SR.GetResourceString(SR.ID2149),
uri: SR.FormatID8000(SR.ID2149));
return;
case { ResponseStatus: WebAuthenticationStatus.ErrorHttp } result:
context.Reject(
error: result.ResponseErrorDetail switch
{
400 => Errors.InvalidRequest,
401 => Errors.InvalidToken,
403 => Errors.InsufficientAccess,
429 => Errors.SlowDown,
500 => Errors.ServerError,
503 => Errors.TemporarilyUnavailable,
_ => Errors.ServerError
},
description: SR.FormatID2161(result.ResponseErrorDetail),
uri: SR.FormatID8000(SR.ID2161));
return;
default:
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2136),
uri: SR.FormatID8000(SR.ID2136));
return;
}
}
catch
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2136),
uri: SR.FormatID8000(SR.ID2136));
return;
}
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
#endif
}
}
/// <summary>
/// Contains the logic responsible for initiating authorization requests using the system browser.
/// Note: this handler is not used when the user session is not interactive.
/// </summary>
public class LaunchSystemBrowser : IOpenIddictClientHandler<ApplyAuthorizationRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyAuthorizationRequestContext>()
.AddFilter<RequireInteractiveSession>()
.AddFilter<RequireSystemBrowser>()
.UseSingletonHandler<LaunchSystemBrowser>()
.SetOrder(InvokeWebAuthenticationBroker.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ApplyAuthorizationRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
var uri = OpenIddictHelpers.AddQueryStringParameters(
uri: new Uri(context.AuthorizationEndpoint, UriKind.Absolute),
parameters: context.Transaction.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => new StringValues((string?[]?) parameter.Value)));
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Note: on Windows, multiple application models exist and must be supported to cover most scenarios:
//
// - Classical Win32 applications, for which no application-specific restriction is enforced.
// - Win32 applications running in an AppContainer, that are very similar to UWP applications.
// - Classical UWP applications, for which strict application restrictions are enforced.
// - Full-trust UWP applications, that are rare but very similar to classical Win32 applications.
// - Modern/hybrid Windows applications, that can be sandboxed or run as full-trust applications.
//
// Since .NET Standard 2.0 support for UWP was only introduced in Windows 10 1709 (also known
// as Fall Creators Update) and OpenIddict requires Windows 10 1809 as the minimum supported
// version, Windows 8/8.1's Metro-style/universal applications are deliberately not supported.
//
// While Process.Start()/ShellExecuteEx() can typically be used without any particular restriction
// by non-sandboxed desktop applications to launch the default system browser, calling these
// APIs in sandboxed applications will result in an UnauthorizedAccessException being thrown.
//
// To avoid that, the OpenIddict host needs to determine whether the platform supports Windows
// Runtime APIs and favor the Launcher.LaunchUriAsync() API when it's offered by the platform.
#if SUPPORTS_WINDOWS_RUNTIME
if (OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported() && await
OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithWindowsRuntimeAsync(uri))
{
context.HandleRequest();
return;
}
#endif
if (await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithShellExecuteAsync(uri))
{
context.HandleRequest();
return;
}
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri))
{
context.HandleRequest();
return;
}
throw new InvalidOperationException(SR.GetResourceString(SR.ID0385));
}
}
/// <summary>
/// Contains the logic responsible for processing OpenID Connect responses that don't specify any parameter.
/// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server.
/// </summary>
public sealed class ProcessEmptyHttpResponse : IOpenIddictClientHandler<ApplyRedirectionResponseContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ApplyRedirectionResponseContext>()
.AddFilter<RequireHttpListenerContext>()
.UseSingletonHandler<ProcessEmptyHttpResponse>()
.SetOrder(int.MaxValue - 100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public async ValueTask HandleAsync(ApplyRedirectionResponseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
Debug.Assert(context.Transaction.Response is not null, SR.GetResourceString(SR.ID4007));
// This handler only applies to HTTP listener requests. If the HTTP context cannot be resolved,
// this may indicate that the request was incorrectly processed by another server stack.
var response = context.Transaction.GetHttpListenerContext()?.Response ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0390));
// Always return a 200 status, even for responses indicating that the authentication failed.
response.StatusCode = 200;
response.ContentType = "text/plain";
// Return a message indicating whether the authentication process
// succeeded or failed and that will be visible by the user.
var buffer = Encoding.UTF8.GetBytes(context.Transaction.Response.Error switch
{
null or { Length: 0 } => "Login completed. Please return to the application.",
Errors.AccessDenied => "Authorization denied. Please return to the application.",
_ => "Authentication failed. Please return to the application."
});
#if SUPPORTS_STREAM_MEMORY_METHODS
await response.OutputStream.WriteAsync(buffer);
#else
await response.OutputStream.WriteAsync(buffer, 0, buffer.Length);
#endif
await response.OutputStream.FlushAsync();
context.HandleRequest();
}
}
}
}

1723
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs

File diff suppressed because it is too large

67
src/OpenIddict.Client.Windows/OpenIddictClientWindowsHelpers.cs → 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;
/// <summary>
/// Exposes companion extensions for the OpenIddict/Windows integration.
/// </summary>
public static class OpenIddictClientWindowsHelpers
public static class OpenIddictClientSystemIntegrationHelpers
{
/// <summary>
/// Gets the <see cref="OpenIddictClientWindowsActivation"/> associated with the current context.
/// Gets the <see cref="OpenIddictClientSystemIntegrationActivation"/> associated with the current context.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="OpenIddictClientWindowsActivation"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static OpenIddictClientWindowsActivation? GetWindowsActivation(this OpenIddictClientTransaction transaction)
=> transaction.GetProperty<OpenIddictClientWindowsActivation>(typeof(OpenIddictClientWindowsActivation).FullName!);
/// <returns>The <see cref="OpenIddictClientSystemIntegrationActivation"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static OpenIddictClientSystemIntegrationActivation? GetProtocolActivation(this OpenIddictClientTransaction transaction)
=> transaction.GetProperty<OpenIddictClientSystemIntegrationActivation>(typeof(OpenIddictClientSystemIntegrationActivation).FullName!);
/// <summary>
/// Gets the <see cref="HttpListenerContext"/> associated with the current context.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="HttpListenerContext"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static HttpListenerContext? GetHttpListenerContext(this OpenIddictClientTransaction transaction)
=> transaction.GetProperty<HttpListenerContext>(typeof(HttpListenerContext).FullName!);
#if SUPPORTS_WINDOWS_RUNTIME
/// <summary>
/// Gets the <see cref="WebAuthenticationResult"/> associated with the current context.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="HttpListenerContext"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static WebAuthenticationResult? GetWebAuthenticationResult(this OpenIddictClientTransaction transaction)
=> transaction.GetProperty<WebAuthenticationResult>(typeof(WebAuthenticationResult).FullName!);
/// <summary>
/// Determines whether the Windows Runtime APIs are supported on this platform.
/// </summary>
/// <returns><see langword="true"/> if the Windows Runtime APIs are supported, <see langword="false"/> otherwise.</returns>
[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;
}
}
/// <summary>
/// Starts the system browser using xdg-open.
/// </summary>
/// <param name="uri">The <see cref="Uri"/> to use.</param>
/// <returns><see langword="true"/> if the browser could be started, <see langword="false"/> otherwise.</returns>
[SupportedOSPlatform("linux")]
internal static async Task<bool> 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;
}
}
}

190
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;
/// <summary>
/// Contains the logic necessary to handle URI protocol activations that
/// are redirected by other instances using inter-process communication.
/// </summary>
/// <remarks>
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientSystemIntegrationActivationHandler"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientSystemIntegrationHttpListener : BackgroundService
{
private readonly TaskCompletionSource<int?> _source = new();
private readonly ILogger<OpenIddictClientSystemIntegrationHttpListener> _logger;
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
private readonly OpenIddictClientSystemIntegrationService _service;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationHttpListener"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="options">The OpenIddict client system integration options.</param>
/// <param name="service">The OpenIddict client system integration service.</param>
public OpenIddictClientSystemIntegrationHttpListener(
ILogger<OpenIddictClientSystemIntegrationHttpListener> logger,
IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options,
OpenIddictClientSystemIntegrationService service)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
_service = service ?? throw new ArgumentNullException(nameof(service));
}
/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// If the embedded web server instantiation was not enabled, signal the task completion source with a
// null value to inform the handlers that no HTTP listener is going to be created and return immediately.
if (_options.CurrentValue.EnableEmbeddedWebServer is not true)
{
_source.SetResult(result: null);
return;
}
try
{
// Note: finding a free port in the IANA dynamic port range can take a bit of time on busy systems.
// To ensure the host initialization is not blocked, the whole process is offloaded to the thread pool.
await Task.Run(cancellationToken: stoppingToken, function: async () =>
{
var (listener, port) = CreateHttpListener(stoppingToken) ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0391));
// Inform the handlers that the HTTP listener was created and can now be accessed via the specified port.
_source.SetResult(port);
using (listener)
{
// Note: while the received load should be minimal, 3 task workers are used
// to be able to process multiple requests at the same time, if necessary.
var tasks = new Task[3];
for (var index = 0; index < tasks.Length; index++)
{
tasks[index] = ProcessRequestsAsync(listener, _service, _logger, stoppingToken);
}
// Wait for all the workers to indicate they finished processing incoming requests.
await Task.WhenAll(tasks);
}
});
}
// Ignore exceptions indicating that the host is shutting down and return immediately.
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
return;
}
static (HttpListener Listener, int Port)? CreateHttpListener(CancellationToken cancellationToken)
{
// Note: HttpListener doesn't offer a native way to select a random, non-busy port.
// To work around this limitation, this local function tries to bind an HttpListener
// on the first free port in the IANA dynamic ports range (typically: 49152 to 65535).
//
// For more information, see
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml.
for (var port = 49152; port < 65535; port++)
{
cancellationToken.ThrowIfCancellationRequested();
var listener = new HttpListener
{
AuthenticationSchemes = AuthenticationSchemes.Anonymous,
IgnoreWriteExceptions = true,
// Note: the prefix registration is deliberately not configurable to ensure
// only the "localhost" authority is used, which enforces the built-in host
// validation performed by HTTP.sys (or the managed .NET implementation on
// non-Windows operating systems) and doesn't require running the application
// as an administrator or adding a namespace reservation/ACL rule on Windows.
Prefixes = { $"http://localhost:{port.ToString(CultureInfo.InvariantCulture)}/" }
};
try
{
listener.Start();
return (listener, port);
}
catch (HttpListenerException)
{
listener.Close();
}
}
return null;
}
static async Task ProcessRequestsAsync(HttpListener listener, OpenIddictClientSystemIntegrationService service,
ILogger<OpenIddictClientSystemIntegrationHttpListener> logger, CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
// Note: HttpListener.GetContextAsync() doesn't support cooperative cancellation. To ensure the host
// can gracefully shut down without being blocked by an asynchronous call that would never complete,
// Task.WaitAsync() is used to stop waiting on the task returned by HttpListener.GetContextAsync()
// when the CancellationToken provided by the host indicates that the application is about to shut down.
var context = await listener.GetContextAsync().WaitAsync(cancellationToken);
using (context.Response)
{
// Only process requests for which the request URL could be decoded/parsed correctly.
if (context.Request.Url is { IsAbsoluteUri: true })
{
await service.HandleHttpRequestAsync(context, cancellationToken);
}
}
}
// Surface operation canceled exceptions when the host is shutting down.
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
// Swallow other exceptions to ensure the worker doesn't exit when encountering an exception.
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
logger.LogWarning(exception, SR.GetResourceString(SR.ID6214));
continue;
}
}
}
}
/// <summary>
/// Resolves the port associated to the <see cref="HttpListener"/> created by this service, or
/// <see langword="null"/> if the embedded web server instantiation was disabled in the options.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>
/// A <see cref="Task"/> that can be used to monitor the asynchronous operation, whose result
/// returns the port associated to the <see cref="HttpListener"/> created by this service, or
/// <see langword="null"/> if the embedded web server instantiation was disabled in the options.
/// </returns>
internal Task<int?> GetEmbeddedServerPortAsync(CancellationToken cancellationToken = default)
=> _source.Task.WaitAsync(cancellationToken);
}

24
src/OpenIddict.Client.Windows/OpenIddictClientWindowsMarshal.cs → 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;
/// <summary>
/// Contains the APIs needed to coordinate authentication operations that happen in a different context.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientWindowsMarshal
public sealed class OpenIddictClientSystemIntegrationMarshal
{
private readonly ConcurrentDictionary<string, Lazy<(
string RequestForgeryProtection,
@ -34,8 +35,10 @@ public sealed class OpenIddictClientWindowsMarshal
/// <param name="nonce">The nonce, used as a unique identifier.</param>
/// <param name="protection">The request forgery protection associated with the specified authentication demand.</param>
/// <returns><see langword="true"/> if the operation could be added, <see langword="false"/> otherwise.</returns>
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))));
/// <summary>
/// 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<bool>(TaskCreationOptions.None);
using (cancellationToken.Register(static state => ((TaskCompletionSource<bool>) 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;
}
/// <summary>

103
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;
/// <summary>
/// Provides various settings needed to configure the OpenIddict client system integration.
/// </summary>
public sealed class OpenIddictClientSystemIntegrationOptions
{
/// <summary>
/// Gets or sets the authentication mode used to start authentication flows.
/// </summary>
/// <remarks>
/// If this property is not explicitly set, its value is automatically set by OpenIddict.
/// </remarks>
public OpenIddictClientSystemIntegrationAuthenticationMode? AuthenticationMode { get; set; }
/// <summary>
/// Gets or sets the timeout after which authentication demands
/// that are not completed are automatically aborted by OpenIddict.
/// </summary>
public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10);
/// <summary>
/// Gets or sets a boolean indicating whether protocol activation processing should be enabled.
/// </summary>
/// <remarks>
/// If this property is not explicitly set, its value is automatically set by OpenIddict
/// depending on the capabilities of the system on which the application is running.
/// </remarks>
public bool? EnableActivationHandling { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether protocol activation redirection should be enabled.
/// </summary>
/// <remarks>
/// If this property is not explicitly set, its value is automatically set by OpenIddict
/// depending on the capabilities of the system on which the application is running.
/// </remarks>
public bool? EnableActivationRedirection { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a local web server
/// should be started on a random port to handle callbacks.
/// </summary>
/// <remarks>
/// If this property is not explicitly set, its value is automatically set by OpenIddict
/// depending on the capabilities of the system on which the application is running.
/// </remarks>
public bool? EnableEmbeddedWebServer { get; set; }
/// <summary>
/// Gets or sets a boolean indicating whether a pipe server should be started to process notifications
/// (e.g protocol activation redirections) sent by other instances of the application.
/// </summary>
/// <remarks>
/// If this property is not explicitly set, its value is automatically set by OpenIddict
/// depending on the capabilities of the system on which the application is running.
/// </remarks>
public bool? EnablePipeServer { get; set; }
/// <summary>
/// Gets or sets the identifier used to represent the current application
/// instance and redirect protocol activations when necessary.
/// </summary>
public string? InstanceIdentifier { get; set; }
/// <summary>
/// Gets or sets the base name of the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <remarks>
/// If no value is explicitly set, a default name is automatically computed.
/// </remarks>
public string PipeName { get; set; } = default!;
/// <summary>
/// Gets or sets the pipe options applied to the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <remarks>
/// If no value is explicitly set, a default combination is automatically used.
/// </remarks>
public PipeOptions? PipeOptions { get; set; }
/// <summary>
/// Gets or sets the security policy applied to the pipe created by OpenIddict
/// to enable inter-process communication and handle protocol activation redirections.
/// </summary>
/// <remarks>
/// If no value is explicitly set, a default policy is automatically created
/// (unless the application is running inside an AppContainer sandbox).
/// </remarks>
[SupportedOSPlatform("windows")]
public PipeSecurity? PipeSecurity { get; set; }
}

183
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;
/// <summary>
/// Contains the logic necessary to handle URI protocol activations that
/// are redirected by other instances using inter-process communication.
/// </summary>
/// <remarks>
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientSystemIntegrationActivationHandler"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientSystemIntegrationPipeListener : BackgroundService
{
private readonly ILogger<OpenIddictClientSystemIntegrationPipeListener> _logger;
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
private readonly OpenIddictClientSystemIntegrationService _service;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationPipeListener"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="options">The OpenIddict client system integration options.</param>
/// <param name="service">The OpenIddict client system integration service.</param>
public OpenIddictClientSystemIntegrationPipeListener(
ILogger<OpenIddictClientSystemIntegrationPipeListener> logger,
IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options,
OpenIddictClientSystemIntegrationService service)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
_service = service ?? throw new ArgumentNullException(nameof(service));
}
/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
if (_options.CurrentValue.EnablePipeServer is not true)
{
return;
}
try
{
// Offload the whole process to avoid delaying the initialization of the host.
await Task.Run(cancellationToken: stoppingToken, function: async () =>
{
// Note: while the received load should be minimal, 3 task workers are used
// to be able to process multiple notifications at the same time, if necessary.
var tasks = new Task[3];
for (var index = 0; index < tasks.Length; index++)
{
tasks[index] = ProcessNotificationsAsync(_service, _logger, _options.CurrentValue, stoppingToken);
}
// Wait for all the workers to indicate they finished processing incoming notifications.
await Task.WhenAll(tasks);
});
}
// Ignore exceptions indicating that the host is shutting down and return immediately.
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
return;
}
static async Task ProcessNotificationsAsync(
OpenIddictClientSystemIntegrationService service, ILogger<OpenIddictClientSystemIntegrationPipeListener> logger,
OpenIddictClientSystemIntegrationOptions options, CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
using var buffer = new MemoryStream();
using var reader = new BinaryReader(buffer);
using var stream = CreatePipeServerStream(options);
// Wait for a writer to connect to the named pipe.
//
// Note: NamedPipeServerStream supports cooperative cancellation but it appears that cancellations
// are not always properly handled in some obscure circumstances. To ensure the application shutdown
// is not delayed by this issue, the Task.WaitAsync(CancellationToken) API is used to stop waiting
// for the task returned by WaitForConnectionAsync() to complete when the application shuts down.
await stream.WaitForConnectionAsync(cancellationToken).WaitAsync(cancellationToken);
// Copy the content to the memory stream asynchronously and rewind it.
await stream.CopyToAsync(buffer, bufferSize: 81_920, cancellationToken);
buffer.Seek(0L, SeekOrigin.Begin);
// Process the inter-process notification based on its declared type.
await (reader.ReadInt32() switch
{
0x01 when ReadProtocolActivation(reader) is var activation
=> service.HandleProtocolActivationAsync(activation, cancellationToken),
var value => throw new InvalidOperationException(SR.FormatID0387(value))
});
}
// Ignore exceptions indicating that the host is shutting down and return immediately.
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
throw;
}
// Swallow other exceptions to ensure the service doesn't exit when encountering an exception.
catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception))
{
logger.LogWarning(exception, SR.GetResourceString(SR.ID6213));
continue;
}
}
}
static NamedPipeServerStream CreatePipeServerStream(OpenIddictClientSystemIntegrationOptions options)
// Note: the ACL-based PipeSecurity class is only supported on Windows. On other operating systems,
// PipeOptions.CurrentUserOnly can be used as an alternative, but only for TFMs that implement it.
=> RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ?
#if SUPPORTS_NAMED_PIPE_CONSTRUCTOR_WITH_ACL
new NamedPipeServerStream(
#elif SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL
NamedPipeServerStreamAcl.Create(
#else
NamedPipeServerStreamConstructors.New(
#endif
pipeName : $@"{options.PipeName}\{options.InstanceIdentifier}",
direction : PipeDirection.In,
maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances,
transmissionMode : PipeTransmissionMode.Byte,
options : options.PipeOptions.GetValueOrDefault(),
inBufferSize : 0,
outBufferSize : 0,
pipeSecurity : options.PipeSecurity,
inheritability : HandleInheritability.None,
additionalAccessRights : default) :
new NamedPipeServerStream(
pipeName : $@"{options.PipeName}\{options.InstanceIdentifier}",
direction : PipeDirection.In,
maxNumberOfServerInstances: NamedPipeServerStream.MaxAllowedServerInstances,
transmissionMode : PipeTransmissionMode.Byte,
options : options.PipeOptions.GetValueOrDefault(),
inBufferSize : 0,
outBufferSize : 0);
static OpenIddictClientSystemIntegrationActivation ReadProtocolActivation(BinaryReader reader)
{
// Ensure the binary serialization format is supported.
var version = reader.ReadInt32();
if (version is not 0x01)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0388));
}
var value = reader.ReadString();
if (string.IsNullOrEmpty(value) || !Uri.TryCreate(value, UriKind.Absolute, out Uri? uri))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0388));
}
return new OpenIddictClientSystemIntegrationActivation(uri)
{
IsActivationRedirected = true
};
}
}
}

208
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;
/// <summary>
/// Contains the logic necessary to handle URI protocol activations (that
/// are typically resolved when launching the application or redirected
/// by other instances using inter-process communication).
/// </summary>
public sealed class OpenIddictClientSystemIntegrationService
{
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientSystemIntegrationService"/> class.
/// </summary>
/// <param name="options">The OpenIddict client system integration options.</param>
/// <param name="provider">The service provider.</param>
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
public OpenIddictClientSystemIntegrationService(
IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> options,
IServiceProvider provider)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_provider = provider ?? throw new ArgumentNullException(nameof(provider));
}
/// <summary>
/// Handles the specified protocol activation.
/// </summary>
/// <param name="activation">The protocol activation details.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="activation"/> is <see langword="null"/>.</exception>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public Task HandleProtocolActivationAsync(OpenIddictClientSystemIntegrationActivation activation,
CancellationToken cancellationToken = default)
{
if (activation is null)
{
throw new ArgumentNullException(nameof(activation));
}
return HandleRequestAsync(activation, cancellationToken);
}
/// <summary>
/// Handles the specified HTTP request.
/// </summary>
/// <param name="request">The HTTP request received by the embedded web server.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="request"/> is <see langword="null"/>.</exception>
internal Task HandleHttpRequestAsync(HttpListenerContext request, CancellationToken cancellationToken = default)
{
if (request is null)
{
throw new ArgumentNullException(nameof(request));
}
return HandleRequestAsync(request, cancellationToken);
}
#if SUPPORTS_WINDOWS_RUNTIME
/// <summary>
/// Handles the specified web authentication result.
/// </summary>
/// <param name="result">The web authentication result.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="result"/> is <see langword="null"/>.</exception>
internal Task HandleWebAuthenticationResultAsync(WebAuthenticationResult result, CancellationToken cancellationToken = default)
{
if (result is null)
{
throw new ArgumentNullException(nameof(result));
}
return HandleRequestAsync(result, cancellationToken);
}
#endif
/// <summary>
/// Handles the request using the specified property.
/// </summary>
/// <param name="property">The property to add to the transaction.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="property"/> is <see langword="null"/>.</exception>
private async Task HandleRequestAsync<TProperty>(TProperty property, CancellationToken cancellationToken) where TProperty : class
{
if (property is null)
{
throw new ArgumentNullException(nameof(property));
}
cancellationToken.ThrowIfCancellationRequested();
var scope = _provider.CreateScope();
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
// Create a client transaction and store the specified instance so
// it can be retrieved by the event handlers that need to access it.
var transaction = await factory.CreateTransactionAsync();
transaction.SetProperty(typeof(TProperty).FullName!, property);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = cancellationToken
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
await dispatcher.DispatchAsync(new ProcessErrorContext(transaction)
{
CancellationToken = cancellationToken,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
Response = new OpenIddictResponse()
});
}
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
/// <summary>
/// Redirects a protocol activation to the specified instance.
/// </summary>
/// <param name="activation">The protocol activation to redirect.</param>
/// <param name="identifier">The identifier of the target instance.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="activation"/> is <see langword="null"/>.</exception>
internal async Task RedirectProtocolActivationAsync(
OpenIddictClientSystemIntegrationActivation activation,
string identifier, CancellationToken cancellationToken = default)
{
if (activation is null)
{
throw new ArgumentNullException(nameof(activation));
}
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier));
}
using var buffer = new MemoryStream();
using var writer = new BinaryWriter(buffer);
using var stream = new NamedPipeClientStream(
serverName : ".",
pipeName : $@"{_options.CurrentValue.PipeName}\{identifier}",
direction : PipeDirection.Out,
options : PipeOptions.Asynchronous,
impersonationLevel: TokenImpersonationLevel.None,
inheritability : HandleInheritability.None);
// Wait for the target to accept the pipe connection.
await stream.ConnectAsync(cancellationToken);
// Write the type of message stored in the shared memory and the
// version used to identify the binary serialization format.
writer.Write(0x01);
writer.Write(0x01);
// Write the protocol activation URI.
writer.Write(activation.ActivationUri.AbsoluteUri);
// Transfer the payload to the target.
buffer.Seek(0L, SeekOrigin.Begin);
await buffer.CopyToAsync(stream, bufferSize: 81_920, cancellationToken);
}
}

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

@ -1,139 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
using System.IO.Pipes;
using OpenIddict.Client.Windows;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// Exposes the necessary methods required to configure
/// the OpenIddict client Windows integration.
/// </summary>
public sealed class OpenIddictClientWindowsBuilder
{
/// <summary>
/// Initializes a new instance of <see cref="OpenIddictClientWindowsBuilder"/>.
/// </summary>
/// <param name="services">The services collection.</param>
public OpenIddictClientWindowsBuilder(IServiceCollection services)
=> Services = services ?? throw new ArgumentNullException(nameof(services));
/// <summary>
/// Gets the services collection.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public IServiceCollection Services { get; }
/// <summary>
/// Amends the default OpenIddict client Windows configuration.
/// </summary>
/// <param name="configuration">The delegate used to configure the OpenIddict options.</param>
/// <remarks>This extension can be safely called multiple times.</remarks>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
public OpenIddictClientWindowsBuilder Configure(Action<OpenIddictClientWindowsOptions> configuration)
{
if (configuration is null)
{
throw new ArgumentNullException(nameof(configuration));
}
Services.Configure(configuration);
return this;
}
/// <summary>
/// Disables the built-in protocol activation processing logic, which
/// can be used to offload this task a separate dedicated executable.
/// </summary>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
public OpenIddictClientWindowsBuilder DisableProtocolActivationProcessing()
=> Configure(options => options.DisableProtocolActivationProcessing = true);
/// <summary>
/// Sets the timeout after which authentication demands that
/// are not completed are automatically aborted by OpenIddict.
/// </summary>
/// <param name="timeout">The authentication timeout.</param>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
public OpenIddictClientWindowsBuilder SetAuthenticationTimeout(TimeSpan timeout)
=> Configure(options => options.AuthenticationTimeout = timeout);
/// <summary>
/// Sets the identifier used to represent the current application
/// instance and redirect protocol activations when necessary.
/// </summary>
/// <param name="identifier">The identifier of the current instance.</param>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientWindowsBuilder SetInstanceIdentifier(string identifier)
{
if (string.IsNullOrEmpty(identifier))
{
throw new ArgumentException(SR.FormatID0366(nameof(identifier)), nameof(identifier));
}
return Configure(options => options.InstanceIdentifier = identifier);
}
/// <summary>
/// Sets the base name of the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <param name="name">The name of the pipe.</param>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientWindowsBuilder SetPipeName(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException(SR.FormatID0366(nameof(name)), nameof(name));
}
return Configure(options => options.PipeName = name);
}
/// <summary>
/// Sets the security policy applied to the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <param name="security">The security policy applied to the pipe.</param>
/// <returns>The <see cref="OpenIddictClientWindowsBuilder"/>.</returns>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public OpenIddictClientWindowsBuilder SetPipeSecurity(PipeSecurity security)
{
if (security is null)
{
throw new ArgumentNullException(nameof(security));
}
return Configure(options => options.PipeSecurity = security);
}
/// <summary>
/// Determines whether the specified object is equal to the current object.
/// </summary>
/// <param name="obj">The object to compare with the current object.</param>
/// <returns><see langword="true"/> if the specified object is equal to the current object; otherwise, false.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override bool Equals(object? obj) => base.Equals(obj);
/// <summary>
/// Serves as the default hash function.
/// </summary>
/// <returns>A hash code for the current object.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override int GetHashCode() => base.GetHashCode();
/// <summary>
/// Returns a string that represents the current object.
/// </summary>
/// <returns>A string that represents the current object.</returns>
[EditorBrowsable(EditorBrowsableState.Never)]
public override string? ToString() => base.ToString();
}

128
src/OpenIddict.Client.Windows/OpenIddictClientWindowsConfiguration.cs

@ -1,128 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
using System.IO.Pipes;
using System.Runtime.CompilerServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using OpenIddict.Extensions;
#if !SUPPORTS_HOST_ENVIRONMENT
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment;
#endif
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains the methods required to ensure that the OpenIddict client configuration is valid.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public sealed class OpenIddictClientWindowsConfiguration : IConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<OpenIddictClientOptions>,
IPostConfigureOptions<OpenIddictClientWindowsOptions>
{
private readonly IHostEnvironment _environment;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsConfiguration"/> class.
/// </summary>
/// <param name="environment">The host environment.</param>
public OpenIddictClientWindowsConfiguration(IHostEnvironment environment)
=> _environment = environment ?? throw new ArgumentNullException(nameof(environment));
/// <inheritdoc/>
public void Configure(OpenIddictClientOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// Register the built-in event handlers used by the OpenIddict Windows client components.
options.Handlers.AddRange(OpenIddictClientWindowsHandlers.DefaultHandlers);
}
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// Ensure an explicit client URI was set when using the Windows integration.
if (options.ClientUri is not { IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0384));
}
}
/// <inheritdoc/>
public void PostConfigure(string? name, OpenIddictClientWindowsOptions options)
{
if (options is null)
{
throw new ArgumentNullException(nameof(options));
}
// If no explicit instance identifier was specified, use a random GUID.
if (string.IsNullOrEmpty(options.InstanceIdentifier))
{
options.InstanceIdentifier = Guid.NewGuid().ToString();
}
// If no explicit pipe name was specified, compute the SHA-256 hash of the
// application name resolved from the host and use it as a unique identifier.
//
// Note: the pipe name is deliberately prefixed with "LOCAL\" to support
// partial trust/sandboxed applications that are executed in an AppContainer
// and cannot communicate with applications outside the sandbox container.
if (string.IsNullOrEmpty(options.PipeName))
{
if (string.IsNullOrEmpty(_environment.ApplicationName))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0386));
}
options.PipeName = $@"LOCAL\OpenIddict.Client.Windows\{
Base64UrlEncoder.Encode(OpenIddictHelpers.ComputeSha256Hash(
Encoding.UTF8.GetBytes(_environment.ApplicationName)))
}";
}
// If no explicit pipe security policy was specified, grant the current user
// full control over the created pipe and allow cross-process communication
// between elevated and non-elevated processes. Note: if the process executes
// inside an AppContainer, don't override the default OS pipe security policy
// to allow all applications with the same identity to access the named pipe.
if (options.PipeSecurity is null)
{
using var identity = WindowsIdentity.GetCurrent(TokenAccessLevels.Query);
if (!IsRunningInAppContainer(identity))
{
options.PipeSecurity = new PipeSecurity();
options.PipeSecurity.SetOwner(identity.User!);
options.PipeSecurity.AddAccessRule(new PipeAccessRule(identity.User!,
PipeAccessRights.FullControl, AccessControlType.Allow));
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
static bool IsRunningInAppContainer(WindowsIdentity identity)
#if SUPPORTS_WINDOWS_RUNTIME
=> OpenIddictClientWindowsHelpers.IsWindowsRuntimeSupported() &&
OpenIddictClientWindowsHelpers.HasAppContainerToken(identity);
#else
=> false;
#endif
}
}

68
src/OpenIddict.Client.Windows/OpenIddictClientWindowsHandlerFilters.cs

@ -1,68 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains a collection of event handler filters commonly used by the Windows handlers.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientWindowsHandlerFilters
{
/// <summary>
/// Represents a filter that excludes the associated handlers
/// if no explicit nonce was attached to the authentication context.
/// </summary>
public sealed class RequireAuthenticationNonce : IOpenIddictClientHandlerFilter<ProcessAuthenticationContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(ProcessAuthenticationContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(!string.IsNullOrEmpty(context.Nonce));
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no interactive user session was detected.
/// </summary>
public sealed class RequireInteractiveSession : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(Environment.UserInteractive);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if no Windows activation was found.
/// </summary>
public sealed class RequireWindowsActivation : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
return new(context.Transaction.GetWindowsActivation() is not null);
}
}
}

125
src/OpenIddict.Client.Windows/OpenIddictClientWindowsListener.cs

@ -1,125 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
using System.IO.Pipes;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains the logic necessary to handle URI protocol activations that
/// are redirected by other instances using inter-process communication.
/// </summary>
/// <remarks>
/// Note: initial URI protocol activations are handled by <see cref="OpenIddictClientWindowsHandler"/>.
/// </remarks>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class OpenIddictClientWindowsListener : BackgroundService
{
private readonly ILogger<OpenIddictClientWindowsListener> _logger;
private readonly IOptionsMonitor<OpenIddictClientWindowsOptions> _options;
private readonly OpenIddictClientWindowsService _service;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsHandler"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="options">The OpenIddict client Windows integration options.</param>
/// <param name="service">The OpenIddict client Windows service.</param>
public OpenIddictClientWindowsListener(
ILogger<OpenIddictClientWindowsListener> logger,
IOptionsMonitor<OpenIddictClientWindowsOptions> options,
OpenIddictClientWindowsService service)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_options = options ?? throw new ArgumentNullException(nameof(options));
_service = service ?? throw new ArgumentNullException(nameof(service));
}
/// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
do
{
try
{
using var buffer = new MemoryStream();
using var reader = new BinaryReader(buffer);
#if SUPPORTS_NAMED_PIPE_CONSTRUCTOR_WITH_ACL
using var stream = new NamedPipeServerStream(
#elif SUPPORTS_NAMED_PIPE_STATIC_FACTORY_WITH_ACL
using var stream = NamedPipeServerStreamAcl.Create(
#else
using var stream = NamedPipeServerStreamConstructors.New(
#endif
pipeName : $@"{_options.CurrentValue.PipeName}\{_options.CurrentValue.InstanceIdentifier}",
direction : PipeDirection.In,
maxNumberOfServerInstances: 1,
transmissionMode : PipeTransmissionMode.Message,
options : PipeOptions.Asynchronous,
inBufferSize : 0,
outBufferSize : 0,
pipeSecurity : _options.CurrentValue.PipeSecurity,
inheritability : HandleInheritability.None,
additionalAccessRights : default);
// Wait for a writer to connect to the named pipe.
await stream.WaitForConnectionAsync(stoppingToken);
// Copy the content to the memory stream asynchronously and rewind it.
await stream.CopyToAsync(buffer, bufferSize: 81_920, stoppingToken);
buffer.Seek(0L, SeekOrigin.Begin);
// Process the inter-process notification based on its declared type.
await (reader.ReadInt32() switch
{
0x01 when GetProtocolActivation(reader) is OpenIddictClientWindowsActivation activation
=> _service.HandleProtocolActivationAsync(activation, stoppingToken),
var value => throw new InvalidOperationException(SR.FormatID0387(value))
});
}
// Ignore operation canceled exceptions when the host is shutting down.
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
}
// Swallow all exceptions to ensure the service doesn't exit when encountering an exception.
catch (Exception exception)
{
_logger.LogWarning(exception, SR.GetResourceString(SR.ID6213));
}
}
while (!stoppingToken.IsCancellationRequested);
static OpenIddictClientWindowsActivation GetProtocolActivation(BinaryReader reader)
{
// Ensure the binary serialization format is supported.
var version = reader.ReadInt32();
if (version is not 0x01)
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0388));
}
var value = reader.ReadString();
if (string.IsNullOrEmpty(value) || !Uri.TryCreate(value, UriKind.Absolute, out Uri? uri))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0388));
}
return new OpenIddictClientWindowsActivation(uri)
{
IsActivationRedirected = true
};
}
}
}

56
src/OpenIddict.Client.Windows/OpenIddictClientWindowsOptions.cs

@ -1,56 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.IO.Pipes;
#if !SUPPORTS_HOST_ENVIRONMENT
using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment;
#endif
namespace OpenIddict.Client.Windows;
/// <summary>
/// Provides various settings needed to configure the OpenIddict Windows client integration.
/// </summary>
public sealed class OpenIddictClientWindowsOptions
{
/// <summary>
/// Gets or sets the timeout after which authentication demands
/// that are not completed are automatically aborted by OpenIddict.
/// </summary>
public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10);
/// <summary>
/// Gets or sets a boolean indicating whether protocol activation processing should be
/// disabled, which can be used to offload this task to a separate dedicated executable.
/// </summary>
public bool DisableProtocolActivationProcessing { get; set; }
/// <summary>
/// Gets or sets the identifier used to represent the current application
/// instance and redirect protocol activations when necessary.
/// </summary>
public string? InstanceIdentifier { get; set; }
/// <summary>
/// Gets or sets the base name of the pipe created by OpenIddict to enable
/// inter-process communication and handle protocol activation redirections.
/// </summary>
/// <remarks>
/// If no value is explicitly set, a default name is automatically computed.
/// </remarks>
public string PipeName { get; set; } = default!;
/// <summary>
/// Gets or sets the security policy applied to the pipe created by OpenIddict
/// to enable inter-process communication and handle protocol activation redirections.
/// </summary>
/// <remarks>
/// If no value is explicitly set, a default policy is automatically created
/// (unless the application is running inside an AppContainer sandbox).
/// </remarks>
public PipeSecurity PipeSecurity { get; set; } = default!;
}

92
src/OpenIddict.Client.Windows/OpenIddictClientWindowsService.cs

@ -1,92 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* See https://github.com/openiddict/openiddict-core for more information concerning
* the license and the contributors participating to this project.
*/
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
namespace OpenIddict.Client.Windows;
/// <summary>
/// Contains the logic necessary to handle URI protocol activations (that
/// are typically resolved when launching the application or redirected
/// by other instances using inter-process communication).
/// </summary>
public sealed class OpenIddictClientWindowsService
{
private readonly IServiceProvider _provider;
/// <summary>
/// Creates a new instance of the <see cref="OpenIddictClientWindowsService"/> class.
/// </summary>
/// <param name="provider">The service provider.</param>
/// <exception cref="ArgumentNullException"><paramref name="provider"/> is <see langword="null"/>.</exception>
public OpenIddictClientWindowsService(IServiceProvider provider)
=> _provider = provider ?? throw new ArgumentNullException(nameof(provider));
/// <summary>
/// Handles the specified protocol activation.
/// </summary>
/// <param name="activation">The protocol activation details.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>A <see cref="Task"/> that can be used to monitor the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException"><paramref name="activation"/> is <see langword="null"/>.</exception>
[EditorBrowsable(EditorBrowsableState.Advanced)]
public async Task HandleProtocolActivationAsync(
OpenIddictClientWindowsActivation activation, CancellationToken cancellationToken = default)
{
if (activation is null)
{
throw new ArgumentNullException(nameof(activation));
}
cancellationToken.ThrowIfCancellationRequested();
var scope = _provider.CreateScope();
try
{
var dispatcher = scope.ServiceProvider.GetRequiredService<IOpenIddictClientDispatcher>();
var factory = scope.ServiceProvider.GetRequiredService<IOpenIddictClientFactory>();
// Create a client transaction and store the protocol activation details so they can be
// retrieved by the Windows-specific client event handlers that need to access them.
var transaction = await factory.CreateTransactionAsync();
transaction.SetProperty(typeof(OpenIddictClientWindowsActivation).FullName!, activation);
var context = new ProcessRequestContext(transaction)
{
CancellationToken = cancellationToken
};
await dispatcher.DispatchAsync(context);
if (context.IsRejected)
{
await dispatcher.DispatchAsync(new ProcessErrorContext(transaction)
{
CancellationToken = cancellationToken,
Error = context.Error ?? Errors.InvalidRequest,
ErrorDescription = context.ErrorDescription,
ErrorUri = context.ErrorUri,
Response = new OpenIddictResponse()
});
}
}
finally
{
if (scope is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
scope.Dispose();
}
}
}
}

13
src/OpenIddict.Client/OpenIddictClientEvents.Authentication.cs

@ -62,6 +62,19 @@ public static partial class OpenIddictClientEvents
set => Transaction.Request = value;
}
/// <summary>
/// Gets or sets the nonce that is used as the unique identifier of the operation, if available.
/// </summary>
public string? Nonce { get; set; }
/// <summary>
/// Gets or sets the redirect URI that was selected during the challenge, if available.
/// </summary>
public string? RedirectUri { get; set; }
/// <summary>
/// Gets or sets the URI of the remote authorization endpoint.
/// </summary>
public string AuthorizationEndpoint { get; set; } = null!;
}

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

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

10
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

152
src/OpenIddict.Client/OpenIddictClientService.cs

@ -37,7 +37,7 @@ public sealed class OpenIddictClientService
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
[RequiresPreviewFeatures]
public ValueTask<string> ChallengeWithBrowserAsync(
public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateInteractivelyAsync(
Uri issuer, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? 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);
}
/// <summary>
@ -64,7 +65,7 @@ public sealed class OpenIddictClientService
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
[RequiresPreviewFeatures]
public ValueTask<string> ChallengeWithBrowserAsync(
public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateInteractivelyAsync(
string provider, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? 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);
}
/// <summary>
/// Initiates an interactive user authentication demand.
/// Completes the interactive authentication demand corresponding to the specified nonce.
/// </summary>
/// <param name="registration">The client registration.</param>
/// <param name="scopes">The scopes to request to the authorization server.</param>
/// <param name="parameters">The additional parameters to send as part of the token request.</param>
/// <param name="properties">The application-specific properties that will be added to the authentication context.</param>
/// <param name="nonce">The nonce obtained after a challenge operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
[RequiresPreviewFeatures]
private async ValueTask<string> ChallengeWithBrowserAsync(
OpenIddictClientRegistration registration, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? 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
}
/// <summary>
/// Completes the interactive authentication demand corresponding to the specified nonce.
/// Initiates an interactive user authentication demand.
/// </summary>
/// <param name="nonce">The nonce obtained after a challenge operation.</param>
/// <param name="registration">The client registration.</param>
/// <param name="scopes">The scopes to request to the authorization server.</param>
/// <param name="parameters">The additional parameters to send as part of the token request.</param>
/// <param name="properties">The application-specific properties that will be added to the authentication context.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> that can be used to abort the operation.</param>
/// <returns>The response and a merged principal containing the claims extracted from the tokens and userinfo response.</returns>
[RequiresPreviewFeatures]
public async ValueTask<(OpenIddictResponse AuthorizationResponse, OpenIddictResponse TokenResponse, ClaimsPrincipal Principal)> AuthenticateWithBrowserAsync(
string nonce, CancellationToken cancellationToken = default)
private async ValueTask<string> ChallengeInteractivelyAsync(
OpenIddictClientRegistration registration, string[]? scopes = null,
Dictionary<string, OpenIddictParameter>? parameters = null,
Dictionary<string, string>? 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

1
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
<ItemGroup>
<ProjectReference Include="..\OpenIddict.Abstractions\OpenIddict.Abstractions.csproj" />
<ProjectReference Include="..\OpenIddict.Client\OpenIddict.Client.csproj" />
<ProjectReference Include="..\OpenIddict.Client.SystemIntegration\OpenIddict.Client.SystemIntegration.csproj" />
<ProjectReference Include="..\OpenIddict.Client.SystemNetHttp\OpenIddict.Client.SystemNetHttp.csproj" />
<ProjectReference Include="..\OpenIddict.Client.WebIntegration\OpenIddict.Client.WebIntegration.csproj" />
<ProjectReference Include="..\OpenIddict.Core\OpenIddict.Core.csproj" />

Loading…
Cancel
Save