Browse Source

Update the embedded web server to listen on 127.0.0.1/::1 when possible and use http://localhost/ as the default client URI

pull/1681/head
Kévin Chalet 3 years ago
parent
commit
ce1d49b3a6
  1. 6
      sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs
  2. 21
      sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs
  3. 2
      sandbox/OpenIddict.Sandbox.Console.Client/OpenIddict.Sandbox.Console.Client.csproj
  4. 5
      sandbox/OpenIddict.Sandbox.Console.Client/Program.cs
  5. 15
      sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs
  6. 8
      sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs
  7. 15
      sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs
  8. 8
      sandbox/OpenIddict.Sandbox.Wpf.Client/Worker.cs
  9. 4
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  10. 30
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs
  11. 21
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs
  12. 91
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
  13. 44
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs
  14. 123
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs
  15. 11
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationOptions.cs

6
sandbox/OpenIddict.Sandbox.AspNetCore.Server/Worker.cs

@ -1,6 +1,6 @@
using System.Globalization; using System.Globalization;
using OpenIddict.Sandbox.AspNetCore.Server.Models;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using OpenIddict.Sandbox.AspNetCore.Server.Models;
using static OpenIddict.Abstractions.OpenIddictConstants; using static OpenIddict.Abstractions.OpenIddictConstants;
namespace OpenIddict.Sandbox.AspNetCore.Server; namespace OpenIddict.Sandbox.AspNetCore.Server;
@ -123,7 +123,7 @@ public class Worker : IHostedService
}, },
RedirectUris = RedirectUris =
{ {
new Uri("openiddict-sandbox-winforms-client://localhost/callback/login/local") new Uri("com.openiddict.sandbox.winforms.client:/callback/login/local")
}, },
Permissions = Permissions =
{ {
@ -157,7 +157,7 @@ public class Worker : IHostedService
}, },
RedirectUris = RedirectUris =
{ {
new Uri("openiddict-sandbox-wpf-client://localhost/callback/login/local") new Uri("com.openiddict.sandbox.wpf.client:/callback/login/local")
}, },
Permissions = Permissions =
{ {

21
sandbox/OpenIddict.Sandbox.Console.Client/InteractiveService.cs

@ -23,8 +23,8 @@ public class InteractiveService : BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
// Wait for the host to confirm that the application has started. // Wait for the host to confirm that the application has started.
var source = new TaskCompletionSource(); var source = new TaskCompletionSource<bool>();
using (_lifetime.ApplicationStarted.Register(static state => ((TaskCompletionSource) state!).SetResult(), source)) using (_lifetime.ApplicationStarted.Register(static state => ((TaskCompletionSource<bool>) state!).SetResult(true), source))
{ {
await source.Task; await source.Task;
} }
@ -37,7 +37,7 @@ public class InteractiveService : BackgroundService
{ {
Console.WriteLine("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 Task.Run(Console.ReadLine).WaitAsync(stoppingToken) switch provider = await WaitAsync(Task.Run(Console.ReadLine, stoppingToken), stoppingToken) switch
{ {
"1" => "Local", "1" => "Local",
"2" => "Twitter", "2" => "Twitter",
@ -74,5 +74,20 @@ public class InteractiveService : BackgroundService
Console.WriteLine("An error occurred while trying to authenticate the user."); Console.WriteLine("An error occurred while trying to authenticate the user.");
} }
} }
static async Task<T> WaitAsync<T>(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;
}
}
} }
} }

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

@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFrameworks>net48;net7.0</TargetFrameworks>
<EnablePreviewFeatures>true</EnablePreviewFeatures> <EnablePreviewFeatures>true</EnablePreviewFeatures>
<IsShipping>false</IsShipping> <IsShipping>false</IsShipping>
<SignAssembly>false</SignAssembly> <SignAssembly>false</SignAssembly>

5
sandbox/OpenIddict.Sandbox.Console.Client/Program.cs

@ -54,9 +54,6 @@ var host = new HostBuilder()
.EnableEmbeddedWebServer() .EnableEmbeddedWebServer()
.UseSystemBrowser(); .UseSystemBrowser();
// Set the client URI that will uniquely identify this application.
options.SetClientUri(new Uri("http://localhost/", UriKind.Absolute));
// Register the System.Net.Http integration and use the identity of the current // 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 // assembly as a more specific user agent, which can be useful when dealing with
// providers that use the user agent as a way to throttle requests (e.g Reddit). // providers that use the user agent as a way to throttle requests (e.g Reddit).
@ -84,7 +81,7 @@ var host = new HostBuilder()
.UseTwitter() .UseTwitter()
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") .SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative)); .SetRedirectUri("callback/login/twitter");
}); });
// Register the worker responsible for creating the database used to store tokens // Register the worker responsible for creating the database used to store tokens

