Browse Source

Introduce iOS support in the OpenIddict client system integration package

pull/2110/head
Kévin Chalet 2 years ago
parent
commit
1e0870f125
  1. 14
      Directory.Build.props
  2. 8
      Directory.Build.targets
  3. 12
      eng/Tools.props
  4. 9
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  5. 4
      src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj
  6. 8
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs
  7. 16
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs
  8. 27
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs
  9. 5
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs
  10. 64
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs
  11. 168
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs
  12. 167
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs
  13. 149
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs
  14. 116
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs
  15. 13
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs
  16. 27
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs
  17. 36
      src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs
  18. 27
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs
  19. 36
      src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs
  20. 1
      src/OpenIddict/OpenIddict.csproj

14
Directory.Build.props

@ -45,6 +45,18 @@
net8.0 net8.0
</NetCoreTargetFrameworks> </NetCoreTargetFrameworks>
<!--
Note: targeting iOS requires installing the .NET iOS workload. To ensure the solution can be
built on machines that don't have the iOS workload installed, a directory check is used to
ensure the iOS reference assemblies pack is present on the machine before targeting iOS.
-->
<NetCoreIOSTargetFrameworks
Condition=" '$(NetCoreIOSTargetFrameworks)' == '' And
($([System.OperatingSystem]::IsMacOS()) Or $([System.OperatingSystem]::IsWindows())) And
('$(GITHUB_ACTIONS)' == 'true' Or Exists('$(DotNetRoot)packs\Microsoft.iOS.Ref')) ">
net8.0-ios
</NetCoreIOSTargetFrameworks>
<NetCoreWindowsTargetFrameworks Condition=" '$(NetCoreWindowsTargetFrameworks)' == '' "> <NetCoreWindowsTargetFrameworks Condition=" '$(NetCoreWindowsTargetFrameworks)' == '' ">
net6.0-windows7.0; net6.0-windows7.0;
net6.0-windows10.0.17763; net6.0-windows10.0.17763;
@ -66,7 +78,7 @@
ensure the reference and contract assemblies are present on the machine before targeting uap10.0. ensure the reference and contract assemblies are present on the machine before targeting uap10.0.
--> -->
<UniversalWindowsPlatformTargetFrameworks <UniversalWindowsPlatformTargetFrameworks
Condition=" '$(UniversalWindowsPlatformTargetFrameworks)' == '' And $([MSBuild]::IsOSPlatform('Windows')) And Condition=" '$(UniversalWindowsPlatformTargetFrameworks)' == '' And $([System.OperatingSystem]::IsWindows()) And
('$(GITHUB_ACTIONS)' == 'true' Or (Exists('$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETCore\v5.0') And ('$(GITHUB_ACTIONS)' == 'true' Or (Exists('$(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETCore\v5.0') And
Exists('$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.17763.0'))) "> Exists('$(MSBuildProgramFiles32)\Windows Kits\10\References\10.0.17763.0'))) ">
uap10.0.17763 uap10.0.17763

8
Directory.Build.targets

@ -122,6 +122,14 @@
<DefineConstants>$(DefineConstants);SUPPORTS_TIME_PROVIDER</DefineConstants> <DefineConstants>$(DefineConstants);SUPPORTS_TIME_PROVIDER</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCoreApp' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '5.0')) And
'$(TargetPlatformIdentifier)' == 'iOS' And '$(TargetPlatformVersion)' != '' And
$([MSBuild]::VersionGreaterThanOrEquals($(TargetPlatformVersion), '12.0'))) ">
<DefineConstants>$(DefineConstants);SUPPORTS_AUTHENTICATION_SERVICES</DefineConstants>
<DefineConstants>$(DefineConstants);SUPPORTS_UIKIT</DefineConstants>
</PropertyGroup>
<PropertyGroup <PropertyGroup
Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCore' And $([MSBuild]::VersionEquals($(TargetFrameworkVersion), '5.0')) And Condition=" ('$(TargetFrameworkIdentifier)' == '.NETCore' And $([MSBuild]::VersionEquals($(TargetFrameworkVersion), '5.0')) And
'$(TargetPlatformIdentifier)' == 'UAP' And '$(TargetPlatformVersion)' != '' And '$(TargetPlatformIdentifier)' == 'UAP' And '$(TargetPlatformVersion)' != '' And

12
eng/Tools.props

@ -4,4 +4,16 @@
<PackageReference Include="NuGet.Build.Tasks.Pack" Version="5.10.0" IsImplicitlyDefined="true" /> <PackageReference Include="NuGet.Build.Tasks.Pack" Version="5.10.0" IsImplicitlyDefined="true" />
</ItemGroup> </ItemGroup>
<!--
Restore the .NET workloads immediately after the .NET tooling has been installed by Arcade.
-->
<Target Name="RestoreWorkloads" AfterTargets="InstallDotNetCore">
<Message Text="Installing the .NET workloads required to build the solution..." />
<Exec Command='"$(DotNetTool)" workload restore' WorkingDirectory="$(RepoRoot)" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="OutputOfExec" />
</Exec>
</Target>
</Project> </Project>

9
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1671,6 +1671,15 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<data name="ID0445" xml:space="preserve"> <data name="ID0445" xml:space="preserve">
<value>An explicit grant type must be attached when specifying a specific response type (except when using the special response_type=none value).</value> <value>An explicit grant type must be attached when specifying a specific response type (except when using the special response_type=none value).</value>
</data> </data>
<data name="ID0446" xml:space="preserve">
<value>AS web authentication sessions are only supported on iOS 12.0 and higher.</value>
</data>
<data name="ID0447" xml:space="preserve">
<value>The current UI window cannot be resolved.</value>
</data>
<data name="ID0448" xml:space="preserve">
<value>An unknown error occurred while trying to start an AS web authentication session.</value>
</data>
<data name="ID2000" xml:space="preserve"> <data name="ID2000" xml:space="preserve">
<value>The security token is missing.</value> <value>The security token is missing.</value>
</data> </data>

4
src/OpenIddict.Client.SystemIntegration/OpenIddict.Client.SystemIntegration.csproj

@ -4,6 +4,7 @@
<TargetFrameworks> <TargetFrameworks>
$(NetFrameworkTargetFrameworks); $(NetFrameworkTargetFrameworks);
$(NetCoreTargetFrameworks); $(NetCoreTargetFrameworks);
$(NetCoreIOSTargetFrameworks);
$(NetCoreWindowsTargetFrameworks); $(NetCoreWindowsTargetFrameworks);
$(NetStandardTargetFrameworks); $(NetStandardTargetFrameworks);
$(UniversalWindowsPlatformTargetFrameworks) $(UniversalWindowsPlatformTargetFrameworks)
@ -14,7 +15,7 @@
<PropertyGroup> <PropertyGroup>
<Description>Operating system integration package for the OpenIddict client.</Description> <Description>Operating system integration package for the OpenIddict client.</Description>
<PackageTags>$(PackageTags);client;linux;windows</PackageTags> <PackageTags>$(PackageTags);client;ios;linux;windows</PackageTags>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@ -52,6 +53,7 @@
<ItemGroup> <ItemGroup>
<SupportedPlatform Remove="@(SupportedPlatform)" /> <SupportedPlatform Remove="@(SupportedPlatform)" />
<SupportedPlatform Include="ios" />
<SupportedPlatform Include="linux" /> <SupportedPlatform Include="linux" />
<SupportedPlatform Include="windows" /> <SupportedPlatform Include="windows" />
</ItemGroup> </ItemGroup>

8
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs

@ -26,5 +26,11 @@ public enum OpenIddictClientSystemIntegrationAuthenticationMode
/// and its use is generally not recommended due to its inherent limitations. /// and its use is generally not recommended due to its inherent limitations.
/// </remarks> /// </remarks>
[SupportedOSPlatform("windows10.0.17763")] [SupportedOSPlatform("windows10.0.17763")]
WebAuthenticationBroker = 1 WebAuthenticationBroker = 1,
/// <summary>
/// AS web authentication session-based authentication and logout.
/// </summary>
[SupportedOSPlatform("ios12.0")]
ASWebAuthenticationSession = 2
} }

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

