|
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using AuthenticationServices;
using Foundation;
#if __IOS__
using SafariServices;
#endif
using ObjCRuntime;
using UIKit;
using WebKit;
using Microsoft.Maui.Authentication;
using Microsoft.Maui.ApplicationModel;
namespace Microsoft.Maui.Authentication
{
partial class WebAuthenticatorImplementation : IWebAuthenticator, IPlatformWebAuthenticatorCallback
{
#if __IOS__
const int asWebAuthenticationSessionErrorCodeCanceledLogin = 1;
const string asWebAuthenticationSessionErrorDomain = "com.apple.AuthenticationServices.WebAuthenticationSession";
const int sfAuthenticationErrorCanceledLogin = 1;
const string sfAuthenticationErrorDomain = "com.apple.SafariServices.Authentication";
#endif
TaskCompletionSource<WebAuthenticatorResult> tcsResponse;
UIViewController currentViewController;
Uri redirectUri;
WebAuthenticatorOptions currentOptions;
#if __IOS__
ASWebAuthenticationSession was;
SFAuthenticationSession sf;
#endif
public async Task<WebAuthenticatorResult> AuthenticateAsync(WebAuthenticatorOptions webAuthenticatorOptions)
{
currentOptions = webAuthenticatorOptions;
var url = webAuthenticatorOptions?.Url;
var callbackUrl = webAuthenticatorOptions?.CallbackUrl;
var prefersEphemeralWebBrowserSession = webAuthenticatorOptions?.PrefersEphemeralWebBrowserSession ?? false;
if (!VerifyHasUrlSchemeOrDoesntRequire(callbackUrl.Scheme))
throw new InvalidOperationException("You must register your URL Scheme handler in your app's Info.plist.");
// Cancel any previous task that's still pending
if (tcsResponse?.Task != null && !tcsResponse.Task.IsCompleted)
tcsResponse.TrySetCanceled();
tcsResponse = new TaskCompletionSource<WebAuthenticatorResult>();
redirectUri = callbackUrl;
var scheme = redirectUri.Scheme;
#if __IOS__
void AuthSessionCallback(NSUrl cbUrl, NSError error)
{
if (error == null)
OpenUrlCallback(cbUrl);
else if (error.Domain == asWebAuthenticationSessionErrorDomain && error.Code == asWebAuthenticationSessionErrorCodeCanceledLogin)
tcsResponse.TrySetCanceled();
else if (error.Domain == sfAuthenticationErrorDomain && error.Code == sfAuthenticationErrorCanceledLogin)
tcsResponse.TrySetCanceled();
else
tcsResponse.TrySetException(new NSErrorException(error));
was = null;
sf = null;
}
if (OperatingSystem.IsIOSVersionAtLeast(12))
{
#if IOS17_4_OR_GREATER || MACCATALYST17_4_OR_GREATER
if (OperatingSystem.IsIOSVersionAtLeast(17, 4) || OperatingSystem.IsMacCatalystVersionAtLeast(17, 4))
{
// Use the new ASWebAuthenticationSession constructor with ASWebAuthenticationSessionCallback overload
var callback = ASWebAuthenticationSessionCallback.Create(scheme);
was = new ASWebAuthenticationSession(WebUtils.GetNativeUrl(url), callback, AuthSessionCallback);
}
else
#endif
{
// Fallback to the original ASWebAuthenticationSession constructor for iOS versions below 17.4
was = new ASWebAuthenticationSession(WebUtils.GetNativeUrl(url), scheme, AuthSessionCallback);
}
if (OperatingSystem.IsIOSVersionAtLeast(13))
{
var ctx = new ContextProvider(WindowStateManager.Default.GetCurrentUIWindow());
was.PresentationContextProvider = ctx;
was.PrefersEphemeralWebBrowserSession = prefersEphemeralWebBrowserSession;
}
else if (prefersEphemeralWebBrowserSession)
{
ClearCookies();
}
using (was)
{
#pragma warning disable CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
was.Start();
#pragma warning restore CA1416
return await tcsResponse.Task;
}
}
if (prefersEphemeralWebBrowserSession)
ClearCookies();
#pragma warning disable CA1422 // 'SFAuthenticationSession' is obsoleted on: 'ios' 12.0 and later
if (OperatingSystem.IsIOSVersionAtLeast(11))
{
sf = new SFAuthenticationSession(WebUtils.GetNativeUrl(url), scheme, AuthSessionCallback);
using (sf)
{
sf.Start();
return await tcsResponse.Task;
}
}
#pragma warning restore CA1422
// This is only on iOS9+ but we only support 10+ in Essentials anyway
var controller = new SFSafariViewController(WebUtils.GetNativeUrl(url), false)
{
Delegate = new NativeSFSafariViewControllerDelegate
{
DidFinishHandler = (svc) =>
{
// Cancel our task if it wasn't already marked as completed
if (!(tcsResponse?.Task?.IsCompleted ?? true))
tcsResponse.TrySetCanceled();
}
},
};
currentViewController = controller;
await WindowStateManager.Default.GetCurrentUIViewController().PresentViewControllerAsync(controller, true);
#else
var opened = UIApplication.SharedApplication.OpenUrl(url);
if (!opened)
tcsResponse.TrySetException(new Exception("Error opening Safari"));
#endif
return await tcsResponse.Task;
}
void ClearCookies()
{
NSUrlCache.SharedCache.RemoveAllCachedResponses();
#if __IOS__
if (OperatingSystem.IsIOSVersionAtLeast(11))
{
WKWebsiteDataStore.DefaultDataStore.HttpCookieStore.GetAllCookies((cookies) =>
{
foreach (var cookie in cookies)
{
#pragma warning disable CA1416 // Known false positive with lambda, here we can also assert the version
WKWebsiteDataStore.DefaultDataStore.HttpCookieStore.DeleteCookie(cookie, null);
#pragma warning restore CA1416
}
});
}
#endif
}
public bool OpenUrlCallback(Uri uri)
{
// If we aren't waiting on a task, don't handle the url
if (tcsResponse?.Task?.IsCompleted ?? true)
return false;
try
{
// If we can't handle the url, don't
if (!WebUtils.CanHandleCallback(redirectUri, uri))
return false;
currentViewController?.DismissViewControllerAsync(true);
currentViewController = null;
tcsResponse.TrySetResult(new WebAuthenticatorResult(uri, currentOptions?.ResponseDecoder));
return true;
}
catch (Exception ex)
{
// TODO change this to ILogger?
Console.WriteLine(ex);
}
return false;
}
static bool VerifyHasUrlSchemeOrDoesntRequire(string scheme)
{
// iOS11+ uses sfAuthenticationSession which handles its own url routing
if (OperatingSystem.IsIOSVersionAtLeast(11, 0) || OperatingSystem.IsTvOSVersionAtLeast(11, 0))
return true;
return AppInfoImplementation.VerifyHasUrlScheme(scheme);
}
#if __IOS__
class NativeSFSafariViewControllerDelegate : SFSafariViewControllerDelegate
{
public Action<SFSafariViewController> DidFinishHandler { get; set; }
public override void DidFinish(SFSafariViewController controller) =>
DidFinishHandler?.Invoke(controller);
}
class ContextProvider : NSObject, IASWebAuthenticationPresentationContextProviding
{
public ContextProvider(UIWindow window) =>
Window = window;
public readonly UIWindow Window;
[Export("presentationAnchorForWebAuthenticationSession:")]
public UIWindow GetPresentationAnchor(ASWebAuthenticationSession session)
=> Window;
}
#endif
}
}
|