15
sandbox/OpenIddict.Sandbox.WinForms.Client/Program.cs

@ -50,9 +50,6 @@ var host = new HostBuilder()
// Add the operating system integration. // Add the operating system integration.
options.UseSystemIntegration(); options.UseSystemIntegration();
// Set the client URI that will uniquely identify this application.
options.SetClientUri(new Uri("openiddict-sandbox-winforms-client://localhost/", UriKind.Absolute));
// Register the System.Net.Http integration and use the identity of the current // 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 // assembly as a more specific user agent, which can be useful when dealing with
// providers that use the user agent as a way to throttle requests (e.g Reddit). // providers that use the user agent as a way to throttle requests (e.g Reddit).
@ -66,7 +63,14 @@ var host = new HostBuilder()
ProviderName = "Local", ProviderName = "Local",
ClientId = "winforms", ClientId = "winforms",
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
// This sample uses protocol activations with a custom URI scheme to handle callbacks.
//
// For more information on how to construct private-use URI schemes,
// read https://www.rfc-editor.org/rfc/rfc8252#section-7.1 and
// https://www.rfc-editor.org/rfc/rfc7595#section-3.8.
RedirectUri = new Uri("com.openiddict.sandbox.winforms.client:/callback/login/local", UriKind.Absolute),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" } Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
}); });
@ -80,7 +84,8 @@ var host = new HostBuilder()
.UseTwitter() .UseTwitter()
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") .SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative)); // Note: Twitter doesn't support the recommended ":/" syntax and requires using "://".
.SetRedirectUri("com.openiddict.sandbox.winforms.client://callback/login/twitter");
}); });
// Register the worker responsible for creating the database used to store tokens // Register the worker responsible for creating the database used to store tokens

8
sandbox/OpenIddict.Sandbox.WinForms.Client/Worker.cs

@ -23,18 +23,20 @@ public class Worker : IHostedService
RegistryKey? root = null; RegistryKey? root = null;
// Create the registry entries necessary to handle URI protocol activations. // Create the registry entries necessary to handle URI protocol activations.
//
// Note: the application MUST be run once as an administrator for this to work, // 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. // 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 // Alternatively, the application can be packaged and use windows.protocol to
// register the protocol handler/custom URI scheme with the operation system. // register the protocol handler/custom URI scheme with the operation system.
try try
{ {
root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-winforms-client"); root = Registry.ClassesRoot.OpenSubKey("com.openiddict.sandbox.winforms.client");
if (root is null) if (root is null)
{ {
root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-winforms-client"); root = Registry.ClassesRoot.CreateSubKey("com.openiddict.sandbox.winforms.client");
root.SetValue(string.Empty, "URL:openiddict-sandbox-winforms-client"); root.SetValue(string.Empty, "URL:com.openiddict.sandbox.winforms.client");
root.SetValue("URL Protocol", string.Empty); root.SetValue("URL Protocol", string.Empty);
using var command = root.CreateSubKey("shell\\open\\command"); using var command = root.CreateSubKey("shell\\open\\command");

15
sandbox/OpenIddict.Sandbox.Wpf.Client/Program.cs

@ -51,9 +51,6 @@ var host = new HostBuilder()
// Add the operating system integration. // Add the operating system integration.
options.UseSystemIntegration(); options.UseSystemIntegration();
// Set the client URI that will uniquely identify this application.
options.SetClientUri(new Uri("openiddict-sandbox-wpf-client://localhost/", UriKind.Absolute));
// Register the System.Net.Http integration and use the identity of the current // 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 // assembly as a more specific user agent, which can be useful when dealing with
// providers that use the user agent as a way to throttle requests (e.g Reddit). // providers that use the user agent as a way to throttle requests (e.g Reddit).
@ -67,7 +64,14 @@ var host = new HostBuilder()
ProviderName = "Local", ProviderName = "Local",
ClientId = "wpf", ClientId = "wpf",
RedirectUri = new Uri("callback/login/local", UriKind.Relative),
// This sample uses protocol activations with a custom URI scheme to handle callbacks.
//
// For more information on how to construct private-use URI schemes,
// read https://www.rfc-editor.org/rfc/rfc8252#section-7.1 and
// https://www.rfc-editor.org/rfc/rfc7595#section-3.8.
RedirectUri = new Uri("com.openiddict.sandbox.wpf.client:/callback/login/local", UriKind.Absolute),
Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" } Scopes = { Scopes.Email, Scopes.Profile, Scopes.OfflineAccess, "demo_api" }
}); });
@ -81,7 +85,8 @@ var host = new HostBuilder()
.UseTwitter() .UseTwitter()
.SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ") .SetClientId("bXgwc0U3N3A3YWNuaWVsdlRmRWE6MTpjaQ")
.SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS") .SetClientSecret("VcohOgBp-6yQCurngo4GAyKeZh0D6SUCCSjJgEo1uRzJarjIUS")
.SetRedirectUri(new Uri("callback/login/twitter", UriKind.Relative)); // Note: Twitter doesn't support the recommended ":/" syntax and requires using "://".
.SetRedirectUri("com.openiddict.sandbox.wpf.client://callback/login/twitter");
}); });
// Register the worker responsible for creating the database used to store tokens // Register the worker responsible for creating the database used to store tokens

