Browse Source

Support the new ASWebAuthenticationSessionCallback API on iOS 17.4+/Mac Catalyst 17.4+/macOS 14.4+

pull/2166/head
Kévin Chalet 2 years ago
parent
commit
290e4150c2
  1. 6
      Directory.Build.props
  2. 12
      WorkloadRollback.json
  3. 4
      sandbox/OpenIddict.Sandbox.Maui.Client/OpenIddict.Sandbox.Maui.Client.csproj
  4. 8
      src/OpenIddict.Abstractions/OpenIddictResources.resx
  5. 106
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs
  6. 106
      src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs

6
Directory.Build.props

@ -118,17 +118,17 @@
<NetCoreIOSTargetFrameworks
Condition=" '$(NetCoreIOSTargetFrameworks)' == '' And '$(SupportsIOSTargeting)' == 'true' ">
net8.0-ios17.2
net8.0-ios17.5
</NetCoreIOSTargetFrameworks>
<NetCoreMacCatalystTargetFrameworks
Condition=" '$(NetCoreMacCatalystTargetFrameworks)' == '' And '$(SupportsMacCatalystTargeting)' == 'true' ">
net8.0-maccatalyst17.2
net8.0-maccatalyst17.5
</NetCoreMacCatalystTargetFrameworks>
<NetCoreMacOSTargetFrameworks
Condition=" '$(NetCoreMacOSTargetFrameworks)' == '' And '$(SupportsMacOSTargeting)' == 'true' ">
net8.0-macos14.2
net8.0-macos14.5
</NetCoreMacOSTargetFrameworks>
<NetCoreWindowsTargetFrameworks

12
WorkloadRollback.json

@ -1,15 +1,15 @@
{
"microsoft.net.sdk.android": "34.0.113/8.0.100",
"microsoft.net.sdk.ios": "17.2.8053/8.0.100",
"microsoft.net.sdk.maccatalyst": "17.2.8053/8.0.100",
"microsoft.net.sdk.macos": "14.2.8053/8.0.100",
"microsoft.net.sdk.maui": "8.0.61/8.0.100",
"microsoft.net.sdk.tvos": "17.2.8053/8.0.100",
"microsoft.net.sdk.ios": "17.5.8020/8.0.100",
"microsoft.net.sdk.maccatalyst": "17.5.8020/8.0.100",
"microsoft.net.sdk.macos": "14.5.8020/8.0.100",
"microsoft.net.sdk.maui": "8.0.72/8.0.100",
"microsoft.net.sdk.tvos": "17.5.8020/8.0.100",
"microsoft.net.workload.mono.toolchain.current": "8.0.8/8.0.100",
"microsoft.net.workload.emscripten.current": "8.0.8/8.0.100",
"microsoft.net.workload.emscripten.net6": "8.0.8/8.0.100",
"microsoft.net.workload.emscripten.net7": "8.0.8/8.0.100",
"microsoft.net.workload.mono.toolchain.net6": "8.0.8/8.0.100",
"microsoft.net.workload.mono.toolchain.net7": "8.0.8/8.0.100",
"microsoft.net.sdk.aspire": "8.0.2/8.0.100"
"microsoft.net.sdk.aspire": "8.1.0/8.0.100"
}

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

@ -3,8 +3,8 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks Condition=" '$(SupportsWindowsTargeting)' == 'true' ">net8.0-windows10.0.19041</TargetFrameworks>
<TargetFrameworks Condition=" '$(SupportsIOSTargeting)' == 'true' ">$(TargetFrameworks);net8.0-ios17.2</TargetFrameworks>
<TargetFrameworks Condition=" '$(SupportsMacCatalystTargeting)' == 'true' ">$(TargetFrameworks);net8.0-maccatalyst17.2</TargetFrameworks>
<TargetFrameworks Condition=" '$(SupportsIOSTargeting)' == 'true' ">$(TargetFrameworks);net8.0-ios17.5</TargetFrameworks>
<TargetFrameworks Condition=" '$(SupportsMacCatalystTargeting)' == 'true' ">$(TargetFrameworks);net8.0-maccatalyst17.5</TargetFrameworks>
<UseMaui Condition=" '$(TargetFrameworks)' != '' ">true</UseMaui>
<TargetFrameworks Condition=" '$(TargetFrameworks)' == '' ">net8.0</TargetFrameworks>
<SingleProject>true</SingleProject>

8
src/OpenIddict.Abstractions/OpenIddictResources.resx