@ -49,6 +49,22 @@ public sealed class OpenIddictClientSystemIntegrationBuilder
return this; return this;
} }
/// <summary>
/// Uses an AS web authentication session to start interactive authentication and logout flows.
/// </summary>
/// <returns>The <see cref="OpenIddictClientSystemIntegrationBuilder"/>.</returns>
[SupportedOSPlatform("ios12.0")]
public OpenIddictClientSystemIntegrationBuilder UseASWebAuthenticationSession()
{
if (!OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported())
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
}
return Configure(options => options.AuthenticationMode =
OpenIddictClientSystemIntegrationAuthenticationMode.ASWebAuthenticationSession);
}
/// <summary> /// <summary>
/// Uses the Windows web authentication broker to start interactive authentication and logout flows. /// Uses the Windows web authentication broker to start interactive authentication and logout flows.
/// </summary> /// </summary>

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

@ -74,7 +74,8 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
} }
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) &&
!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389));
@ -82,21 +83,27 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO
#pragma warning disable CA1416 #pragma warning disable CA1416
// If explicitly set, ensure the specified authentication mode is supported. // If explicitly set, ensure the specified authentication mode is supported.
if (options.AuthenticationMode is OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker && if (options.AuthenticationMode is OpenIddictClientSystemIntegrationAuthenticationMode.ASWebAuthenticationSession &&
!OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported())
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
}
else if (options.AuthenticationMode is OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker &&
!OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported()) !OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported())
{ {
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392));
} }
#pragma warning restore CA1416 #pragma warning restore CA1416
// Note: the OpenIddict client system integration is currently only supported on Windows options.AuthenticationMode ??= OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported() ?
// and Linux. As such, using the system browser as the default authentication method in OpenIddictClientSystemIntegrationAuthenticationMode.ASWebAuthenticationSession :
// conjunction with the embedded web server and activation handling should be always supported. OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser;
options.AuthenticationMode ??= OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser;
options.EnableActivationHandling ??= true; options.EnableActivationHandling ??= !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios"));
options.EnableActivationRedirection ??= true; options.EnableActivationRedirection ??= !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios"));
options.EnablePipeServer ??= true; options.EnablePipeServer ??= !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios"));
options.EnableEmbeddedWebServer ??= HttpListener.IsSupported; options.EnableEmbeddedWebServer ??= !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) && HttpListener.IsSupported;
// If no explicit application discriminator was specified, compute the SHA-256 hash // If no explicit application discriminator was specified, compute the SHA-256 hash
// of the application name resolved from the host and use it as a unique identifier. // of the application name resolved from the host and use it as a unique identifier.

5
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs

@ -31,7 +31,8 @@ public static class OpenIddictClientSystemIntegrationExtensions
throw new ArgumentNullException(nameof(builder)); throw new ArgumentNullException(nameof(builder));
} }
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) &&
!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) !RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{ {
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389));
@ -58,6 +59,8 @@ public static class OpenIddictClientSystemIntegrationExtensions
.Single()); .Single());
// Register the built-in filters used by the default OpenIddict client system integration event handlers. // Register the built-in filters used by the default OpenIddict client system integration event handlers.
builder.Services.TryAddSingleton<RequireASWebAuthenticationSession>();
builder.Services.TryAddSingleton<RequireASWebAuthenticationCallbackUrl>();
builder.Services.TryAddSingleton<RequireAuthenticationNonce>(); builder.Services.TryAddSingleton<RequireAuthenticationNonce>();
builder.Services.TryAddSingleton<RequireEmbeddedWebServerEnabled>(); builder.Services.TryAddSingleton<RequireEmbeddedWebServerEnabled>();
builder.Services.TryAddSingleton<RequireHttpListenerContext>(); builder.Services.TryAddSingleton<RequireHttpListenerContext>();

64
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs

