diff --git a/Directory.Build.props b/Directory.Build.props index 1287cab9..f949ca97 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -118,17 +118,17 @@ - net8.0-ios17.2 + net8.0-ios17.5 - net8.0-maccatalyst17.2 + net8.0-maccatalyst17.5 - net8.0-macos14.2 + net8.0-macos14.5 Exe net8.0-windows10.0.19041 - $(TargetFrameworks);net8.0-ios17.2 - $(TargetFrameworks);net8.0-maccatalyst17.2 + $(TargetFrameworks);net8.0-ios17.5 + $(TargetFrameworks);net8.0-maccatalyst17.5 true net8.0 true diff --git a/src/OpenIddict.Abstractions/OpenIddictResources.resx b/src/OpenIddict.Abstractions/OpenIddictResources.resx index 702e67ee..ee173139 100644 --- a/src/OpenIddict.Abstractions/OpenIddictResources.resx +++ b/src/OpenIddict.Abstractions/OpenIddictResources.resx @@ -1684,7 +1684,7 @@ To apply post-logout redirection responses, create a class implementing 'IOpenId 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'). - 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. + 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+). 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. @@ -2874,6 +2874,12 @@ This may indicate that the hashed entry is corrupted or malformed. The revocation request was rejected by the remote authorization server: {Response}. + + An error was returned by ASWebAuthenticationSession while trying to start a challenge operation. + + + An error was returned by ASWebAuthenticationSession while trying to start a sign-out operation. + https://documentation.openiddict.com/errors/{0} diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs index c38c61c5..d0adb393 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs +++ b/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(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 diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs index 84493d89..ac0067de 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs +++ b/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(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