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);