@ -16,6 +16,70 @@ namespace OpenIddict.Client.SystemIntegration;
[EditorBrowsable(EditorBrowsableState.Advanced)] [EditorBrowsable(EditorBrowsableState.Advanced)]
public static class OpenIddictClientSystemIntegrationHandlerFilters public static class OpenIddictClientSystemIntegrationHandlerFilters
{ {
/// <summary>
/// Represents a filter that excludes the associated handlers if no AS web
/// authentication callback URL can be found in the transaction properties.
/// </summary>
public sealed class RequireASWebAuthenticationCallbackUrl : IOpenIddictClientHandlerFilter<BaseContext>
{
/// <inheritdoc/>
public ValueTask<bool> IsActiveAsync(BaseContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
#if SUPPORTS_AUTHENTICATION_SERVICES
if (OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported())
{
return new(ContainsASWebAuthenticationSessionResult(context.Transaction));
}
[MethodImpl(MethodImplOptions.NoInlining)]
static bool ContainsASWebAuthenticationSessionResult(OpenIddictClientTransaction transaction)
=> transaction.GetASWebAuthenticationCallbackUrl() is not null;
#endif
return new(false);
}
}
/// <summary>
/// Represents a filter that excludes the associated handlers if
/// the AS web authentication session integration was not enabled.
/// </summary>
public sealed class RequireASWebAuthenticationSession : IOpenIddictClientHandlerFilter<BaseContext>
{
private readonly IOptionsMonitor<OpenIddictClientSystemIntegrationOptions> _options;
public RequireASWebAuthenticationSession(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 SUPPORTS_AUTHENTICATION_SERVICES
if (OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported())
{
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.ASWebAuthenticationSession);
}
#endif
return new(false);
}
}
/// <summary> /// <summary>
/// Represents a filter that excludes the associated handlers /// Represents a filter that excludes the associated handlers
/// if no explicit nonce was attached to the authentication context. /// if no explicit nonce was attached to the authentication context.

168
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs

@ -12,6 +12,10 @@ using System.Text;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using OpenIddict.Extensions; using OpenIddict.Extensions;
#if SUPPORTS_AUTHENTICATION_SERVICES
using AuthenticationServices;
#endif
#if SUPPORTS_WINDOWS_RUNTIME #if SUPPORTS_WINDOWS_RUNTIME
using Windows.Security.Authentication.Web; using Windows.Security.Authentication.Web;
using Windows.UI.Core; using Windows.UI.Core;
@ -27,6 +31,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
/* /*
* Authorization request processing: * Authorization request processing:
*/ */
InvokeASWebAuthenticationSession.Descriptor,
InvokeWebAuthenticationBroker.Descriptor, InvokeWebAuthenticationBroker.Descriptor,
LaunchSystemBrowser.Descriptor, LaunchSystemBrowser.Descriptor,
@ -35,6 +40,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
*/ */
ExtractGetOrPostHttpListenerRequest<ExtractRedirectionRequestContext>.Descriptor, ExtractGetOrPostHttpListenerRequest<ExtractRedirectionRequestContext>.Descriptor,
ExtractProtocolActivationParameters<ExtractRedirectionRequestContext>.Descriptor, ExtractProtocolActivationParameters<ExtractRedirectionRequestContext>.Descriptor,
ExtractASWebAuthenticationCallbackUrlData<ExtractRedirectionRequestContext>.Descriptor,
ExtractWebAuthenticationResultData<ExtractRedirectionRequestContext>.Descriptor, ExtractWebAuthenticationResultData<ExtractRedirectionRequestContext>.Descriptor,
/* /*
@ -44,9 +50,163 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
AttachCacheControlHeader<ApplyRedirectionResponseContext>.Descriptor, AttachCacheControlHeader<ApplyRedirectionResponseContext>.Descriptor,
ProcessEmptyHttpResponse.Descriptor, ProcessEmptyHttpResponse.Descriptor,
ProcessProtocolActivationResponse<ApplyRedirectionResponseContext>.Descriptor, ProcessProtocolActivationResponse<ApplyRedirectionResponseContext>.Descriptor,
ProcessASWebAuthenticationSessionResponse<ApplyRedirectionResponseContext>.Descriptor,
ProcessWebAuthenticationResultResponse<ApplyRedirectionResponseContext>.Descriptor ProcessWebAuthenticationResultResponse<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 InvokeASWebAuthenticationSession : IOpenIddictClientHandler<ApplyAuthorizationRequestContext>
{
private readonly OpenIddictClientSystemIntegrationService _service;
public InvokeASWebAuthenticationSession(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<RequireASWebAuthenticationSession>()
.UseSingletonHandler<InvokeASWebAuthenticationSession>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
[SupportedOSPlatform("ios12.0")]
#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_AUTHENTICATION_SERVICES
if (string.IsNullOrEmpty(context.RedirectUri))
{
return;
}
if (!OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported())
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
}
var source = new TaskCompletionSource<NSUrl>(TaskCreationOptions.RunContinuationsAsynchronously);
// 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 callback validation 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, ASWebAuthenticationSession 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.
using var session = new ASWebAuthenticationSession(
url: new NSUrl(OpenIddictHelpers.AddQueryStringParameters(
uri: new Uri(context.AuthorizationEndpoint, UriKind.Absolute),
parameters: context.Transaction.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => new StringValues((string?[]?) parameter.Value))).AbsoluteUri),
callbackUrlScheme: new Uri(context.RedirectUri, UriKind.Absolute).Scheme,
completionHandler: (url, error) =>
{
if (url is not null)
{
source.SetResult(url);
}
else
{
source.SetException(new NSErrorException(error));
}
});
// On iOS 13.0 and higher, a presentation context provider returning the UI window to
// which the Safari web view will be attached MUST be provided (otherwise, a code 2
// error is returned by ASWebAuthenticationSession). To avoid that, a default provider
// pointing to the current UI window is automatically attached on iOS 13.0 and higher.
if (OpenIddictClientSystemIntegrationHelpers.IsIOSVersionAtLeast(13))
{
#pragma warning disable CA1416
session.PresentationContextProvider = new ASWebAuthenticationPresentationContext(
OpenIddictClientSystemIntegrationHelpers.GetCurrentUIWindow() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0447)));
#pragma warning restore CA1416
}
using var registration = context.CancellationToken.Register(
static state => ((ASWebAuthenticationSession) state!).Cancel(), session);
if (!session.Start())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0448));
}
NSUrl url;
try
{
url = await source.Task.WaitAsync(context.CancellationToken);
}
// Since the result of this operation is known by the time the task signaled by ASWebAuthenticationSession
// returns, canceled demands can directly be handled and surfaced here, as part of the challenge handling.
catch (NSErrorException exception) when (exception.Error.Code is
(int) ASWebAuthenticationSessionErrorCode.CanceledLogin)
{
context.Reject(
error: Errors.AccessDenied,
description: SR.GetResourceString(SR.ID2149),
uri: SR.FormatID8000(SR.ID2149));
return;
}
catch (NSErrorException)
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2136),
uri: SR.FormatID8000(SR.ID2136));
return;
}
await _service.HandleASWebAuthenticationCallbackUrlAsync(url, context.CancellationToken);
context.HandleRequest();
return;
#pragma warning restore CA1416
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
#endif
}
#if SUPPORTS_AUTHENTICATION_SERVICES
class ASWebAuthenticationPresentationContext(UIWindow window) : NSObject,
IASWebAuthenticationPresentationContextProviding
{
UIWindow IASWebAuthenticationPresentationContextProviding.GetPresentationAnchor(
ASWebAuthenticationSession session) => window;
}
#endif
}
/// <summary> /// <summary>
/// Contains the logic responsible for initiating authorization requests using the web authentication broker. /// 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. /// Note: this handler is not used when the user session is not interactive.
@ -244,6 +404,14 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
} }
} }
#if SUPPORTS_UIKIT
if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) &&
await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithUIApplicationAsync(uri))
{
context.HandleRequest();
return;
}
#endif
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri)) await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri))
{ {

167
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs

@ -12,6 +12,10 @@ using System.Text;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
using OpenIddict.Extensions; using OpenIddict.Extensions;
#if SUPPORTS_AUTHENTICATION_SERVICES
using AuthenticationServices;
#endif
#if SUPPORTS_WINDOWS_RUNTIME #if SUPPORTS_WINDOWS_RUNTIME
using Windows.Security.Authentication.Web; using Windows.Security.Authentication.Web;
using Windows.UI.Core; using Windows.UI.Core;
@ -27,6 +31,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
/* /*
* Logout request processing: * Logout request processing:
*/ */
InvokeASWebAuthenticationSession.Descriptor,
InvokeWebAuthenticationBroker.Descriptor, InvokeWebAuthenticationBroker.Descriptor,
LaunchSystemBrowser.Descriptor, LaunchSystemBrowser.Descriptor,
@ -35,6 +40,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
*/ */
ExtractGetOrPostHttpListenerRequest<ExtractPostLogoutRedirectionRequestContext>.Descriptor, ExtractGetOrPostHttpListenerRequest<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
ExtractProtocolActivationParameters<ExtractPostLogoutRedirectionRequestContext>.Descriptor, ExtractProtocolActivationParameters<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
ExtractASWebAuthenticationCallbackUrlData<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
ExtractWebAuthenticationResultData<ExtractPostLogoutRedirectionRequestContext>.Descriptor, ExtractWebAuthenticationResultData<ExtractPostLogoutRedirectionRequestContext>.Descriptor,
/* /*
@ -44,9 +50,162 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
AttachCacheControlHeader<ApplyPostLogoutRedirectionResponseContext>.Descriptor, AttachCacheControlHeader<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
ProcessEmptyHttpResponse.Descriptor, ProcessEmptyHttpResponse.Descriptor,
ProcessProtocolActivationResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor, ProcessProtocolActivationResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
ProcessASWebAuthenticationSessionResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor,
ProcessWebAuthenticationResultResponse<ApplyPostLogoutRedirectionResponseContext>.Descriptor ProcessWebAuthenticationResultResponse<ApplyPostLogoutRedirectionResponseContext>.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 InvokeASWebAuthenticationSession : IOpenIddictClientHandler<ApplyLogoutRequestContext>
{
private readonly OpenIddictClientSystemIntegrationService _service;
public InvokeASWebAuthenticationSession(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<ApplyLogoutRequestContext>()
.AddFilter<RequireInteractiveSession>()
.AddFilter<RequireASWebAuthenticationSession>()
.UseSingletonHandler<InvokeASWebAuthenticationSession>()
.SetOrder(100_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
[SupportedOSPlatform("ios12.0")]
#pragma warning disable CS1998
public async ValueTask HandleAsync(ApplyLogoutRequestContext 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_AUTHENTICATION_SERVICES
if (string.IsNullOrEmpty(context.PostLogoutRedirectUri))
{
return;
}
if (!OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported())
{
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
}
var source = new TaskCompletionSource<NSUrl>(TaskCreationOptions.RunContinuationsAsynchronously);
// OpenIddict represents the complete interactive logout dance as a two-phase process:
// - The sign-out, 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 callback validation 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, ASWebAuthenticationSession 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.
using var session = new ASWebAuthenticationSession(
url: new NSUrl(OpenIddictHelpers.AddQueryStringParameters(
uri: new Uri(context.EndSessionEndpoint, UriKind.Absolute),
parameters: context.Transaction.Request.GetParameters().ToDictionary(
parameter => parameter.Key,
parameter => new StringValues((string?[]?) parameter.Value))).AbsoluteUri),
callbackUrlScheme: new Uri(context.PostLogoutRedirectUri, UriKind.Absolute).Scheme,
completionHandler: (url, error) =>
{
if (url is not null)
{
source.SetResult(url);
}
else
{
source.SetException(new NSErrorException(error));
}
});
// On iOS 13.0 and higher, a presentation context provider returning the UI window to
// which the Safari web view will be attached MUST be provided (otherwise, a code 2
// error is returned by ASWebAuthenticationSession). To avoid that, a default provider
// pointing to the current UI window is automatically attached on iOS 13.0 and higher.
if (OpenIddictClientSystemIntegrationHelpers.IsIOSVersionAtLeast(13))
{
#pragma warning disable CA1416
session.PresentationContextProvider = new ASWebAuthenticationPresentationContext(
OpenIddictClientSystemIntegrationHelpers.GetCurrentUIWindow() ??
throw new InvalidOperationException(SR.GetResourceString(SR.ID0447)));
#pragma warning restore CA1416
}
using var registration = context.CancellationToken.Register(
static state => ((ASWebAuthenticationSession) state!).Cancel(), session);
if (!session.Start())
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0448));
}
NSUrl url;
try
{
url = await source.Task.WaitAsync(context.CancellationToken);
}
// Since the result of this operation is known by the time the task signaled by ASWebAuthenticationSession
// returns, canceled demands can directly be handled and surfaced here, as part of the challenge handling.
catch (NSErrorException exception) when (exception.Error.Code is
(int) ASWebAuthenticationSessionErrorCode.CanceledLogin)
{
context.Reject(
error: Errors.AccessDenied,
description: SR.GetResourceString(SR.ID2149),
uri: SR.FormatID8000(SR.ID2149));
return;
}
catch (NSErrorException)
{
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2136),
uri: SR.FormatID8000(SR.ID2136));
return;
}
await _service.HandleASWebAuthenticationCallbackUrlAsync(url, context.CancellationToken);
context.HandleRequest();
return;
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
#endif
}
#if SUPPORTS_AUTHENTICATION_SERVICES
class ASWebAuthenticationPresentationContext(UIWindow window) : NSObject,
IASWebAuthenticationPresentationContextProviding
{
UIWindow IASWebAuthenticationPresentationContextProviding.GetPresentationAnchor(
ASWebAuthenticationSession session) => window;
}
#endif
}
/// <summary> /// <summary>
/// Contains the logic responsible for initiating logout requests using the web authentication broker. /// Contains the logic responsible for initiating logout requests using the web authentication broker.
/// Note: this handler is not used when the user session is not interactive. /// Note: this handler is not used when the user session is not interactive.
@ -244,6 +403,14 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
} }
} }
#if SUPPORTS_UIKIT
if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) &&
await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithUIApplicationAsync(uri))
{
context.HandleRequest();
return;
}
#endif
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri)) await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri))
{ {

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

@ -39,6 +39,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
*/ */
ResolveRequestUriFromHttpListenerRequest.Descriptor, ResolveRequestUriFromHttpListenerRequest.Descriptor,
ResolveRequestUriFromProtocolActivation.Descriptor, ResolveRequestUriFromProtocolActivation.Descriptor,
ResolveRequestUriFromASWebAuthenticationCallbackUrl.Descriptor,
ResolveRequestUriFromWebAuthenticationResult.Descriptor, ResolveRequestUriFromWebAuthenticationResult.Descriptor,
InferEndpointTypeFromDynamicAddress.Descriptor, InferEndpointTypeFromDynamicAddress.Descriptor,
RejectUnknownHttpRequests.Descriptor, RejectUnknownHttpRequests.Descriptor,
@ -204,6 +205,49 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for resolving the request URI from the AS web authentication session callback URL.
/// Note: this handler is not used when the OpenID Connect request is not an AS web authentication session callback URL.
/// </summary>
public sealed class ResolveRequestUriFromASWebAuthenticationCallbackUrl : IOpenIddictClientHandler<ProcessRequestContext>
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireASWebAuthenticationCallbackUrl>()
.UseSingletonHandler<ResolveRequestUriFromASWebAuthenticationCallbackUrl>()
.SetOrder(ResolveRequestUriFromProtocolActivation.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
[SupportedOSPlatform("ios12.0")]
public ValueTask HandleAsync(ProcessRequestContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
#if SUPPORTS_AUTHENTICATION_SERVICES
(context.BaseUri, context.RequestUri) = context.Transaction.GetASWebAuthenticationCallbackUrl() switch
{
NSUrl url when Uri.TryCreate(url.AbsoluteString, UriKind.Absolute, out Uri? uri) => (
BaseUri: new UriBuilder(uri) { Path = null, Query = null, Fragment = null }.Uri,
RequestUri: uri),
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0393))
};
return default;
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
#endif
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for resolving the request URI from the web authentication result. /// Contains the logic responsible for resolving the request URI from the web authentication result.
/// Note: this handler is not used when the OpenID Connect request is not a web authentication result. /// Note: this handler is not used when the OpenID Connect request is not a web authentication result.
@ -217,7 +261,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<ProcessRequestContext>()
.AddFilter<RequireWebAuthenticationResult>() .AddFilter<RequireWebAuthenticationResult>()
.UseSingletonHandler<ResolveRequestUriFromWebAuthenticationResult>() .UseSingletonHandler<ResolveRequestUriFromWebAuthenticationResult>()
.SetOrder(ResolveRequestUriFromProtocolActivation.Descriptor.Order + 1_000) .SetOrder(ResolveRequestUriFromASWebAuthenticationCallbackUrl.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -572,6 +616,70 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for extracting OpenID Connect requests from the callback URL of an AS session.
/// Note: this handler is not used when the OpenID Connect request is not an AS web authentication session callback.
/// </summary>
public sealed class ExtractASWebAuthenticationCallbackUrlData<TContext> : IOpenIddictClientHandler<TContext> where TContext : BaseValidatingContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireASWebAuthenticationCallbackUrl>()
.UseSingletonHandler<ExtractASWebAuthenticationCallbackUrlData<TContext>>()
.SetOrder(ExtractProtocolActivationParameters<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
[SupportedOSPlatform("ios12.0")]
public ValueTask HandleAsync(TContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
#if SUPPORTS_AUTHENTICATION_SERVICES
if (context.Transaction.GetASWebAuthenticationCallbackUrl()
is not NSUrl url || !Uri.TryCreate(url.AbsoluteString, UriKind.Absolute, out Uri? uri))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0393));
}
var parameters = new Dictionary<string, StringValues>(StringComparer.Ordinal);
if (!string.IsNullOrEmpty(uri.Query))
{
foreach (var parameter in OpenIddictHelpers.ParseQuery(uri.Query))
{
parameters[parameter.Key] = parameter.Value;
}
}
// Note: the fragment is always processed after the query string to ensure that
// parameters extracted from the fragment are preferred to parameters extracted
// from the query string when they are present in both parts.
if (!string.IsNullOrEmpty(uri.Fragment))
{
foreach (var parameter in OpenIddictHelpers.ParseFragment(uri.Fragment))
{
parameters[parameter.Key] = parameter.Value;
}
}
context.Transaction.Request = new OpenIddictRequest(parameters);
return default;
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
#endif
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for extracting OpenID Connect /// Contains the logic responsible for extracting OpenID Connect
/// requests from the response data of a web authentication result. /// requests from the response data of a web authentication result.
@ -586,7 +694,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>() = OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireWebAuthenticationResult>() .AddFilter<RequireWebAuthenticationResult>()
.UseSingletonHandler<ExtractWebAuthenticationResultData<TContext>>() .UseSingletonHandler<ExtractWebAuthenticationResultData<TContext>>()
.SetOrder(ExtractProtocolActivationParameters<TContext>.Descriptor.Order + 1_000) .SetOrder(ExtractASWebAuthenticationCallbackUrlData<TContext>.Descriptor.Order + 1_000)
.SetType(OpenIddictClientHandlerType.BuiltIn) .SetType(OpenIddictClientHandlerType.BuiltIn)
.Build(); .Build();
@ -2405,6 +2513,43 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
} }
} }
/// <summary>
/// Contains the logic responsible for marking OpenID Connect responses
/// returned via AS web authentication callback URLs as processed.
/// </summary>
public sealed class ProcessASWebAuthenticationSessionResponse<TContext> : IOpenIddictClientHandler<TContext>
where TContext : BaseRequestContext
{
/// <summary>
/// Gets the default descriptor definition assigned to this handler.
/// </summary>
public static OpenIddictClientHandlerDescriptor Descriptor { get; }
= OpenIddictClientHandlerDescriptor.CreateBuilder<TContext>()
.AddFilter<RequireASWebAuthenticationCallbackUrl>()
.UseSingletonHandler<ProcessASWebAuthenticationSessionResponse<TContext>>()
.SetOrder(int.MaxValue)
.SetType(OpenIddictClientHandlerType.BuiltIn)
.Build();
/// <inheritdoc/>
public ValueTask HandleAsync(TContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
// For both protocol activations (initial or redirected) and web-view-like results,
// no proper response can be generated and eventually displayed to the user. In this
// case, simply stop processing the response and mark the request as fully handled.
//
// Note: this logic applies to both successful and errored responses.
context.HandleRequest();
return default;
}
}
/// <summary> /// <summary>
/// Contains the logic responsible for marking OpenID Connect /// Contains the logic responsible for marking OpenID Connect
/// responses returned via web authentication results as processed. /// responses returned via web authentication results as processed.

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