@ -1684,7 +1684,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId
<value>The generic version of the OpenIddict.Client.SystemIntegration package cannot be used on this platform. Make sure your application is referencing the correct version by using the appropriate OS-specific TFM (e.g on macOS, 'net8.0-macos10.15').</value>
</data>
<data name="ID0450" xml:space="preserve">
<value>An HTTP/HTTPS redirect_uri or post_logout_redirect_uri cannot be used when using AS web authentication sessions. Make sure you're using a custom protocol scheme for all the callback URIs attached to the client registration.</value>
<value>An HTTP redirect_uri or post_logout_redirect_uri cannot be used when using AS web authentication sessions. Make sure you're using a custom protocol scheme for all the callback URIs attached to the client registration. Alternatively, you can register an associated domain and use an HTTPS redirect_uri or post_logout_redirect_uri pointing to that domain (supported only on iOS 17.4+, Mac Catalyst 17.4+ and macOS 14.4+).</value>
</data>
<data name="ID0451" xml:space="preserve">
<value>The Zoho integration requires sending the region of the server when using the client credentials or refresh token grants. For that, attach a ".location" authentication property containing the region to use.</value>
@ -2874,6 +2874,12 @@ This may indicate that the hashed entry is corrupted or malformed.</value>
<data name="ID6230" xml:space="preserve">
<value>The revocation request was rejected by the remote authorization server: {Response}.</value>
</data>
<data name="ID6231" xml:space="preserve">
<value>An error was returned by ASWebAuthenticationSession while trying to start a challenge operation.</value>
</data>
<data name="ID6232" xml:space="preserve">
<value>An error was returned by ASWebAuthenticationSession while trying to start a sign-out operation.</value>
</data>
<data name="ID8000" xml:space="preserve">
<value>https://documentation.openiddict.com/errors/{0}</value>
</data>

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

@ -9,6 +9,7 @@ using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using OpenIddict.Extensions;
@ -111,7 +112,8 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
#if SUPPORTS_AUTHENTICATION_SERVICES && SUPPORTS_FOUNDATION
if (string.IsNullOrEmpty(context.RedirectUri))
if (string.IsNullOrEmpty(context.RedirectUri) ||
!Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri))
{
return;
}
@ -121,13 +123,6 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
}
if (!Uri.TryCreate(context.RedirectUri, UriKind.Absolute, out Uri? uri) ||
(string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0450));
}
var source = new TaskCompletionSource<NSUrl>(TaskCreationOptions.RunContinuationsAsynchronously);
// OpenIddict represents the complete interactive authentication dance as a two-phase process:
@ -143,30 +138,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
// 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: uri.Scheme,
completionHandler: (url, error) =>
{
if (url is not null)
{
source.SetResult(url);
}
else if (error is not null)
{
source.SetException(new NSErrorException(error));
}
else
{
source.SetException(new InvalidOperationException(SR.GetResourceString(SR.ID0448)));
}
});
using var session = CreateASWebAuthenticationSession();
#if SUPPORTS_PRESENTATION_CONTEXT_PROVIDER
// On iOS 13.0 and higher, a presentation context provider returning the UI window to
@ -211,8 +183,10 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
return;
}
catch (NSErrorException)
catch (NSErrorException exception)
{
context.Logger.LogError(exception, SR.GetResourceString(SR.ID6231));
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2136),
@ -224,6 +198,72 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
await _service.HandleASWebAuthenticationCallbackUrlAsync(url, context.CancellationToken);
context.HandleRequest();
return;
ASWebAuthenticationSession CreateASWebAuthenticationSession()
{
// Starting with iOS 17.4+, Mac Catalyst 17.4+ and macOS 14.4+, the ASWebAuthenticationSession initializer
// accepting a custom scheme string is now deprecated and is replaced by an initializer taking an
// ASWebAuthenticationSessionCallback object, which allows supporting HTTPS callback URIs/Universal Links.
if (OperatingSystem.IsIOSVersionAtLeast(17, 4) ||
OperatingSystem.IsMacCatalystVersionAtLeast(17, 4) ||
OperatingSystem.IsMacOSVersionAtLeast(14, 4))
{
return new ASWebAuthenticationSession(
url: CreateUrl(),
callback: uri switch
{
// Note: non-default ports are not allowed in associated domains, that are
// required to use HTTPS URIs with the ASWebAuthenticationSessionCallback API.
Uri { IsDefaultPort: true } uri when string.Equals(
uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
=> ASWebAuthenticationSessionCallback.Create(
httpsHost: uri.Host,
path : uri.AbsolutePath is ['/', _, ..] ? uri.AbsolutePath[1..] : uri.AbsoluteUri),
Uri uri when !string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
=> ASWebAuthenticationSessionCallback.Create(uri.Scheme),
// HTTP-only callback URIs and HTTPS URIs using non-default ports
// are not supported by the ASWebAuthenticationSessionCallback API.
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0450))
},
completionHandler: HandleCallback);
}
// On older platforms, only callback URIs using a custom scheme can be used.
if (string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0450));
}
return new ASWebAuthenticationSession(CreateUrl(), uri.Scheme, HandleCallback);
NSUrl CreateUrl() => new(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);
void HandleCallback(NSUrl? url, NSError? error)
{
if (url is not null)
{
source.SetResult(url);
}
else if (error is not null)
{
source.SetException(new NSErrorException(error));
}
else
{
source.SetException(new InvalidOperationException(SR.GetResourceString(SR.ID0448)));
}
}
}
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
#endif

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