8
sandbox/OpenIddict.Sandbox.Wpf.Client/Worker.cs

@ -23,18 +23,20 @@ public class Worker : IHostedService
RegistryKey? root = null; RegistryKey? root = null;
// Create the registry entries necessary to handle URI protocol activations. // Create the registry entries necessary to handle URI protocol activations.
//
// Note: the application MUST be run once as an administrator for this to work, // 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. // 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 // Alternatively, the application can be packaged and use windows.protocol to
// register the protocol handler/custom URI scheme with the operation system. // register the protocol handler/custom URI scheme with the operation system.
try try
{ {
root = Registry.ClassesRoot.OpenSubKey("openiddict-sandbox-wpf-client"); root = Registry.ClassesRoot.OpenSubKey("com.openiddict.sandbox.wpf.client");
if (root is null) if (root is null)
{ {
root = Registry.ClassesRoot.CreateSubKey("openiddict-sandbox-wpf-client"); root = Registry.ClassesRoot.CreateSubKey("com.openiddict.sandbox.wpf.client");
root.SetValue(string.Empty, "URL:openiddict-sandbox-wpf-client"); root.SetValue(string.Empty, "URL:com.openiddict.sandbox.wpf.client");
root.SetValue("URL Protocol", string.Empty); root.SetValue("URL Protocol", string.Empty);
using var command = root.CreateSubKey("shell\\open\\command"); using var command = root.CreateSubKey("shell\\open\\command");

4
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1451,7 +1451,7 @@ 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> <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>
<data name="ID0384" xml:space="preserve"> <data name="ID0384" xml:space="preserve">
<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> <value>An error occurred while trying to create an embedded web server on port {0}.</value>
</data> </data>
<data name="ID0385" xml:space="preserve"> <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> <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>
@ -1472,7 +1472,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>The HTTP listener context cannot be resolved or contains invalid data.</value> <value>The HTTP listener context cannot be resolved or contains invalid data.</value>
</data> </data>
<data name="ID0391" xml:space="preserve"> <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> <value>An error occurred while instantiating the embedded web server, which may indicate a permission issue.</value>
</data> </data>
<data name="ID0392" xml:space="preserve"> <data name="ID0392" xml:space="preserve">
<value>The web authentication broker is not supported on this platform.</value> <value>The web authentication broker is not supported on this platform.</value>

30
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs

@ -6,6 +6,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.IO.Pipes; using System.IO.Pipes;
using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using OpenIddict.Client.SystemIntegration; using OpenIddict.Client.SystemIntegration;
@ -65,7 +66,6 @@ public sealed class OpenIddictClientSystemIntegrationBuilder
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
} }
#if SUPPORTS_WINDOWS_RUNTIME
if (!OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported()) if (!OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported())
{ {
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
@ -73,9 +73,6 @@ public sealed class OpenIddictClientSystemIntegrationBuilder
return Configure(options => options.AuthenticationMode = return Configure(options => options.AuthenticationMode =
OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker); OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker);
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
#endif
} }
/// <summary> /// <summary>
@ -86,6 +83,31 @@ public sealed class OpenIddictClientSystemIntegrationBuilder
=> Configure(options => options.AuthenticationMode = => Configure(options => options.AuthenticationMode =
OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser); OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser);
/// <summary>
/// Sets the list of static ports the embedded web server will be allowed to
/// listen on, if enabled. The first port in the list that is not already used
/// by another program is automatically chosen and the other ports are ignored.
/// </summary>
/// <remarks>
/// If this property is not explicitly set, a port in the 49152-65535
/// dynamic ports range is automatically chosen by OpenIddict at runtime.
/// </remarks>
/// <param name="ports">The static ports the embedded web server will be allowed to listen on.</param>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
public OpenIddictClientSystemIntegrationBuilder SetAllowedEmbeddedWebServerPorts(params int[] ports)
{
if (Array.Exists(ports, static port => port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort))
{
throw new ArgumentOutOfRangeException(nameof(ports));
}
return Configure(options =>
{
options.AllowedEmbeddedWebServerPorts.Clear();
options.AllowedEmbeddedWebServerPorts.AddRange(ports);
});
}
/// <summary> /// <summary>
/// Sets the timeout after which authentication demands that /// Sets the timeout after which authentication demands that
/// are not completed are automatically aborted by OpenIddict. /// are not completed are automatically aborted by OpenIddict.

21
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs

@ -61,11 +61,16 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
// Ensure an explicit client URI was set when using the system integration. // If no explicit client URI was set, default to the static "http://localhost/" address, which is
if (options.ClientUri is not { IsAbsoluteUri: true }) // adequate for a native/mobile client and points to the embedded web server when it is enabled.
{ //
throw new InvalidOperationException(SR.GetResourceString(SR.ID0384)); // Note: while the RFC8252 specification recommends using 127.0.0.1 or ::1 instead of "localhost",
} // OpenIddict deliberately uses "localhost" to be compatible with platforms that don't allow binding
// on loopback addresses without administrator rights and to avoid using a hard-to-predict client URI
// that would be tied to a specific IP version dependent on the protocols supported/allowed by the OS.
//
// See https://www.rfc-editor.org/rfc/rfc8252#section-8.3 for more information.
options.ClientUri ??= new Uri("http://localhost/", UriKind.Absolute);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -161,12 +166,8 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO
[MethodImpl(MethodImplOptions.NoInlining)] [MethodImpl(MethodImplOptions.NoInlining)]
[SupportedOSPlatform("windows")] [SupportedOSPlatform("windows")]
static bool IsRunningInAppContainer(WindowsIdentity identity) static bool IsRunningInAppContainer(WindowsIdentity identity)
#if SUPPORTS_WINDOWS_RUNTIME => OpenIddictClientSystemIntegrationHelpers.IsWindowsVersionAtLeast(10, 0, 10240) &&
=> OpenIddictClientSystemIntegrationHelpers.IsWindowsRuntimeSupported() &&
OpenIddictClientSystemIntegrationHelpers.HasAppContainerToken(identity); OpenIddictClientSystemIntegrationHelpers.HasAppContainerToken(identity);
#else
=> false;
#endif
} }
} }
} }

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