@ -44,6 +44,17 @@ public static class OpenIddictClientSystemIntegrationHelpers
public static HttpListenerContext? GetHttpListenerContext(this OpenIddictClientTransaction transaction) public static HttpListenerContext? GetHttpListenerContext(this OpenIddictClientTransaction transaction)
=> transaction.GetProperty<HttpListenerContext>(typeof(HttpListenerContext).FullName!); => transaction.GetProperty<HttpListenerContext>(typeof(HttpListenerContext).FullName!);
#if SUPPORTS_AUTHENTICATION_SERVICES
/// <summary>
/// Gets the AS web authentication callback URL associated with the current context.
/// </summary>
/// <param name="transaction">The transaction instance.</param>
/// <returns>The <see cref="NSUrl"/> instance or <see langword="null"/> if it couldn't be found.</returns>
[SupportedOSPlatform("ios12.0")]
public static NSUrl? GetASWebAuthenticationCallbackUrl(this OpenIddictClientTransaction transaction)
=> transaction.GetProperty<NSUrl>(typeof(NSUrl).FullName!);
#endif
#if SUPPORTS_WINDOWS_RUNTIME #if SUPPORTS_WINDOWS_RUNTIME
/// <summary> /// <summary>
/// Gets the <see cref="WebAuthenticationResult"/> associated with the current context. /// Gets the <see cref="WebAuthenticationResult"/> associated with the current context.
@ -55,6 +66,27 @@ public static class OpenIddictClientSystemIntegrationHelpers
=> transaction.GetProperty<WebAuthenticationResult>(typeof(WebAuthenticationResult).FullName!); => transaction.GetProperty<WebAuthenticationResult>(typeof(WebAuthenticationResult).FullName!);
#endif #endif
/// <summary>
/// Determines whether the current iOS version
/// is greater than or equals to the specified version.
/// </summary>
/// <returns>
/// <see langword="true"/> if the current iOS version is greater than
/// or equals to the specified version, <see langword="false"/> otherwise.
/// </returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[SupportedOSPlatformGuard("ios")]
internal static bool IsIOSVersionAtLeast(int major, int minor = 0, int build = 0)
{
#if SUPPORTS_OPERATING_SYSTEM_VERSIONS_COMPARISON
return OperatingSystem.IsIOSVersionAtLeast(major, minor, build);
#else
return RuntimeInformation.OSDescription.StartsWith("iOS ", StringComparison.OrdinalIgnoreCase) &&
RuntimeInformation.OSDescription["iOS ".Length..] is string value &&
Version.TryParse(value, out Version? version) && version >= new Version(major, minor, build);
#endif
}
/// <summary> /// <summary>
/// Determines whether the current Windows version /// Determines whether the current Windows version
/// is greater than or equals to the specified version. /// is greater than or equals to the specified version.
@ -87,6 +119,14 @@ public static class OpenIddictClientSystemIntegrationHelpers
#endif #endif
} }
/// <summary>
/// Determines whether the ASWebAuthenticationSession API is supported on this platform.
/// </summary>
/// <returns><see langword="true"/> if the ASWebAuthenticationSession API is supported, <see langword="false"/> otherwise.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[SupportedOSPlatformGuard("ios12.0")]
internal static bool IsASWebAuthenticationSessionSupported() => IsIOSVersionAtLeast(12);
/// <summary> /// <summary>
/// Determines whether the Windows Runtime APIs are supported on this platform. /// Determines whether the Windows Runtime APIs are supported on this platform.
/// </summary> /// </summary>
@ -208,6 +248,69 @@ public static class OpenIddictClientSystemIntegrationHelpers
out uint ReturnLength); out uint ReturnLength);
} }
#if SUPPORTS_UIKIT
/// <summary>
/// Gets a reference to the current <see cref="UIWindow"/>.
/// </summary>
/// <returns>The <see cref="UIWindow"/> or <see langword="null"/> if it couldn't be resolved.</returns>
internal static UIWindow? GetCurrentUIWindow()
{
var window = GetKeyWindow();
if (window is not null && window.WindowLevel == UIWindowLevel.Normal)
{
return window;
}
return GetWindows()
?.OrderByDescending(static window => window.WindowLevel)
?.Where(static window => window.RootViewController is not null)
?.Where(static window => window.WindowLevel == UIWindowLevel.Normal)
?.FirstOrDefault();
static UIWindow? GetKeyWindow()
{
if (IsIOSVersionAtLeast(13))
{
try
{
using var scenes = UIApplication.SharedApplication.ConnectedScenes;
var scene = scenes.ToArray<UIWindowScene>().FirstOrDefault();
return scene?.Windows.FirstOrDefault();
}
catch (InvalidCastException)
{
return null;
}
}
return UIApplication.SharedApplication.KeyWindow;
}
static UIWindow[]? GetWindows()
{
if (IsIOSVersionAtLeast(13))
{
try
{
using var scenes = UIApplication.SharedApplication.ConnectedScenes;
var scene = scenes.ToArray<UIWindowScene>().FirstOrDefault();
return scene?.Windows;
}
catch (InvalidCastException)
{
return null;
}
}
return UIApplication.SharedApplication.Windows;
}
}
#endif
#if SUPPORTS_WINDOWS_RUNTIME #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.
@ -284,6 +387,8 @@ public static class OpenIddictClientSystemIntegrationHelpers
/// </summary> /// </summary>
/// <param name="uri">The <see cref="Uri"/> to use.</param> /// <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> /// <returns><see langword="true"/> if the browser could be started, <see langword="false"/> otherwise.</returns>
[SupportedOSPlatform("linux")]
[SupportedOSPlatform("windows")]
internal static async Task<bool> TryLaunchBrowserWithShellExecuteAsync(Uri uri) internal static async Task<bool> TryLaunchBrowserWithShellExecuteAsync(Uri uri)
{ {
try try
@ -308,6 +413,17 @@ public static class OpenIddictClientSystemIntegrationHelpers
} }
} }
#if SUPPORTS_UIKIT
/// <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("ios")]
internal static Task<bool> TryLaunchBrowserWithUIApplicationAsync(Uri uri)
=> UIApplication.SharedApplication.OpenUrlAsync(new NSUrl(uri.AbsoluteUri), new UIApplicationOpenUrlOptions());
#endif
/// <summary> /// <summary>
/// Starts the system browser using xdg-open. /// Starts the system browser using xdg-open.
/// </summary> /// </summary>

