diff --git a/Directory.Build.props b/Directory.Build.props index 69ec0e41..1da89f30 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -34,6 +34,18 @@ + + true + true @@ -98,6 +111,11 @@ net8.0 + + net8.0-android34.0 + + net8.0-ios12.0; diff --git a/Directory.Build.targets b/Directory.Build.targets index a03a3e15..556ef49f 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -15,39 +15,31 @@ false - - 12.0 - - - - 13.1 - - - - 10.15 - - - - 7.0 - - - - 10.0.17763 + + 21.0 + + 12.0 + + 13.1 + + 10.15 + + 7.0 + $(NoWarn);CS8002 Operating system integration package for the OpenIddict client. - $(PackageTags);client;ios;linux;maccatalyst;macos;windows + $(PackageTags);client;android;ios;linux;maccatalyst;macos;windows @@ -41,6 +49,13 @@ + + + + @@ -50,10 +65,12 @@ + + diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivationHandler.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivationHandler.cs index afe1f69f..f46a9688 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivationHandler.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationActivationHandler.cs @@ -72,15 +72,13 @@ public sealed class OpenIddictClientSystemIntegrationActivationHandler : IHosted { #if SUPPORTS_WINDOWS_RUNTIME // On platforms that support WinRT, always favor the AppInstance.GetActivatedEventArgs() API. - if (OpenIddictClientSystemIntegrationHelpers.IsAppInstanceActivationSupported() && - OpenIddictClientSystemIntegrationHelpers.GetProtocolActivationUriWithWindowsRuntime() is Uri uri) + if (IsAppInstanceActivationSupported() && GetProtocolActivationUriWithWindowsRuntime() is Uri uri) { return new OpenIddictClientSystemIntegrationActivation(uri); } #endif // Otherwise, try to extract the protocol activation from the command line arguments. - if (OpenIddictClientSystemIntegrationHelpers.GetProtocolActivationUriFromCommandLineArguments( - Environment.GetCommandLineArgs()) is Uri value) + if (GetProtocolActivationUriFromCommandLineArguments(Environment.GetCommandLineArgs()) is Uri value) { return new OpenIddictClientSystemIntegrationActivation(value); } diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs index c8c72a07..8eeb3716 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationAuthenticationMode.cs @@ -34,5 +34,11 @@ public enum OpenIddictClientSystemIntegrationAuthenticationMode [SupportedOSPlatform("ios12.0")] [SupportedOSPlatform("maccatalyst13.1")] [SupportedOSPlatform("macos10.15")] - ASWebAuthenticationSession = 2 + ASWebAuthenticationSession = 2, + + /// + /// Custom tabs intent-based authentication and logout. + /// + [SupportedOSPlatform("android21.0")] + CustomTabsIntent = 3 } diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs index b59ff58c..f3cac42e 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationBuilder.cs @@ -9,6 +9,7 @@ using System.IO.Pipes; using System.Net; using System.Runtime.Versioning; using OpenIddict.Client.SystemIntegration; +using static OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrationAuthenticationMode; namespace Microsoft.Extensions.DependencyInjection; @@ -58,13 +59,27 @@ public sealed class OpenIddictClientSystemIntegrationBuilder [SupportedOSPlatform("macos10.15")] public OpenIddictClientSystemIntegrationBuilder UseASWebAuthenticationSession() { - if (!OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported()) + if (!IsASWebAuthenticationSessionSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446)); } - return Configure(options => options.AuthenticationMode = - OpenIddictClientSystemIntegrationAuthenticationMode.ASWebAuthenticationSession); + return Configure(options => options.AuthenticationMode = ASWebAuthenticationSession); + } + + /// + /// Uses a custom tabs intent to start interactive authentication and logout flows. + /// + /// The . + [SupportedOSPlatform("android21.0")] + public OpenIddictClientSystemIntegrationBuilder UseCustomTabsIntent() + { + if (!IsCustomTabsIntentSupported()) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0452)); + } + + return Configure(options => options.AuthenticationMode = CustomTabsIntent); } /// @@ -78,13 +93,12 @@ public sealed class OpenIddictClientSystemIntegrationBuilder [SupportedOSPlatform("windows10.0.17763")] public OpenIddictClientSystemIntegrationBuilder UseWebAuthenticationBroker() { - if (!OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported()) + if (!IsWebAuthenticationBrokerSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); } - return Configure(options => options.AuthenticationMode = - OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker); + return Configure(options => options.AuthenticationMode = WebAuthenticationBroker); } /// @@ -92,8 +106,7 @@ public sealed class OpenIddictClientSystemIntegrationBuilder /// /// The . public OpenIddictClientSystemIntegrationBuilder UseSystemBrowser() - => Configure(options => options.AuthenticationMode = - OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser); + => Configure(options => options.AuthenticationMode = SystemBrowser); /// /// Sets the list of static ports the embedded web server will be allowed to diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs index 0b435020..48a8ddfe 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationConfiguration.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict.Extensions; +using static OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrationAuthenticationMode; #if !SUPPORTS_HOST_ENVIRONMENT using IHostEnvironment = Microsoft.Extensions.Hosting.IHostingEnvironment; @@ -72,7 +73,9 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO throw new ArgumentNullException(nameof(options)); } - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) && + // Ensure the operating system is supported. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create("android")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && !RuntimeInformation.IsOSPlatform(OSPlatform.Create("maccatalyst")) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && @@ -81,8 +84,15 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); } +#if !SUPPORTS_ANDROID + // When running on Android, iOS, Mac Catalyst or macOS, ensure the version compiled for + // these platforms is used to prevent the generic/non-OS specific TFM from being used. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("android"))) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); + } +#endif #if !SUPPORTS_APPKIT - // When running on iOS, Mac Catalyst or macOS, ensure the version compiled for these platforms is used. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); @@ -95,26 +105,49 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); } #endif + +#if SUPPORTS_OPERATING_SYSTEM_VERSIONS_COMPARISON + // Ensure the operating system version is supported. + if ((OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(21)) || + (OperatingSystem.IsIOS() && !OperatingSystem.IsIOSVersionAtLeast(12)) || + (OperatingSystem.IsMacCatalyst() && !OperatingSystem.IsMacCatalystVersionAtLeast(13, 1)) || + (OperatingSystem.IsMacOS() && !OperatingSystem.IsMacOSVersionAtLeast(10, 15)) || + (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(7))) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); + } +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !IsWindowsVersionAtLeast(7)) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); + } +#endif + #pragma warning disable CA1416 // If explicitly set, ensure the specified authentication mode is supported. - if (options.AuthenticationMode is OpenIddictClientSystemIntegrationAuthenticationMode.ASWebAuthenticationSession && - !OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported()) + if (options.AuthenticationMode is ASWebAuthenticationSession && !IsASWebAuthenticationSessionSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446)); } - else if (options.AuthenticationMode is OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker && - !OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported()) + else if (options.AuthenticationMode is CustomTabsIntent && !IsCustomTabsIntentSupported()) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0452)); + } + + else if (options.AuthenticationMode is WebAuthenticationBroker && !IsWebAuthenticationBrokerSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); } #pragma warning restore CA1416 - options.AuthenticationMode ??= OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported() ? - OpenIddictClientSystemIntegrationAuthenticationMode.ASWebAuthenticationSession : - OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser; + // When possible, always prefer OS-managed modes. Otherwise, fall back to the system browser. + options.AuthenticationMode ??= + IsASWebAuthenticationSessionSupported() ? ASWebAuthenticationSession : + IsCustomTabsIntentSupported() ? CustomTabsIntent : SystemBrowser; - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) && + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create("android")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) && !RuntimeInformation.IsOSPlatform(OSPlatform.Create("maccatalyst")) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { @@ -190,8 +223,7 @@ public sealed class OpenIddictClientSystemIntegrationConfiguration : IConfigureO { using var identity = WindowsIdentity.GetCurrent(TokenAccessLevels.Query); - if (!OpenIddictClientSystemIntegrationHelpers.IsWindowsVersionAtLeast(10, 0, 10240) || - !OpenIddictClientSystemIntegrationHelpers.HasAppContainerToken(identity)) + if (!IsWindowsVersionAtLeast(10, 0, 10240) || !HasAppContainerToken(identity)) { options.PipeSecurity = new PipeSecurity(); options.PipeSecurity.SetOwner(identity.User!); diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs index af20f33d..63d27902 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationExtensions.cs @@ -31,7 +31,9 @@ public static class OpenIddictClientSystemIntegrationExtensions throw new ArgumentNullException(nameof(builder)); } - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) && + // Ensure the operating system is supported. + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Create("android")) && + !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ios")) && !RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && !RuntimeInformation.IsOSPlatform(OSPlatform.Create("maccatalyst")) && !RuntimeInformation.IsOSPlatform(OSPlatform.OSX) && @@ -40,8 +42,15 @@ public static class OpenIddictClientSystemIntegrationExtensions throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); } +#if !SUPPORTS_ANDROID + // When running on Android, iOS, Mac Catalyst or macOS, ensure the version compiled for + // these platforms is used to prevent the generic/non-OS specific TFM from being used. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Create("android"))) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); + } +#endif #if !SUPPORTS_APPKIT - // When running on iOS, Mac Catalyst or macOS, ensure the version compiled for these platforms is used. if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); @@ -54,6 +63,24 @@ public static class OpenIddictClientSystemIntegrationExtensions throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0449)); } #endif + +#if SUPPORTS_OPERATING_SYSTEM_VERSIONS_COMPARISON + // Ensure the operating system version is supported. + if ((OperatingSystem.IsAndroid() && !OperatingSystem.IsAndroidVersionAtLeast(21)) || + (OperatingSystem.IsIOS() && !OperatingSystem.IsIOSVersionAtLeast(12)) || + (OperatingSystem.IsMacCatalyst() && !OperatingSystem.IsMacCatalystVersionAtLeast(13, 1)) || + (OperatingSystem.IsMacOS() && !OperatingSystem.IsMacOSVersionAtLeast(10, 15)) || + (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(7))) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); + } +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !IsWindowsVersionAtLeast(7)) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0389)); + } +#endif + // Note: the OpenIddict activation handler service is deliberately registered as early as possible to // ensure protocol activations can be handled before another service can stop the initialization of the // application (e.g Dapplo.Microsoft.Extensions.Hosting.AppServices relies on an IHostedService to implement @@ -78,6 +105,8 @@ public static class OpenIddictClientSystemIntegrationExtensions builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); + builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); builder.Services.TryAddSingleton(); diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs index 6636533d..6d809c25 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlerFilters.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using Microsoft.Extensions.Options; +using static OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrationAuthenticationMode; namespace OpenIddict.Client.SystemIntegration; @@ -31,7 +32,7 @@ public static class OpenIddictClientSystemIntegrationHandlerFilters } #if SUPPORTS_AUTHENTICATION_SERVICES && SUPPORTS_FOUNDATION - if (OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported()) + if (IsASWebAuthenticationSessionSupported()) { return new(ContainsASWebAuthenticationSessionResult(context.Transaction)); } @@ -64,7 +65,7 @@ public static class OpenIddictClientSystemIntegrationHandlerFilters } #if SUPPORTS_AUTHENTICATION_SERVICES - if (OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported()) + if (IsASWebAuthenticationSessionSupported()) { if (!context.Transaction.Properties.TryGetValue( typeof(OpenIddictClientSystemIntegrationAuthenticationMode).FullName!, out var result) || @@ -73,7 +74,7 @@ public static class OpenIddictClientSystemIntegrationHandlerFilters mode = _options.CurrentValue.AuthenticationMode.GetValueOrDefault(); } - return new(mode is OpenIddictClientSystemIntegrationAuthenticationMode.ASWebAuthenticationSession); + return new(mode is ASWebAuthenticationSession); } #endif return new(false); @@ -98,6 +99,68 @@ public static class OpenIddictClientSystemIntegrationHandlerFilters } } + /// + /// Represents a filter that excludes the associated handlers if the custom tabs intent integration was not enabled. + /// + public sealed class RequireCustomTabsIntent : IOpenIddictClientHandlerFilter + { + private readonly IOptionsMonitor _options; + + public RequireCustomTabsIntent(IOptionsMonitor options) + => _options = options ?? throw new ArgumentNullException(nameof(options)); + + /// + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + +#if SUPPORTS_ANDROID && SUPPORTS_ANDROIDX_BROWSER + if (IsCustomTabsIntentSupported()) + { + 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 CustomTabsIntent); + } +#endif + return new(false); + } + } + /// + /// Represents a filter that excludes the associated handlers if no + /// custom tabs intent data can be found in the transaction properties. + /// + public sealed class RequireCustomTabsIntentData : IOpenIddictClientHandlerFilter + { + /// + public ValueTask IsActiveAsync(BaseContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + +#if SUPPORTS_ANDROID && SUPPORTS_ANDROIDX_BROWSER + if (IsCustomTabsIntentSupported()) + { + return new(ContainsCustomTabsIntentData(context.Transaction)); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static bool ContainsCustomTabsIntentData(OpenIddictClientTransaction transaction) + => transaction.GetCustomTabsIntentData() is not null; +#endif + return new(false); + } + } + /// /// Represents a filter that excludes the associated handlers if the embedded web server was not enabled. /// @@ -197,7 +260,7 @@ public static class OpenIddictClientSystemIntegrationHandlerFilters mode = _options.CurrentValue.AuthenticationMode.GetValueOrDefault(); } - return new(mode is OpenIddictClientSystemIntegrationAuthenticationMode.SystemBrowser); + return new(mode is SystemBrowser); } } @@ -221,7 +284,7 @@ public static class OpenIddictClientSystemIntegrationHandlerFilters } #if SUPPORTS_WINDOWS_RUNTIME - if (OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported()) + if (IsWebAuthenticationBrokerSupported()) { if (!context.Transaction.Properties.TryGetValue( typeof(OpenIddictClientSystemIntegrationAuthenticationMode).FullName!, out var result) || @@ -230,7 +293,7 @@ public static class OpenIddictClientSystemIntegrationHandlerFilters mode = _options.CurrentValue.AuthenticationMode.GetValueOrDefault(); } - return new(mode is OpenIddictClientSystemIntegrationAuthenticationMode.WebAuthenticationBroker); + return new(mode is WebAuthenticationBroker); } #endif return new(false); @@ -252,7 +315,7 @@ public static class OpenIddictClientSystemIntegrationHandlerFilters } #if SUPPORTS_WINDOWS_RUNTIME - if (OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported()) + if (IsWebAuthenticationBrokerSupported()) { return new(ContainsWebAuthenticationResult(context.Transaction)); } diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs index 5e7e795a..49cae0fd 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Authentication.cs @@ -12,12 +12,17 @@ using System.Text; using Microsoft.Extensions.Primitives; using OpenIddict.Extensions; -#if SUPPORTS_AUTHENTICATION_SERVICES -using AuthenticationServices; +#if SUPPORTS_ANDROID +using Android.Content; +using NativeUri = Android.Net.Uri; +#endif + +#if SUPPORTS_ANDROID && SUPPORTS_ANDROIDX_BROWSER +using AndroidX.Browser.CustomTabs; #endif -#if SUPPORTS_FOUNDATION -using Foundation; +#if SUPPORTS_AUTHENTICATION_SERVICES +using AuthenticationServices; #endif #if SUPPORTS_APPKIT @@ -41,7 +46,8 @@ public static partial class OpenIddictClientSystemIntegrationHandlers /* * Authorization request processing: */ - InvokeASWebAuthenticationSession.Descriptor, + StartASWebAuthenticationSession.Descriptor, + LaunchCustomTabsIntent.Descriptor, InvokeWebAuthenticationBroker.Descriptor, LaunchSystemBrowser.Descriptor, @@ -51,6 +57,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers ExtractGetOrPostHttpListenerRequest.Descriptor, ExtractProtocolActivationParameters.Descriptor, ExtractASWebAuthenticationCallbackUrlData.Descriptor, + ExtractCustomTabsIntentData.Descriptor, ExtractWebAuthenticationResultData.Descriptor, /* @@ -61,18 +68,19 @@ public static partial class OpenIddictClientSystemIntegrationHandlers ProcessEmptyHttpResponse.Descriptor, ProcessProtocolActivationResponse.Descriptor, ProcessASWebAuthenticationSessionResponse.Descriptor, + ProcessCustomTabsIntentResponse.Descriptor, ProcessWebAuthenticationResultResponse.Descriptor ]); /// - /// Contains the logic responsible for initiating authorization requests using the web authentication broker. + /// Contains the logic responsible for initiating authorization requests using an AS web authentication session. /// Note: this handler is not used when the user session is not interactive. /// - public class InvokeASWebAuthenticationSession : IOpenIddictClientHandler + public class StartASWebAuthenticationSession : IOpenIddictClientHandler { private readonly OpenIddictClientSystemIntegrationService _service; - public InvokeASWebAuthenticationSession(OpenIddictClientSystemIntegrationService service) + public StartASWebAuthenticationSession(OpenIddictClientSystemIntegrationService service) => _service = service ?? throw new ArgumentNullException(nameof(service)); /// @@ -82,7 +90,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -108,7 +116,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers return; } - if (!OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported()) + if (!IsASWebAuthenticationSessionSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446)); } @@ -164,8 +172,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers { #pragma warning disable CA1416 session.PresentationContextProvider = new ASWebAuthenticationPresentationContext( - OpenIddictClientSystemIntegrationHelpers.GetCurrentUIWindow() ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0447))); + GetCurrentUIWindow() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0447))); #pragma warning restore CA1416 } #endif @@ -187,8 +194,9 @@ public static partial class OpenIddictClientSystemIntegrationHandlers // 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) + catch (NSErrorException exception) when (exception.Error is { + Domain: "com.apple.AuthenticationServices.WebAuthenticationSession", + Code : (int) ASWebAuthenticationSessionErrorCode.CanceledLogin }) { context.Reject( error: Errors.AccessDenied, @@ -211,7 +219,6 @@ public static partial class OpenIddictClientSystemIntegrationHandlers await _service.HandleASWebAuthenticationCallbackUrlAsync(url, context.CancellationToken); context.HandleRequest(); return; -#pragma warning restore CA1416 #else throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446)); #endif @@ -227,6 +234,74 @@ public static partial class OpenIddictClientSystemIntegrationHandlers #endif } + /// + /// Contains the logic responsible for initiating authorization requests using a custom tabs intent. + /// Note: this handler is not used when the user session is not interactive. + /// + public class LaunchCustomTabsIntent : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + [SupportedOSPlatform("android21.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_ANDROID && SUPPORTS_ANDROIDX_BROWSER + if (string.IsNullOrEmpty(context.RedirectUri)) + { + return; + } + + if (!IsCustomTabsIntentSupported()) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0452)); + } + + using var builder = new CustomTabsIntent.Builder(); + using var intent = builder.Build(); + + // Note: using ActivityFlags.NewTask is required when + // creating intents without a parent activity attached. + intent.Intent.AddFlags(ActivityFlags.NewTask); + + // Note: unlike iOS's ASWebAuthenticationSession or UWP's WebAuthenticationBroker, + // Android's CustomTabsIntent API doesn't support specifying a "target" URI and uses + // an asynchronous and isolated model that doesn't allow tracking the current URI. + // + // As such, the callback request can only be handled at a later stage by creating a + // custom activity responsible for handling callback URIs pointing to a custom scheme. + intent.LaunchUrl(Application.Context, NativeUri.Parse(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)!); + + context.HandleRequest(); +#else + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0452)); +#endif + } + } + /// /// 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. @@ -277,8 +352,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers // incompatible application model (e.g WinUI 3.0), the presence of a CoreWindow is verified here. // // See https://github.com/microsoft/WindowsAppSDK/issues/398 for more information. - if (!OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported() || - CoreWindow.GetForCurrentThread() is null) + if (!IsWebAuthenticationBrokerSupported() || CoreWindow.GetForCurrentThread() is null) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); } @@ -410,35 +484,41 @@ public static partial class OpenIddictClientSystemIntegrationHandlers // Runtime APIs and favor the Launcher.LaunchUriAsync() API when it's offered by the platform. #if SUPPORTS_WINDOWS_RUNTIME - if (OpenIddictClientSystemIntegrationHelpers.IsUriLauncherSupported() && await - OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithWindowsRuntimeAsync(uri)) + if (IsUriLauncherSupported() && await TryLaunchBrowserWithWindowsRuntimeAsync(uri)) { context.HandleRequest(); return; } #endif - if (await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithShellExecuteAsync(uri)) + if (await TryLaunchBrowserWithShellExecuteAsync(uri)) { context.HandleRequest(); return; } } -#if SUPPORTS_APPKIT - if (OperatingSystem.IsMacOS() && await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithNSWorkspaceAsync(uri)) +#if SUPPORTS_ANDROID + if (OperatingSystem.IsAndroid() && TryLaunchBrowserWithGenericIntent(uri)) { context.HandleRequest(); return; } -#elif SUPPORTS_UIKIT - if (OperatingSystem.IsIOS() && await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithUIApplicationAsync(uri)) +#endif + +#if SUPPORTS_UIKIT + if ((OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) && await TryLaunchBrowserWithUIApplicationAsync(uri)) + { + context.HandleRequest(); + return; + } +#elif SUPPORTS_APPKIT + if (OperatingSystem.IsMacOS() && TryLaunchBrowserWithNSWorkspace(uri)) { context.HandleRequest(); return; } #endif - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && - await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && await TryLaunchBrowserWithXdgOpenAsync(uri)) { context.HandleRequest(); return; diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs index 48f3aabf..9bc57a57 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.Session.cs @@ -12,12 +12,17 @@ using System.Text; using Microsoft.Extensions.Primitives; using OpenIddict.Extensions; -#if SUPPORTS_AUTHENTICATION_SERVICES -using AuthenticationServices; +#if SUPPORTS_ANDROID +using Android.Content; +using NativeUri = Android.Net.Uri; +#endif + +#if SUPPORTS_ANDROID && SUPPORTS_ANDROIDX_BROWSER +using AndroidX.Browser.CustomTabs; #endif -#if SUPPORTS_FOUNDATION -using Foundation; +#if SUPPORTS_AUTHENTICATION_SERVICES +using AuthenticationServices; #endif #if SUPPORTS_APPKIT @@ -41,7 +46,8 @@ public static partial class OpenIddictClientSystemIntegrationHandlers /* * Logout request processing: */ - InvokeASWebAuthenticationSession.Descriptor, + StartASWebAuthenticationSession.Descriptor, + LaunchCustomTabsIntent.Descriptor, InvokeWebAuthenticationBroker.Descriptor, LaunchSystemBrowser.Descriptor, @@ -51,6 +57,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers ExtractGetOrPostHttpListenerRequest.Descriptor, ExtractProtocolActivationParameters.Descriptor, ExtractASWebAuthenticationCallbackUrlData.Descriptor, + ExtractCustomTabsIntentData.Descriptor, ExtractWebAuthenticationResultData.Descriptor, /* @@ -61,18 +68,19 @@ public static partial class OpenIddictClientSystemIntegrationHandlers ProcessEmptyHttpResponse.Descriptor, ProcessProtocolActivationResponse.Descriptor, ProcessASWebAuthenticationSessionResponse.Descriptor, + ProcessCustomTabsIntentResponse.Descriptor, ProcessWebAuthenticationResultResponse.Descriptor ]); /// - /// Contains the logic responsible for initiating authorization requests using the web authentication broker. + /// Contains the logic responsible for initiating logout requests using an AS web authentication session. /// Note: this handler is not used when the user session is not interactive. /// - public class InvokeASWebAuthenticationSession : IOpenIddictClientHandler + public class StartASWebAuthenticationSession : IOpenIddictClientHandler { private readonly OpenIddictClientSystemIntegrationService _service; - public InvokeASWebAuthenticationSession(OpenIddictClientSystemIntegrationService service) + public StartASWebAuthenticationSession(OpenIddictClientSystemIntegrationService service) => _service = service ?? throw new ArgumentNullException(nameof(service)); /// @@ -82,7 +90,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .AddFilter() - .UseSingletonHandler() + .UseSingletonHandler() .SetOrder(100_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -108,7 +116,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers return; } - if (!OpenIddictClientSystemIntegrationHelpers.IsASWebAuthenticationSessionSupported()) + if (!IsASWebAuthenticationSessionSupported()) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0446)); } @@ -164,8 +172,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers { #pragma warning disable CA1416 session.PresentationContextProvider = new ASWebAuthenticationPresentationContext( - OpenIddictClientSystemIntegrationHelpers.GetCurrentUIWindow() ?? - throw new InvalidOperationException(SR.GetResourceString(SR.ID0447))); + GetCurrentUIWindow() ?? throw new InvalidOperationException(SR.GetResourceString(SR.ID0447))); #pragma warning restore CA1416 } #endif @@ -187,8 +194,9 @@ public static partial class OpenIddictClientSystemIntegrationHandlers // 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) + catch (NSErrorException exception) when (exception.Error is { + Domain: "com.apple.AuthenticationServices.WebAuthenticationSession", + Code : (int) ASWebAuthenticationSessionErrorCode.CanceledLogin }) { context.Reject( error: Errors.AccessDenied, @@ -226,6 +234,74 @@ public static partial class OpenIddictClientSystemIntegrationHandlers #endif } + /// + /// Contains the logic responsible for initiating logout requests using a custom tabs intent. + /// Note: this handler is not used when the user session is not interactive. + /// + public class LaunchCustomTabsIntent : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .AddFilter() + .UseSingletonHandler() + .SetOrder(100_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + [SupportedOSPlatform("android21.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_ANDROID && SUPPORTS_ANDROIDX_BROWSER + if (string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + { + return; + } + + if (!IsCustomTabsIntentSupported()) + { + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0452)); + } + + using var builder = new CustomTabsIntent.Builder(); + using var intent = builder.Build(); + + // Note: using ActivityFlags.NewTask is required when + // creating intents without a parent activity attached. + intent.Intent.AddFlags(ActivityFlags.NewTask); + + // Note: unlike iOS's ASWebAuthenticationSession or UWP's WebAuthenticationBroker, + // Android's CustomTabsIntent API doesn't support specifying a "target" URI and uses + // an asynchronous and isolated model that doesn't allow tracking the current URI. + // + // As such, the callback request can only be handled at a later stage by creating a + // custom activity responsible for handling callback URIs pointing to a custom scheme. + intent.LaunchUrl(Application.Context, NativeUri.Parse(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)!); + + context.HandleRequest(); +#else + throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0452)); +#endif + } + } + /// /// 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. @@ -276,8 +352,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers // incompatible application model (e.g WinUI 3.0), the presence of a CoreWindow is verified here. // // See https://github.com/microsoft/WindowsAppSDK/issues/398 for more information. - if (!OpenIddictClientSystemIntegrationHelpers.IsWebAuthenticationBrokerSupported() || - CoreWindow.GetForCurrentThread() is null) + if (!IsWebAuthenticationBrokerSupported() || CoreWindow.GetForCurrentThread() is null) { throw new PlatformNotSupportedException(SR.GetResourceString(SR.ID0392)); } @@ -409,35 +484,41 @@ public static partial class OpenIddictClientSystemIntegrationHandlers // Runtime APIs and favor the Launcher.LaunchUriAsync() API when it's offered by the platform. #if SUPPORTS_WINDOWS_RUNTIME - if (OpenIddictClientSystemIntegrationHelpers.IsUriLauncherSupported() && await - OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithWindowsRuntimeAsync(uri)) + if (IsUriLauncherSupported() && await TryLaunchBrowserWithWindowsRuntimeAsync(uri)) { context.HandleRequest(); return; } #endif - if (await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithShellExecuteAsync(uri)) + if (await TryLaunchBrowserWithShellExecuteAsync(uri)) { context.HandleRequest(); return; } } -#if SUPPORTS_APPKIT - if (OperatingSystem.IsMacOS() && await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithNSWorkspaceAsync(uri)) +#if SUPPORTS_ANDROID + if (OperatingSystem.IsAndroid() && TryLaunchBrowserWithGenericIntent(uri)) { context.HandleRequest(); return; } -#elif SUPPORTS_UIKIT - if (OperatingSystem.IsIOS() && await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithUIApplicationAsync(uri)) +#endif + +#if SUPPORTS_UIKIT + if ((OperatingSystem.IsIOS() || OperatingSystem.IsMacCatalyst()) && await TryLaunchBrowserWithUIApplicationAsync(uri)) + { + context.HandleRequest(); + return; + } +#elif SUPPORTS_APPKIT + if (OperatingSystem.IsMacOS() && TryLaunchBrowserWithNSWorkspace(uri)) { context.HandleRequest(); return; } #endif - if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && - await OpenIddictClientSystemIntegrationHelpers.TryLaunchBrowserWithXdgOpenAsync(uri)) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && await TryLaunchBrowserWithXdgOpenAsync(uri)) { context.HandleRequest(); return; diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs index 1ee48ac1..aad73fa1 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHandlers.cs @@ -24,6 +24,10 @@ using static OpenIddict.Client.SystemIntegration.OpenIddictClientSystemIntegrati using IHostApplicationLifetime = Microsoft.Extensions.Hosting.IApplicationLifetime; #endif +#if SUPPORTS_ANDROID +using NativeUri = Android.Net.Uri; +#endif + #if SUPPORTS_FOUNDATION using Foundation; #endif @@ -44,6 +48,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers ResolveRequestUriFromHttpListenerRequest.Descriptor, ResolveRequestUriFromProtocolActivation.Descriptor, ResolveRequestUriFromASWebAuthenticationCallbackUrl.Descriptor, + ResolveRequestUriFromCustomTabsIntentData.Descriptor, ResolveRequestUriFromWebAuthenticationResult.Descriptor, InferEndpointTypeFromDynamicAddress.Descriptor, RejectUnknownHttpRequests.Descriptor, @@ -254,6 +259,49 @@ public static partial class OpenIddictClientSystemIntegrationHandlers } } + /// + /// Contains the logic responsible for resolving the request URI from the custom tabs intent data. + /// Note: this handler is not used when the OpenID Connect request is not an custom tabs intent callback. + /// + public sealed class ResolveRequestUriFromCustomTabsIntentData : IOpenIddictClientHandler + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler() + .SetOrder(ResolveRequestUriFromASWebAuthenticationCallbackUrl.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + [SupportedOSPlatform("android21.0")] + public ValueTask HandleAsync(ProcessRequestContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + +#if SUPPORTS_ANDROID + (context.BaseUri, context.RequestUri) = context.Transaction.GetCustomTabsIntentData() switch + { + NativeUri url when Uri.TryCreate(url.ToString(), 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.ID0452)); +#endif + } + } + /// /// 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. @@ -267,7 +315,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler() - .SetOrder(ResolveRequestUriFromASWebAuthenticationCallbackUrl.Descriptor.Order + 1_000) + .SetOrder(ResolveRequestUriFromCustomTabsIntentData.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -688,6 +736,70 @@ public static partial class OpenIddictClientSystemIntegrationHandlers } } + /// + /// Contains the logic responsible for extracting OpenID Connect requests from the callback URL of a custom tabs intent. + /// Note: this handler is not used when the OpenID Connect request is not a custom tabs intent result. + /// + public sealed class ExtractCustomTabsIntentData : IOpenIddictClientHandler where TContext : BaseValidatingContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(ExtractProtocolActivationParameters.Descriptor.Order + 1_000) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + [SupportedOSPlatform("android21.0")] + public ValueTask HandleAsync(TContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + +#if SUPPORTS_ANDROID + if (context.Transaction.GetCustomTabsIntentData() + is not NativeUri url || !Uri.TryCreate(url.ToString(), UriKind.Absolute, out Uri? uri)) + { + throw new InvalidOperationException(SR.GetResourceString(SR.ID0393)); + } + + var parameters = new Dictionary(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 + } + } + /// /// Contains the logic responsible for extracting OpenID Connect /// requests from the response data of a web authentication result. @@ -702,7 +814,7 @@ public static partial class OpenIddictClientSystemIntegrationHandlers = OpenIddictClientHandlerDescriptor.CreateBuilder() .AddFilter() .UseSingletonHandler>() - .SetOrder(ExtractASWebAuthenticationCallbackUrlData.Descriptor.Order + 1_000) + .SetOrder(ExtractCustomTabsIntentData.Descriptor.Order + 1_000) .SetType(OpenIddictClientHandlerType.BuiltIn) .Build(); @@ -2521,6 +2633,42 @@ public static partial class OpenIddictClientSystemIntegrationHandlers } } + /// + /// Contains the logic responsible for marking OpenID Connect responses returned via a custom tabs intent web as processed. + /// + public sealed class ProcessCustomTabsIntentResponse : IOpenIddictClientHandler + where TContext : BaseRequestContext + { + /// + /// Gets the default descriptor definition assigned to this handler. + /// + public static OpenIddictClientHandlerDescriptor Descriptor { get; } + = OpenIddictClientHandlerDescriptor.CreateBuilder() + .AddFilter() + .UseSingletonHandler>() + .SetOrder(int.MaxValue) + .SetType(OpenIddictClientHandlerType.BuiltIn) + .Build(); + + /// + 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; + } + } + /// /// Contains the logic responsible for marking OpenID Connect responses /// returned via AS web authentication callback URLs as processed. diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs index f2f0c971..f29678dd 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHelpers.cs @@ -13,6 +13,11 @@ using System.Runtime.Versioning; using System.Security.Principal; using OpenIddict.Extensions; +#if SUPPORTS_ANDROID +using Android.Content; +using NativeUri = Android.Net.Uri; +#endif + #if SUPPORTS_FOUNDATION using Foundation; #endif @@ -68,6 +73,17 @@ public static class OpenIddictClientSystemIntegrationHelpers => transaction.GetProperty(typeof(NSUrl).FullName!); #endif +#if SUPPORTS_ANDROID && SUPPORTS_ANDROIDX_BROWSER + /// + /// Gets the custom tabs intent data associated with the current context. + /// + /// The transaction instance. + /// The instance or if it couldn't be found. + [SupportedOSPlatform("android21.0")] + public static NativeUri? GetCustomTabsIntentData(this OpenIddictClientTransaction transaction) + => transaction.GetProperty(typeof(NativeUri).FullName!); +#endif + #if SUPPORTS_WINDOWS_RUNTIME /// /// Gets the associated with the current context. @@ -128,6 +144,19 @@ public static class OpenIddictClientSystemIntegrationHelpers => false; #endif + /// + /// Determines whether the CustomTabsIntent API is supported on this platform. + /// + /// if the CustomTabsIntent API is supported, otherwise. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [SupportedOSPlatformGuard("android21.0")] + internal static bool IsCustomTabsIntentSupported() +#if SUPPORTS_OPERATING_SYSTEM_VERSIONS_COMPARISON + => OperatingSystem.IsAndroidVersionAtLeast(21); +#else + => false; +#endif + /// /// Determines whether the Windows Runtime APIs are supported on this platform. /// @@ -422,6 +451,33 @@ public static class OpenIddictClientSystemIntegrationHelpers } } +#if SUPPORTS_ANDROID + /// + /// Starts the system browser using . + /// + /// The to use. + /// if the browser could be started, otherwise. + [SupportedOSPlatform("android")] + internal static bool TryLaunchBrowserWithGenericIntent(Uri uri) + { + using var intent = new Intent(Intent.ActionView); + intent.AddFlags(ActivityFlags.NewTask); + intent.SetData(NativeUri.Parse(uri.AbsoluteUri)); + + try + { + Application.Context.StartActivity(intent); + + return true; + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + return false; + } + } +#endif + #if SUPPORTS_APPKIT /// /// Starts the system browser using . @@ -429,8 +485,18 @@ public static class OpenIddictClientSystemIntegrationHelpers /// The to use. /// if the browser could be started, otherwise. [SupportedOSPlatform("macos")] - internal static ValueTask TryLaunchBrowserWithNSWorkspaceAsync(Uri uri) - => new(NSWorkspace.SharedWorkspace.OpenUrl(new NSUrl(uri.AbsoluteUri))); + internal static bool TryLaunchBrowserWithNSWorkspace(Uri uri) + { + try + { + return NSWorkspace.SharedWorkspace.OpenUrl(new NSUrl(uri.AbsoluteUri)); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + return false; + } + } #endif #if SUPPORTS_UIKIT @@ -440,8 +506,19 @@ public static class OpenIddictClientSystemIntegrationHelpers /// The to use. /// if the browser could be started, otherwise. [SupportedOSPlatform("ios")] - internal static ValueTask TryLaunchBrowserWithUIApplicationAsync(Uri uri) - => new(UIApplication.SharedApplication.OpenUrlAsync(new NSUrl(uri.AbsoluteUri), new UIApplicationOpenUrlOptions())); + [SupportedOSPlatform("maccatalyst")] + internal static async ValueTask TryLaunchBrowserWithUIApplicationAsync(Uri uri) + { + try + { + return await UIApplication.SharedApplication.OpenUrlAsync(new NSUrl(uri.AbsoluteUri), new UIApplicationOpenUrlOptions()); + } + + catch (Exception exception) when (!OpenIddictHelpers.IsFatal(exception)) + { + return false; + } + } #endif /// diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs index b890caf9..72804afd 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationHttpListener.cs @@ -155,7 +155,7 @@ public sealed class OpenIddictClientSystemIntegrationHttpListener : BackgroundSe // configured to reject such requests) without requiring administrator rights. // // See https://www.rfc-editor.org/rfc/rfc8252#section-8.3 for more information. - if (OpenIddictClientSystemIntegrationHelpers.IsWindowsVersionAtLeast(10, 0, 10586)) + if (IsWindowsVersionAtLeast(10, 0, 10586)) { if (Socket.OSSupportsIPv4) { diff --git a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs index 6f29b691..de7099b1 100644 --- a/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs +++ b/src/OpenIddict.Client.SystemIntegration/OpenIddictClientSystemIntegrationService.cs @@ -12,6 +12,11 @@ using System.Security.Principal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; +#if SUPPORTS_ANDROID +using Android.Content; +using NativeUri = Android.Net.Uri; +#endif + #if SUPPORTS_FOUNDATION using Foundation; #endif @@ -58,6 +63,19 @@ public sealed class OpenIddictClientSystemIntegrationService OpenIddictClientSystemIntegrationActivation activation, CancellationToken cancellationToken = default) => HandleRequestAsync(activation ?? throw new ArgumentNullException(nameof(activation)), cancellationToken); +#if SUPPORTS_ANDROID && SUPPORTS_ANDROIDX_BROWSER + /// + /// Handles the specified intent. + /// + /// The intent. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + /// is . + [EditorBrowsable(EditorBrowsableState.Advanced)] + public Task HandleCustomTabsIntentAsync(Intent intent, CancellationToken cancellationToken = default) + => HandleRequestAsync(intent?.Data ?? throw new ArgumentNullException(nameof(intent)), cancellationToken); +#endif + /// /// Handles the specified HTTP request. /// diff --git a/src/OpenIddict/OpenIddict.csproj b/src/OpenIddict/OpenIddict.csproj index c074a675..5164a067 100644 --- a/src/OpenIddict/OpenIddict.csproj +++ b/src/OpenIddict/OpenIddict.csproj @@ -4,6 +4,7 @@ $(NetFrameworkTargetFrameworks); $(NetCoreTargetFrameworks); + $(NetCoreAndroidTargetFrameworks); $(NetCoreIOSTargetFrameworks); $(NetCoreMacCatalystTargetFrameworks); $(NetCoreMacOSTargetFrameworks);