@ -37,6 +37,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
ResolveRequestUriFromHttpListenerRequest.Descriptor, ResolveRequestUriFromHttpListenerRequest.Descriptor,
ResolveRequestUriFromProtocolActivation.Descriptor, ResolveRequestUriFromProtocolActivation.Descriptor,
ResolveRequestUriFromWebAuthenticationResult.Descriptor, ResolveRequestUriFromWebAuthenticationResult.Descriptor,
InferEndpointTypeFromDynamicAddress.Descriptor,
RejectUnknownHttpRequests.Descriptor, RejectUnknownHttpRequests.Descriptor,
/* /*
@ -133,7 +134,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
// that don't include a Host header (e.g HTTP/1.0 requests) or specify an invalid value. // that don't include a Host header (e.g HTTP/1.0 requests) or specify an invalid value.
{ Request.Url: { IsAbsoluteUri: true } uri } => ( { Request.Url: { IsAbsoluteUri: true } uri } => (
BaseUri: new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), BaseUri: new UriBuilder(uri) { Path = null, Query = null, Fragment = null }.Uri,
RequestUri: uri), RequestUri: uri),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0390)) _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0390))
@ -171,7 +172,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
(context.BaseUri, context.RequestUri) = context.Transaction.GetProtocolActivation() switch (context.BaseUri, context.RequestUri) = context.Transaction.GetProtocolActivation() switch
{ {
{ ActivationUri: Uri uri } => ( { ActivationUri: Uri uri } => (
BaseUri: new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), BaseUri: new UriBuilder(uri) { Path = null, Query = null, Fragment = null }.Uri,
RequestUri: uri), RequestUri: uri),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375)) _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0375))
@ -211,7 +212,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
{ {
{ ResponseStatus: WebAuthenticationStatus.Success, ResponseData: string data } when { ResponseStatus: WebAuthenticationStatus.Success, ResponseData: string data } when
Uri.TryCreate(data, UriKind.Absolute, out Uri? uri) => ( Uri.TryCreate(data, UriKind.Absolute, out Uri? uri) => (
BaseUri: new Uri(uri.GetLeftPart(UriPartial.Authority), UriKind.Absolute), BaseUri: new UriBuilder(uri) { Path = null, Query = null, Fragment = null }.Uri,
RequestUri: uri), RequestUri: uri),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0393)) _ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0393))
@ -224,6 +225,84 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for inferring the endpoint type from the request URI, ignoring
/// the port when comparing the request URI with the endpoint URIs configured in the options.
/// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server.
/// </summary>
public sealed class InferEndpointTypeFromDynamicAddress : IOpenIddictClientHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpListenerContext>()
.UseSingletonHandler<InferEndpointTypeFromDynamicAddress>()
.SetOrder(InferEndpointType.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(ProcessRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
if (context is not { BaseUri.IsAbsoluteUri: true, RequestUri.IsAbsoluteUri: true })
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0127));
}
// If an endpoint was already inferred by the generic handler, don't override it.
if (context.EndpointType is not OpenIddictClientEndpointType.Unknown)
{
return default;
}
context.EndpointType =
Matches(context.Options.RedirectionEndpointUris) ? OpenIddictClientEndpointType.Redirection :
Matches(context.Options.PostLogoutRedirectionEndpointUris) ? OpenIddictClientEndpointType.PostLogoutRedirection :
OpenIddictClientEndpointType.Unknown;
return default;
bool Matches(IReadOnlyList<Uri> uris)
{
for (var index = 0; index < uris.Count; index++)
{
var uri = uris[index];
if (uri.IsAbsoluteUri && uri.IsLoopback && uri.IsDefaultPort && Equals(uri, context.RequestUri))
{
return true;
}
}
return false;
}
static bool Equals(Uri left, Uri right) =>
string.Equals(left.Scheme, right.Scheme, StringComparison.OrdinalIgnoreCase) &&
string.Equals(left.Host, right.Host, StringComparison.OrdinalIgnoreCase) &&
//
// Deliberately ignore the port when doing comparisons in this specialized handler.
//
// Note: paths are considered equivalent even if the casing isn't identical or if one of the two
// paths only differs by a trailing slash, which matches the classical behavior seen on ASP.NET,
// Microsoft.Owin/Katana and ASP.NET Core. Developers who prefer a different behavior can remove
// this handler and replace it by a custom version implementing a more strict comparison logic.
(string.Equals(left.AbsolutePath, right.AbsolutePath, StringComparison.OrdinalIgnoreCase) ||
(left.AbsolutePath.Length == right.AbsolutePath.Length + 1 &&
left.AbsolutePath.StartsWith(right.AbsolutePath, StringComparison.OrdinalIgnoreCase) &&
left.AbsolutePath[^1] is '/') ||
(right.AbsolutePath.Length == left.AbsolutePath.Length + 1 &&
right.AbsolutePath.StartsWith(left.AbsolutePath, StringComparison.OrdinalIgnoreCase) &&
right.AbsolutePath[^1] is '/'));
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for rejecting unknown requests handled by the embedded web server, if applicable. /// Contains the logic responsible for rejecting unknown requests handled by the embedded web server, if applicable.
/// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server. /// Note: this handler is not used when the OpenID Connect request is not handled by the embedded web server.
@ -237,7 +316,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireHttpListenerContext>() .AddFilter<RequireHttpListenerContext>()
.UseSingletonHandler<RejectUnknownHttpRequests>() .UseSingletonHandler<RejectUnknownHttpRequests>()
.SetOrder(InferEndpointType.Descriptor.Order + 500) .SetOrder(InferEndpointTypeFromDynamicAddress.Descriptor.Order + 250)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -1417,13 +1496,13 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
throw new ArgumentNullException(nameof(context)); throw new ArgumentNullException(nameof(context));
} }
// If the redirect_uri uses "localhost" as the authority and doesn't include a non-default port, // If the redirect_uri uses a loopback host/IP as the authority and doesn't include a non-default port,
// determine whether the embedded web server is running: if so, override the port in the redirect_uri // determine whether the embedded web server is running: if so, override the port in the redirect_uri
// by the port used by the embedded web server (guaranteed to be running if a value is returned). // by the port used by the embedded web server (guaranteed to be running if a value is returned).
if (!string.IsNullOrEmpty(context.RedirectUri) && if (!string.IsNullOrEmpty(context.RedirectUri) &&
Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) && Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) &&
string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) && string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
string.Equals(uri.Authority, "localhost", StringComparison.OrdinalIgnoreCase) && uri.IsDefaultPort && uri.IsLoopback && uri.IsDefaultPort &&
await _listener.GetEmbeddedServerPortAsync(context.CancellationToken) is int port) await _listener.GetEmbeddedServerPortAsync(context.CancellationToken) is int port)
{ {
var builder = new UriBuilder(context.RedirectUri) var builder = new UriBuilder(context.RedirectUri)

44
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs

@ -51,28 +51,25 @@ public static class OpenIddictClientSystemIntegrationHelpers
/// <returns>The <see cref="HttpListenerContext"/> instance or <see langword="null"/> if it couldn't be found.</returns> /// <returns>The <see cref="HttpListenerContext"/> instance or <see langword="null"/> if it couldn't be found.</returns>
public static WebAuthenticationResult? GetWebAuthenticationResult(this OpenIddictClientTransaction transaction) public static WebAuthenticationResult? GetWebAuthenticationResult(this OpenIddictClientTransaction transaction)
=> transaction.GetProperty<WebAuthenticationResult>(typeof(WebAuthenticationResult).FullName!); => transaction.GetProperty<WebAuthenticationResult>(typeof(WebAuthenticationResult).FullName!);
#endif
/// <summary> /// <summary>
/// Determines whether the Windows Runtime APIs are supported on this platform. /// Determines whether the current Windows version
/// is greater than or equals to the specified version.
/// </summary> /// </summary>
/// <returns><see langword="true"/> if the Windows Runtime APIs are supported, <see langword="false"/> otherwise.</returns> /// <returns>
/// <see langword="true"/> if the current Windows version is greater than
/// or equals to the specified version, <see langword="false"/> otherwise.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
[SupportedOSPlatform("windows")] [SupportedOSPlatformGuard("windows")]
[SupportedOSPlatformGuard("windows10.0.17763")] internal static bool IsWindowsVersionAtLeast(int major, int minor = 0, int build = 0, int revision = 0)
internal static bool IsWindowsRuntimeSupported()
{ {
// Note: as WinRT is only supported on Windows 8 and higher, trying to call any of the
// WinRT APIs on previous versions of Windows will typically result in type-load or
// type-initialization exceptions. To prevent that, this method acts as a platform
// guard that will prevent the WinRT projections from being loaded by the runtime on
// platforms that don't support it. Since OpenIddict declares Windows 10 1809 as the
// oldest supported version in the package, it is also used for the runtime check.
#if SUPPORTS_OPERATING_SYSTEM_VERSIONS_COMPARISON #if SUPPORTS_OPERATING_SYSTEM_VERSIONS_COMPARISON
return OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17763); return OperatingSystem.IsWindowsVersionAtLeast(major, minor, build, revision);
#else #else
if (Environment.OSVersion.Platform is PlatformID.Win32NT && if (Environment.OSVersion.Platform is PlatformID.Win32NT &&
Environment.OSVersion.Version >= new Version(10, 0, 17763)) Environment.OSVersion.Version >= new Version(major, minor, build, revision))
{ {
return true; return true;
} }
@ -84,10 +81,24 @@ public static class OpenIddictClientSystemIntegrationHelpers
// the hood) is made. Note: no version is returned on UWP due to the missing Win32 API. // the hood) is made. Note: no version is returned on UWP due to the missing Win32 API.
return RuntimeInformation.OSDescription.StartsWith("Microsoft Windows ", StringComparison.OrdinalIgnoreCase) && return RuntimeInformation.OSDescription.StartsWith("Microsoft Windows ", StringComparison.OrdinalIgnoreCase) &&
RuntimeInformation.OSDescription["Microsoft Windows ".Length..] is string value && RuntimeInformation.OSDescription["Microsoft Windows ".Length..] is string value &&
Version.TryParse(value, out Version? version) && version >= new Version(10, 0, 17763); Version.TryParse(value, out Version? version) && version >= new Version(major, minor, build, revision);
#endif #endif
} }
/// <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")]
// Note: as WinRT is only supported on Windows 8 and higher, trying to call any of the
// WinRT APIs on previous versions of Windows will typically result in type-load or
// type-initialization exceptions. To prevent that, this method acts as a platform
// guard that will prevent the WinRT projections from being loaded by the runtime on
// platforms that don't support it. Since OpenIddict declares Windows 10 1809 as the
// oldest supported version in the package, it is also used for the runtime check.
internal static bool IsWindowsRuntimeSupported() => IsWindowsVersionAtLeast(10, 0, 17763);
/// <summary> /// <summary>
/// Determines whether the specified identity contains an AppContainer /// Determines whether the specified identity contains an AppContainer
/// token, indicating it's running in an AppContainer sandbox. /// token, indicating it's running in an AppContainer sandbox.
@ -97,7 +108,7 @@ public static class OpenIddictClientSystemIntegrationHelpers
/// <see langword="true"/> if the specified identity contains an /// <see langword="true"/> if the specified identity contains an
/// AppContainer token, <see langword="false"/> otherwise. /// AppContainer token, <see langword="false"/> otherwise.
/// </returns> /// </returns>
[SupportedOSPlatform("windows10.0.17763")] [SupportedOSPlatform("windows10.0.10240")]
internal static unsafe bool HasAppContainerToken(WindowsIdentity identity) internal static unsafe bool HasAppContainerToken(WindowsIdentity identity)
{ {
if (identity is null) if (identity is null)
@ -129,6 +140,7 @@ public static class OpenIddictClientSystemIntegrationHelpers
out uint ReturnLength); out uint ReturnLength);
} }
#if SUPPORTS_WINDOWS_RUNTIME
/// <summary> /// <summary>
/// Resolves the protocol activation using the Windows Runtime APIs, if applicable. /// Resolves the protocol activation using the Windows Runtime APIs, if applicable.
/// </summary> /// </summary>

123
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs

@ -7,6 +7,8 @@
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Net; using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@ -44,6 +46,14 @@ public sealed class OpenIddictClientSystemIntegrationHttpListener : BackgroundSe
/// <inheritdoc/> /// <inheritdoc/>
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {
// Note: the RFC8252 specification recommends starting the web server only when an authorization request is
// about to be sent and closing it when the response is received. Unfortunately, such an approach has important
// downsides, as it increases the delay seen by the user before the browser is launched and differs potential
// server initialization errors. To avoid degrading the user experience, the embedded web server is started in
// parallel to the host and unsollicted callback requests are always rejected (as they don't include a valid
// state token). Whenever possible, the HTTP listener is configured to only listen on loopback IP endpoints
// and rejects unknown requests with an HTTP 404, making attacks targeting the embedded web server unlikely.
// If the embedded web server instantiation was not enabled, signal the task completion source with a // 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. // 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) if (_options.CurrentValue.EnableEmbeddedWebServer is not true)
@ -58,14 +68,14 @@ public sealed class OpenIddictClientSystemIntegrationHttpListener : BackgroundSe
// To ensure the host initialization is not blocked, the whole process is offloaded to the thread pool. // To ensure the host initialization is not blocked, the whole process is offloaded to the thread pool.
await Task.Run(cancellationToken: stoppingToken, function: async () => await Task.Run(cancellationToken: stoppingToken, function: async () =>
{ {
var (listener, port) = CreateHttpListener(stoppingToken) ?? var (listener, port) = CreateHttpListener(_options.CurrentValue.AllowedEmbeddedWebServerPorts, 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) using (listener)
{ {
// Inform the handlers that the HTTP listener was created and can
// now be accessed via the static port configured in the options
// or dynamically chosen at runtime in the IANA dynamic ports range.
_source.SetResult(port);
// Note: while the received load should be minimal, 3 task workers are used // 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. // to be able to process multiple requests at the same time, if necessary.
var tasks = new Task[3]; var tasks = new Task[3];
@ -87,32 +97,89 @@ public sealed class OpenIddictClientSystemIntegrationHttpListener : BackgroundSe
return; return;
} }
static (HttpListener Listener, int Port)? CreateHttpListener(CancellationToken cancellationToken) static (HttpListener Listener, int Port) CreateHttpListener(List<int> ports, CancellationToken cancellationToken)
{ {
// Note: HttpListener doesn't offer a native way to select a random, non-busy port. // Note: HttpListener doesn't offer a native way to select a non-busy port from
// To work around this limitation, this local function tries to bind an HttpListener // an arbitrary list. To work around this limitation, this local function tries
// on the first free port in the IANA dynamic ports range (typically: 49152 to 65535). // to bind an HttpListener on the first free port in the specified list or in
// the IANA dynamic ports range if the list doesn't contain any explicit port.
// //
// For more information, see // For more information, see
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml. // https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml.
for (var port = 49152; port < 65535; port++) Stack<Exception>? exceptions = null;
for (var port = IPEndPoint.MinPort; port <= IPEndPoint.MaxPort; port++)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// If one or more explicit ports were specified, ignore ports that are not listed.
// Otherwise, ignore all the ports outside the IANA dynamic ports range.
if (ports.Count is 0)
{
if (port < 49152)
{
continue;
}
}
else if (!ports.Contains(port))
{
continue;
}
var listener = new HttpListener var listener = new HttpListener
{ {
AuthenticationSchemes = AuthenticationSchemes.Anonymous, AuthenticationSchemes = AuthenticationSchemes.Anonymous,
IgnoreWriteExceptions = true, 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)}/" }
}; };
// Note: the prefix registration is deliberately not configurable to ensure
// only loopback authorities are used, which enforces the built-in host header
// 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.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// On Windows 10 1511 and higher, listening on 127.0.0.1 and ::1 is preferred
// to localhost as it allows ignoring requests that are sent by other machines
// located on the same network (even if the firewall is not enabled or not
// configured to reject such requests) without requiring administrator rights.
//
// See https://www.rfc-editor.org/rfc/rfc8252#section-8.3 for more information.
if (OpenIddictClientSystemIntegrationHelpers.IsWindowsVersionAtLeast(10, 0, 10586))
{
if (Socket.OSSupportsIPv4)
{
listener.Prefixes.Add($"http://{IPAddress.Loopback}:{port.ToString(CultureInfo.InvariantCulture)}/");
}
if (Socket.OSSupportsIPv6)
{
listener.Prefixes.Add($"http://[{IPAddress.IPv6Loopback}]:{port.ToString(CultureInfo.InvariantCulture)}/");
}
}
// On older versions, listening on 127.0.0.1 and ::1 requires administrator rights.
else
{
listener.Prefixes.Add($"http://localhost:{port.ToString(CultureInfo.InvariantCulture)}/");
}
}
else
{
// Note: the managed HttpListener implementation doesn't support IPv6 and
// doesn't allow sending a Host header containing the "localhost" authority
// when binding on the 127.0.0.1 address. To keep using "localhost" instead of
// being forced to use 127.0.0.1, the embedded web server is configured to listen
// on "localhost" on platforms that use the managed HttpListener implementation.
//
// See https://github.com/dotnet/runtime/issues/34399 for more information.
listener.Prefixes.Add($"http://localhost:{port.ToString(CultureInfo.InvariantCulture)}/");
}
try try
{ {
listener.Start(); listener.Start();
@ -120,19 +187,31 @@ public sealed class OpenIddictClientSystemIntegrationHttpListener : BackgroundSe
return (listener, port); return (listener, port);
} }
catch (HttpListenerException) catch (HttpListenerException exception)
{ {
listener.Close(); listener.Close();
exceptions ??= new(capacity: 3);
exceptions.Push(new InvalidOperationException(SR.FormatID0384(port), exception));
}
catch (Exception exception)
{
listener.Close();
throw new InvalidOperationException(SR.GetResourceString(SR.ID0391), exception);
} }
} }
return null; throw exceptions is { Count: > 0 } ?
new InvalidOperationException(SR.GetResourceString(SR.ID0391), new AggregateException(exceptions.Take(3))) :
new InvalidOperationException(SR.GetResourceString(SR.ID0391));
} }
static async Task ProcessRequestsAsync(HttpListener listener, OpenIddictClientSystemIntegrationService service, static async Task ProcessRequestsAsync(HttpListener listener, OpenIddictClientSystemIntegrationService service,
ILogger<OpenIddictClientSystemIntegrationHttpListener> logger, CancellationToken cancellationToken) ILogger<OpenIddictClientSystemIntegrationHttpListener> logger, CancellationToken cancellationToken)
{ {
while (true) while (listener.IsListening)
{ {
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();

11
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationOptions.cs

@ -28,6 +28,17 @@ public sealed class OpenIddictClientSystemIntegrationOptions
/// </summary> /// </summary>
public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10); public TimeSpan AuthenticationTimeout { get; set; } = TimeSpan.FromMinutes(10);
/// <summary>
/// Gets the list of static ports the embedded web server will be allowed to
/// listen on, if enabled. The first port in the list that is not already used
/// by another program is automatically chosen and the other ports are ignored.
/// </summary>
/// <remarks>
/// If this property is not explicitly set, a port in the 49152-65535
/// dynamic ports range is automatically chosen by OpenIddict at runtime.
/// </remarks>
public List<int> AllowedEmbeddedWebServerPorts { get; } = new();
/// <summary> /// <summary>
/// Gets or sets a boolean indicating whether protocol activation processing should be enabled. /// Gets or sets a boolean indicating whether protocol activation processing should be enabled.
/// </summary> /// </summary>

Loading…
Cancel
Save