13
src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs

@ -64,6 +64,19 @@ public sealed class OpenIddictClientSystemIntegrationService
internal Task HandleHttpRequestAsync(HttpListenerContext request, CancellationToken cancellationToken = default) internal Task HandleHttpRequestAsync(HttpListenerContext request, CancellationToken cancellationToken = default)
=> HandleRequestAsync(request ?? throw new ArgumentNullException(nameof(request)), cancellationToken); => HandleRequestAsync(request ?? throw new ArgumentNullException(nameof(request)), cancellationToken);
#if SUPPORTS_AUTHENTICATION_SERVICES
/// <summary>
/// Handles the specified AS web authentication session callback URL.
/// </summary>
/// <param name="url">The AS web authentication session callback URL.</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="url"/> is <see langword="null"/>.</exception>
[SupportedOSPlatform("ios12.0")]
internal Task HandleASWebAuthenticationCallbackUrlAsync(NSUrl url, CancellationToken cancellationToken = default)
=> HandleRequestAsync(url, cancellationToken);
#endif
#if SUPPORTS_WINDOWS_RUNTIME #if SUPPORTS_WINDOWS_RUNTIME
/// <summary> /// <summary>
/// Handles the specified web authentication result. /// Handles the specified web authentication result.

27
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpConfiguration.cs