@ -9,6 +9,7 @@ using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using OpenIddict.Extensions;
@ -111,7 +112,8 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
Debug.Assert(context.Transaction.Request is not null, SR.GetResourceString(SR.ID4008));
#if SUPPORTS_AUTHENTICATION_SERVICES && SUPPORTS_FOUNDATION
if (string.IsNullOrEmpty(context.PostLogoutRedirectUri))
if (string.IsNullOrEmpty(context.PostLogoutRedirectUri) ||
!Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri? uri))
{
return;
}
@ -121,13 +123,6 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
}
if (!Uri.TryCreate(context.PostLogoutRedirectUri, UriKind.Absolute, out Uri? uri) ||
(string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0450));
}
var source = new TaskCompletionSource<NSUrl>(TaskCreationOptions.RunContinuationsAsynchronously);
// OpenIddict represents the complete interactive logout dance as a two-phase process:
@ -143,30 +138,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
// 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: uri.Scheme,
completionHandler: (url, error) =>
{
if (url is not null)
{
source.SetResult(url);
}
else if (error is not null)
{
source.SetException(new NSErrorException(error));
}
else
{
source.SetException(new InvalidOperationException(SR.GetResourceString(SR.ID0448)));
}
});
using var session = CreateASWebAuthenticationSession();
#if SUPPORTS_PRESENTATION_CONTEXT_PROVIDER
// On iOS 13.0 and higher, a presentation context provider returning the UI window to
@ -211,8 +183,10 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
return;
}
catch (NSErrorException)
catch (NSErrorException exception)
{
context.Logger.LogError(exception, SR.GetResourceString(SR.ID6232));
context.Reject(
error: Errors.ServerError,
description: SR.GetResourceString(SR.ID2136),
@ -224,6 +198,72 @@ public static partial class OpenIddictClientSystemIntegrationHandlers
await _service.HandleASWebAuthenticationCallbackUrlAsync(url, context.CancellationToken);
context.HandleRequest();
return;
ASWebAuthenticationSession CreateASWebAuthenticationSession()
{
// Starting with iOS 17.4+, Mac Catalyst 17.4+ and macOS 14.4+, the ASWebAuthenticationSession initializer
// accepting a custom scheme string is now deprecated and is replaced by an initializer taking an
// ASWebAuthenticationSessionCallback object, which allows supporting HTTPS callback URIs/Universal Links.
if (OperatingSystem.IsIOSVersionAtLeast(17, 4) ||
OperatingSystem.IsMacCatalystVersionAtLeast(17, 4) ||
OperatingSystem.IsMacOSVersionAtLeast(14, 4))
{
return new ASWebAuthenticationSession(
url: CreateUrl(),
callback: uri switch
{
// Note: non-default ports are not allowed in associated domains, that are
// required to use HTTPS URIs with the ASWebAuthenticationSessionCallback API.
Uri { IsDefaultPort: true } uri when string.Equals(
uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
=> ASWebAuthenticationSessionCallback.Create(
httpsHost: uri.Host,
path : uri.AbsolutePath is ['/', _, ..] ? uri.AbsolutePath[1..] : uri.AbsoluteUri),
Uri uri when !string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
=> ASWebAuthenticationSessionCallback.Create(uri.Scheme),
// HTTP-only callback URIs and HTTPS URIs using non-default ports
// are not supported by the ASWebAuthenticationSessionCallback API.
_ => throw new InvalidOperationException(SR.GetResourceString(SR.ID0450))
},
completionHandler: HandleCallback);
}
// On older platforms, only callback URIs using a custom scheme can be used.
if (string.Equals(uri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) ||
string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidOperationException(SR.GetResourceString(SR.ID0450));
}
return new ASWebAuthenticationSession(CreateUrl(), uri.Scheme, HandleCallback);
NSUrl CreateUrl() => new(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);
void HandleCallback(NSUrl? url, NSError? error)
{
if (url is not null)
{
source.SetResult(url);
}
else if (error is not null)
{
source.SetException(new NSErrorException(error));
}
else
{
source.SetException(new InvalidOperationException(SR.GetResourceString(SR.ID0448)));
}
}
}
#else
throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446));
#endif

Loading…
Cancel
Save