@ -6,6 +6,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http; using System.Net.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http; using Microsoft.Extensions.Http;
@ -99,6 +100,32 @@ public sealed class OpenIddictClientSystemNetHttpConfiguration : IConfigureOptio
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)); throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName));
} }
// Note: automatic content decompression can be enabled by constructing an HttpClient wrapping
// a generic HttpClientHandler, a SocketsHttpHandler or a WinHttpHandler instance with the
// AutomaticDecompression property set to the desired algorithms (e.g GZip, Deflate or Brotli).
//
// Unfortunately, while convenient and efficient, relying on this property has a downside:
// setting AutomaticDecompression always overrides the Accept-Encoding header of all requests
// to include the selected algorithms without offering a way to make this behavior opt-in.
// Sadly, using HTTP content compression with transport security enabled has security implications
// that could potentially lead to compression side-channel attacks if the client is used with
// remote endpoints that reflect user-defined data and contain secret values (e.g BREACH attacks).
//
// Since OpenIddict itself cannot safely assume such scenarios will never happen (e.g a token request
// will typically be sent with an authorization code that can be defined by a malicious user and can
// potentially be reflected in the token response depending on the configuration of the remote server),
// it is safer to disable compression by default by not sending an Accept-Encoding header while
// still allowing encoded responses to be processed (e.g StackExchange forces content compression
// for all the supported HTTP APIs even if no Accept-Encoding header is explicitly sent by the client).
//
// For these reasons, OpenIddict doesn't rely on the automatic decompression feature and uses
// a custom event handler to deal with GZip/Deflate/Brotli-encoded responses, so that servers
// that require using HTTP compression can be supported without having to use it for all servers.
if (handler.SupportsAutomaticDecompression)
{
handler.AutomaticDecompression = DecompressionMethods.None;
}
// OpenIddict uses IHttpClientFactory to manage the creation of the HTTP clients and // OpenIddict uses IHttpClientFactory to manage the creation of the HTTP clients and
// their underlying HTTP message handlers, that are cached for the specified duration // their underlying HTTP message handlers, that are cached for the specified duration
// and re-used to process multiple requests during that period. While remote APIs are // and re-used to process multiple requests during that period. While remote APIs are

36
src/OpenIddict.Client.SystemNetHttp/OpenIddictClientSystemNetHttpHandlers.cs

@ -10,6 +10,7 @@ using System.Diagnostics;
using System.IO.Compression; using System.IO.Compression;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
@ -507,18 +508,12 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
// a generic HttpClientHandler, a SocketsHttpHandler or a WinHttpHandler instance with the // a generic HttpClientHandler, a SocketsHttpHandler or a WinHttpHandler instance with the
// AutomaticDecompression property set to the desired algorithms (e.g GZip, Deflate or Brotli). // AutomaticDecompression property set to the desired algorithms (e.g GZip, Deflate or Brotli).
// //
// Unfortunately, while convenient and efficient, relying on this property has two downsides: // Unfortunately, while convenient and efficient, relying on this property has a downside:
// // setting AutomaticDecompression always overrides the Accept-Encoding header of all requests
// - By being specific to HttpClientHandler/SocketsHttpHandler/WinHttpHandler, the automatic // to include the selected algorithms without offering a way to make this behavior opt-in.
// decompression feature cannot be used with any other type of client handler, forcing users // Sadly, using HTTP content compression with transport security enabled has security implications
// to use a specific instance configured with decompression support enforced and preventing // that could potentially lead to compression side-channel attacks if the client is used with
// them from chosing their own implementation (e.g via ConfigurePrimaryHttpMessageHandler()). // remote endpoints that reflect user-defined data and contain secret values (e.g BREACH attacks).
//
// - Setting AutomaticDecompression always overrides the Accept-Encoding header of all requests
// to include the selected algorithms without offering a way to make this behavior opt-in.
// Sadly, using HTTP content compression with transport security enabled has security implications
// that could potentially lead to compression side-channel attacks if the client is used with
// remote endpoints that reflect user-defined data and contain secret values (e.g BREACH attacks).
// //
// Since OpenIddict itself cannot safely assume such scenarios will never happen (e.g a token request // Since OpenIddict itself cannot safely assume such scenarios will never happen (e.g a token request
// will typically be sent with an authorization code that can be defined by a malicious user and can // will typically be sent with an authorization code that can be defined by a malicious user and can
@ -528,8 +523,8 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
// for all the supported HTTP APIs even if no Accept-Encoding header is explicitly sent by the client). // for all the supported HTTP APIs even if no Accept-Encoding header is explicitly sent by the client).
// //
// For these reasons, OpenIddict doesn't rely on the automatic decompression feature and uses // For these reasons, OpenIddict doesn't rely on the automatic decompression feature and uses
// a custom event handler to deal with GZip/Deflate/Brotli-encoded responses, so that providers // a custom event handler to deal with GZip/Deflate/Brotli-encoded responses, so that servers
// that require using HTTP compression can be supported without having to use it for all providers. // that require using HTTP compression can be supported without having to use it for all servers.
// This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved, // This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved,
// this may indicate that the request was incorrectly processed by another client stack. // this may indicate that the request was incorrectly processed by another client stack.
@ -542,6 +537,19 @@ public static partial class OpenIddictClientSystemNetHttpHandlers
return; return;
} }
// On iOS, the generic HttpClientHandler type instantiates a NSUrlSessionHandler under the hood.
// NSURLSession is known for enforcing response compression on certain versions of iOS: when
// using this type, an Accept-Encoding header is automatically attached by iOS and the response
// is automatically decompressed. Unfortunately, NSUrlSessionHandler doesn't remove the
// Content-Encoding header from the response, which leads to incorrect results when trying
// to decompress the content a second time. To avoid that, the entire logic used in this
// handler is ignored on iOS if the native HTTP handler (NSUrlSessionHandler) is used.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) &&
AppContext.TryGetSwitch("System.Net.Http.UseNativeHttpHandler", out bool value) && value)
{
return;
}
Stream? stream = null; Stream? stream = null;
// Iterate the returned encodings and wrap the response stream using the specified algorithm. // Iterate the returned encodings and wrap the response stream using the specified algorithm.

27
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpConfiguration.cs

@ -5,6 +5,7 @@
*/ */
using System.ComponentModel; using System.ComponentModel;
using System.Net;
using System.Net.Http; using System.Net.Http;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http; using Microsoft.Extensions.Http;
@ -92,6 +93,32 @@ public sealed class OpenIddictValidationSystemNetHttpConfiguration : IConfigureO
throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName)); throw new InvalidOperationException(SR.FormatID0373(typeof(HttpClientHandler).FullName));
} }
// Note: automatic content decompression can be enabled by constructing an HttpClient wrapping
// a generic HttpClientHandler, a SocketsHttpHandler or a WinHttpHandler instance with the
// AutomaticDecompression property set to the desired algorithms (e.g GZip, Deflate or Brotli).
//
// Unfortunately, while convenient and efficient, relying on this property has a downside:
// setting AutomaticDecompression always overrides the Accept-Encoding header of all requests
// to include the selected algorithms without offering a way to make this behavior opt-in.
// Sadly, using HTTP content compression with transport security enabled has security implications
// that could potentially lead to compression side-channel attacks if the client is used with
// remote endpoints that reflect user-defined data and contain secret values (e.g BREACH attacks).
//
// Since OpenIddict itself cannot safely assume such scenarios will never happen (e.g a token request
// will typically be sent with an authorization code that can be defined by a malicious user and can
// potentially be reflected in the token response depending on the configuration of the remote server),
// it is safer to disable compression by default by not sending an Accept-Encoding header while
// still allowing encoded responses to be processed (e.g StackExchange forces content compression
// for all the supported HTTP APIs even if no Accept-Encoding header is explicitly sent by the client).
//
// For these reasons, OpenIddict doesn't rely on the automatic decompression feature and uses
// a custom event handler to deal with GZip/Deflate/Brotli-encoded responses, so that servers
// that require using HTTP compression can be supported without having to use it for all servers.
if (handler.SupportsAutomaticDecompression)
{
handler.AutomaticDecompression = DecompressionMethods.None;
}
// OpenIddict uses IHttpClientFactory to manage the creation of the HTTP clients and // OpenIddict uses IHttpClientFactory to manage the creation of the HTTP clients and
// their underlying HTTP message handlers, that are cached for the specified duration // their underlying HTTP message handlers, that are cached for the specified duration
// and re-used to process multiple requests during that period. While remote APIs are // and re-used to process multiple requests during that period. While remote APIs are

36
src/OpenIddict.Validation.SystemNetHttp/OpenIddictValidationSystemNetHttpHandlers.cs

@ -10,6 +10,7 @@ using System.Diagnostics;
using System.IO.Compression; using System.IO.Compression;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
@ -502,18 +503,12 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
// a generic HttpClientHandler, a SocketsHttpHandler or a WinHttpHandler instance with the // a generic HttpClientHandler, a SocketsHttpHandler or a WinHttpHandler instance with the
// AutomaticDecompression property set to the desired algorithms (e.g GZip, Deflate or Brotli). // AutomaticDecompression property set to the desired algorithms (e.g GZip, Deflate or Brotli).
// //
// Unfortunately, while convenient and efficient, relying on this property has two downsides: // Unfortunately, while convenient and efficient, relying on this property has a downside:
// // setting AutomaticDecompression always overrides the Accept-Encoding header of all requests
// - By being specific to HttpClientHandler/SocketsHttpHandler/WinHttpHandler, the automatic // to include the selected algorithms without offering a way to make this behavior opt-in.
// decompression feature cannot be used with any other type of client handler, forcing users // Sadly, using HTTP content compression with transport security enabled has security implications
// to use a specific instance configured with decompression support enforced and preventing // that could potentially lead to compression side-channel attacks if the client is used with
// them from chosing their own implementation (e.g via ConfigurePrimaryHttpMessageHandler()). // remote endpoints that reflect user-defined data and contain secret values (e.g BREACH attacks).
//
// - Setting AutomaticDecompression always overrides the Accept-Encoding header of all requests
// to include the selected algorithms without offering a way to make this behavior opt-in.
// Sadly, using HTTP content compression with transport security enabled has security implications
// that could potentially lead to compression side-channel attacks if the client is used with
// remote endpoints that reflect user-defined data and contain secret values (e.g BREACH attacks).
// //
// Since OpenIddict itself cannot safely assume such scenarios will never happen (e.g a token request // Since OpenIddict itself cannot safely assume such scenarios will never happen (e.g a token request
// will typically be sent with an authorization code that can be defined by a malicious user and can // will typically be sent with an authorization code that can be defined by a malicious user and can
@ -523,8 +518,8 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
// for all the supported HTTP APIs even if no Accept-Encoding header is explicitly sent by the client). // for all the supported HTTP APIs even if no Accept-Encoding header is explicitly sent by the client).
// //
// For these reasons, OpenIddict doesn't rely on the automatic decompression feature and uses // For these reasons, OpenIddict doesn't rely on the automatic decompression feature and uses
// a custom event handler to deal with GZip/Deflate/Brotli-encoded responses, so that providers // a custom event handler to deal with GZip/Deflate/Brotli-encoded responses, so that servers
// that require using HTTP compression can be supported without having to use it for all providers. // that require using HTTP compression can be supported without having to use it for all servers.
// This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved, // This handler only applies to System.Net.Http requests. If the HTTP response cannot be resolved,
// this may indicate that the request was incorrectly processed by another client stack. // this may indicate that the request was incorrectly processed by another client stack.
@ -537,6 +532,19 @@ public static partial class OpenIddictValidationSystemNetHttpHandlers
return; return;
} }
// On iOS, the generic HttpClientHandler type instantiates a NSUrlSessionHandler under the hood.
// NSURLSession is known for enforcing response compression on certain versions of iOS: when
// using this type, an Accept-Encoding header is automatically attached by iOS and the response
// is automatically decompressed. Unfortunately, NSUrlSessionHandler doesn't remove the
// Content-Encoding header from the response, which leads to incorrect results when trying
// to decompress the content a second time. To avoid that, the entire logic used in this
// handler is ignored on iOS if the native HTTP handler (NSUrlSessionHandler) is used.
if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) &&
AppContext.TryGetSwitch("System.Net.Http.UseNativeHttpHandler", out bool value) && value)
{
return;
}
Stream? stream = null; Stream? stream = null;
// Iterate the returned encodings and wrap the response stream using the specified algorithm. // Iterate the returned encodings and wrap the response stream using the specified algorithm.

1
src/OpenIddict/OpenIddict.csproj

@ -4,6 +4,7 @@
<TargetFrameworks> <TargetFrameworks>
$(NetFrameworkTargetFrameworks); $(NetFrameworkTargetFrameworks);
$(NetCoreTargetFrameworks); $(NetCoreTargetFrameworks);
$(NetCoreIOSTargetFrameworks);
$(NetCoreWindowsTargetFrameworks); $(NetCoreWindowsTargetFrameworks);
$(NetStandardTargetFrameworks); $(NetStandardTargetFrameworks);
$(UniversalWindowsPlatformTargetFrameworks) $(UniversalWindowsPlatformTargetFrameworks)

Loading…
Cancel
Save