From ff12f74f4fff0aab58e58a761c67fd27a4668a69 Mon Sep 17 00:00:00 2001 From: Cassius Pacheco Date: Wed, 27 Sep 2023 12:09:46 +1000 Subject: [PATCH] feat: use ASWebAuthenticationSession for iOS PKCE This is the standard way of dealing with SSO on iOS. Asynchronous error handling is now being bubbled up to PassportImpl where it can be parsed by their identifier to complete unfinished tasks. --- .../Passport/Editor/PassportPostprocess.cs | 6 +- .../Core/BrowserCommunicationsManager.cs | 33 +++++++-- .../Runtime/Scripts/Private/PassportImpl.cs | 28 +++++++- .../Runtime/Scripts/Public/Passport.cs | 2 +- .../Gree/Assets/Plugins/GreeBrowserClient.cs | 35 +++++++--- .../Gree/Assets/Plugins/WebViewObject.cs | 69 ++++++++++++++----- .../Gree/Assets/Plugins/iOS/WebView.mm | 40 ++++++++++- .../ImmutableBrowserCore/IWebBrowserClient.cs | 4 ++ .../OnUnityPostMessage.cs | 1 + .../Runtime/Core/WebBrowserClient.cs | 7 ++ .../Core/BrowserCommunicationsManagerTests.cs | 8 +++ .../Tests/Runtime/Scripts/PassportTests.cs | 9 +++ 12 files changed, 209 insertions(+), 33 deletions(-) diff --git a/src/Packages/Passport/Editor/PassportPostprocess.cs b/src/Packages/Passport/Editor/PassportPostprocess.cs index e105ae5c..0e641b15 100644 --- a/src/Packages/Passport/Editor/PassportPostprocess.cs +++ b/src/Packages/Passport/Editor/PassportPostprocess.cs @@ -84,7 +84,11 @@ public void OnPostprocessBuild(BuildReport report) var method = type.GetMethod("AddFrameworkToProject"); method.Invoke(proj, new object[] { target, "WebKit.framework", false }); } - + { + var method = type.GetMethod("AddFrameworkToProject"); + method.Invoke(proj, new object[] { target, "AuthenticationServices.framework", false }); + } + var cflags = ""; if (EditorUserBuildSettings.development) { diff --git a/src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserCommunicationsManager.cs b/src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserCommunicationsManager.cs index 60ef33e2..c8ea9e9c 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserCommunicationsManager.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserCommunicationsManager.cs @@ -19,7 +19,10 @@ namespace Immutable.Passport.Core public interface IBrowserCommunicationsManager { + public event OnUnityPostMessageDelegate? OnAuthPostMessage; + public event OnUnityPostMessageErrorDelegate? OnPostMessageError; public void SetCallTimeout(int ms); + public void LaunchAuthURL(string url); public UniTask Call(string fxName, string? data = null, bool ignoreTimeout = false); } @@ -34,6 +37,8 @@ public class BrowserCommunicationsManager : IBrowserCommunicationsManager private readonly IDictionary> requestTaskMap = new Dictionary>(); private readonly IWebBrowserClient webBrowserClient; public event OnBrowserReadyDelegate? OnReady; + public event OnUnityPostMessageDelegate? OnAuthPostMessage; + public event OnUnityPostMessageErrorDelegate? OnPostMessageError; /// /// Timeout time for waiting for each call to respond in milliseconds @@ -44,7 +49,9 @@ public class BrowserCommunicationsManager : IBrowserCommunicationsManager public BrowserCommunicationsManager(IWebBrowserClient webBrowserClient) { this.webBrowserClient = webBrowserClient; - this.webBrowserClient.OnUnityPostMessage += OnUnityPostMessage; + this.webBrowserClient.OnUnityPostMessage += onUnityPostMessage; + this.webBrowserClient.OnAuthPostMessage += onAuthPostMessage; + this.webBrowserClient.OnPostMessageError += onPostMessageError; } #region Unity to Browser @@ -84,14 +91,32 @@ private void CallFunction(string requestId, string fxName, string? data = null) webBrowserClient.ExecuteJs(js); } + public void LaunchAuthURL(string url) + { + Debug.Log($"{TAG} LaunchAuthURL"); + webBrowserClient.LaunchAuthURL(url); + } + + private void onUnityPostMessage(string message) + { + Debug.Log($"{TAG} onUnityPostMessage: {message}"); + HandleResponse(message); + } + #endregion #region Browser to Unity - private void OnUnityPostMessage(string message) + private void onAuthPostMessage(string message) { - Debug.Log($"{TAG} OnUnityPostMessage: {message}"); - HandleResponse(message); + Debug.Log($"{TAG} onAuthPostMessage: {message}"); + OnAuthPostMessage?.Invoke(message); + } + + private void onPostMessageError(string id, string message) + { + Debug.Log($"{TAG} onPostMessageError id: {id} message: {message}"); + OnPostMessageError?.Invoke(id, message); } private void HandleResponse(string message) diff --git a/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs b/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs index 67be2942..9cfda0bf 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs @@ -42,6 +42,8 @@ public PassportImpl(IBrowserCommunicationsManager communicationsManager) public async UniTask Init(string clientId, string environment, string? redirectUri = null, string? deeplink = null) { this.redirectUri = redirectUri; + this.communicationsManager.OnAuthPostMessage += OnDeepLinkActivated; + this.communicationsManager.OnPostMessageError += onPostMessageError; var versionInfo = new VersionInfo { @@ -102,6 +104,7 @@ public async UniTask Connect(long? timeoutMs = null) public async void OnDeepLinkActivated(string url) { + Debug.Log($"{TAG} OnDeepLinkActivated: {url} starts with {redirectUri}"); if (url.StartsWith(redirectUri)) await CompletePKCEFlow(url); } @@ -129,7 +132,7 @@ private async UniTask LaunchAuthUrl() AndroidJavaClass customTabLauncher = new AndroidJavaClass("com.immutable.unity.ImmutableAndroid"); customTabLauncher.CallStatic("launchUrl", activity, url); #else - Application.OpenURL(url); + communicationsManager.LaunchAuthURL(url); #endif return; } @@ -362,5 +365,28 @@ public async UniTask ZkEvmGetBalance(string address, string blockNumberO string callResponse = await communicationsManager.Call(PassportFunction.ZK_EVM.GET_BALANCE, json); return JsonConvert.DeserializeObject(callResponse).Result ?? "0x0"; } + + private void onPostMessageError(string id, string message) + { + if (id == "CallFromAuthCallbackError") + { + if (message == "") + { + Debug.Log($"{TAG} Get PKCE Auth URL user cancelled"); + pkceCompletionSource.TrySetCanceled(); + } + else + { + Debug.Log($"{TAG} Get PKCE Auth URL error: {message}"); + pkceCompletionSource.TrySetException(new PassportException( + "Something went wrong, please call ConnectPKCE() again", + PassportErrorType.AUTHENTICATION_ERROR + )); + } + return; + } + + Debug.Log($"{TAG} Unhandled onPostMessageError. id: {id} message: {message}"); + } } } \ No newline at end of file diff --git a/src/Packages/Passport/Runtime/Scripts/Public/Passport.cs b/src/Packages/Passport/Runtime/Scripts/Public/Passport.cs index 5a8d9e13..270eed6d 100644 --- a/src/Packages/Passport/Runtime/Scripts/Public/Passport.cs +++ b/src/Packages/Passport/Runtime/Scripts/Public/Passport.cs @@ -314,7 +314,7 @@ private PassportImpl GetPassportImpl() throw new PassportException("Passport not initialised"); } - private async void onDeepLinkActivated(string url) + private void onDeepLinkActivated(string url) { deeplink = url; diff --git a/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/GreeBrowserClient.cs b/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/GreeBrowserClient.cs index 031e9486..fe367bc7 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/GreeBrowserClient.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/GreeBrowserClient.cs @@ -11,20 +11,17 @@ public class GreeBrowserClient : IWebBrowserClient private const string MAC_DATA_DIRECTORY = "/Resources/Data"; private readonly WebViewObject webViewObject; public event OnUnityPostMessageDelegate OnUnityPostMessage; + public event OnUnityPostMessageDelegate OnAuthPostMessage; + public event OnUnityPostMessageErrorDelegate OnPostMessageError; public GreeBrowserClient() { webViewObject = new(); webViewObject.Init( cb: _cb, - httpErr: (msg) => - { - Debug.LogError($"{TAG} http err: {msg}"); - }, - err: (msg) => - { - Debug.LogError($"{TAG} err: {msg}"); - } + httpErr: _onPostMessageError, + err: _onPostMessageError, + auth: _onAuthPostMessage ); #if UNITY_ANDROID string filePath = Constants.SCHEME_FILE + ANDROID_DATA_DIRECTORY + Constants.PASSPORT_DATA_DIRECTORY_NAME + Constants.PASSPORT_HTML_FILE_NAME; @@ -42,6 +39,23 @@ private void _cb(string msg) InvokeOnUnityPostMessage(msg); } + private void _onAuthPostMessage(string url) + { + Debug.Log($"Received auth url: {url}"); + InvokeOnAuthPostMessage(url); + } + + private void _onPostMessageError(string id, string message) + { + Debug.LogError($"{TAG} id: {id} err: {message}"); + OnPostMessageError.Invoke(id, message); + } + + internal void InvokeOnAuthPostMessage(string message) + { + OnAuthPostMessage.Invoke(message); + } + internal void InvokeOnUnityPostMessage(string message) { OnUnityPostMessage?.Invoke(message); @@ -51,5 +65,10 @@ public void ExecuteJs(string js) { webViewObject.EvaluateJS(js); } + + public void LaunchAuthURL(string url) + { + webViewObject.LaunchAuthURL(url); + } } } \ No newline at end of file diff --git a/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/WebViewObject.cs b/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/WebViewObject.cs index 0f0b8e01..511ed3f2 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/WebViewObject.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/WebViewObject.cs @@ -40,14 +40,16 @@ #endif using Callback = System.Action; +using ErrorCallback = System.Action; #if UNITY_IPHONE || UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX public class Singleton { private static Singleton _instance; public Callback onJS; - public Callback onError; - public Callback onHttpError; + public ErrorCallback onError; + public ErrorCallback onHttpError; + public Callback onAuth; public static Singleton Instance { @@ -67,8 +69,9 @@ public class WebViewObject { private const string TAG = "[WebViewObject]"; Callback onJS; - Callback onError; - Callback onHttpError; + ErrorCallback onError; + ErrorCallback onHttpError; + Callback onAuth; #if UNITY_ANDROID class AndroidCallback : AndroidJavaProxy { @@ -101,6 +104,8 @@ private static extern void _CWebViewPlugin_LoadURL( private static extern void _CWebViewPlugin_EvaluateJS( IntPtr instance, string url); [DllImport("__Internal")] + private static extern void _CWebViewPlugin_LaunchAuthURL(IntPtr instance, string url); + [DllImport("__Internal")] private static extern void _CWebViewPlugin_SetDelegate(DelegateMessage callback); #elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX [DllImport("WebView")] @@ -139,10 +144,10 @@ private static void delegateMessageReceived(string key, string message) { return; } - if (key == "CallOnError") { + if (key == "CallOnError" || key == "CallFromAuthCallbackError") { if (Singleton.Instance.onError != null) { Debug.Log($"{TAG} ==== onError callback running message: " + message); - Singleton.Instance.onError(message); + Singleton.Instance.onError(key, message); } return; } @@ -150,7 +155,15 @@ private static void delegateMessageReceived(string key, string message) { if (key == "CallOnHttpError") { if (Singleton.Instance.onHttpError != null) { Debug.Log($"{TAG} ==== onHttpError callback running message: " + message); - Singleton.Instance.onHttpError(message); + Singleton.Instance.onHttpError(key, message); + } + return; + } + + if (key == "CallFromAuthCallback") { + if (Singleton.Instance.onAuth != null) { + Debug.Log($"{TAG} ==== CallFromAuthCallback callback running message: " + message); + Singleton.Instance.onAuth(message); } return; } @@ -170,18 +183,19 @@ public void handleMessage(string message) CallFromJS(message.Substring(i + 1)); break; case "CallOnError": - CallOnError(message.Substring(i + 1)); + CallOnError("CallOnError", message.Substring(i + 1)); break; case "CallOnHttpError": - CallOnHttpError(message.Substring(i + 1)); + CallOnHttpError("CallOnHttpError", message.Substring(i + 1)); break; } } public void Init( Callback cb = null, - Callback err = null, - Callback httpErr = null, + ErrorCallback err = null, + ErrorCallback httpErr = null, + Callback auth = null, string ua = "", // android int androidForceDarkMode = 0 // 0: follow system setting, 1: force dark off, 2: force dark on @@ -190,6 +204,7 @@ public void Init( onJS = cb; onError = err; onHttpError = httpErr; + onAuth = auth; #if UNITY_WEBGL #if !UNITY_EDITOR _gree_unity_webview_init(); @@ -202,8 +217,9 @@ public void Init( #elif UNITY_IPHONE || UNITY_STANDALONE_OSX webView = _CWebViewPlugin_Init(ua); Singleton.Instance.onJS = ((message) => CallFromJS(message)); - Singleton.Instance.onError = ((message) => CallOnError(message)); - Singleton.Instance.onHttpError = ((message) => CallOnHttpError(message)); + Singleton.Instance.onError = ((id, message) => CallOnError(id, message)); + Singleton.Instance.onHttpError = ((id, message) => CallOnHttpError(id, message)); + Singleton.Instance.onAuth = ((message) => CallOnAuth(message)); _CWebViewPlugin_SetDelegate(delegateMessageReceived); #elif UNITY_ANDROID webView = new AndroidJavaObject("net.gree.unitywebview.CWebViewPluginNoUi"); @@ -258,19 +274,38 @@ public void EvaluateJS(string js) #endif } - public void CallOnError(string error) + public void LaunchAuthURL(string url) + { +#if UNITY_IPHONE + if (webView == IntPtr.Zero) + return; + _CWebViewPlugin_LaunchAuthURL(webView, url); +#else + Application.OpenURL(url); +#endif + } + + public void CallOnError(string id, string error) { if (onError != null) { - onError(error); + onError(id, error); } } - public void CallOnHttpError(string error) + public void CallOnHttpError(string id, string error) { if (onHttpError != null) { - onHttpError(error); + onHttpError(id, error); + } + } + + public void CallOnAuth(string url) + { + if (onAuth != null) + { + onAuth(url); } } diff --git a/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/iOS/WebView.mm b/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/iOS/WebView.mm index f8a3342d..973dd8a9 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/iOS/WebView.mm +++ b/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/iOS/WebView.mm @@ -21,6 +21,7 @@ #import #import +#import // NOTE: we need extern without "C" before unity 4.5 extern "C" UIViewController *UnityGetGLViewController(); @@ -86,7 +87,7 @@ - (void)load:(NSURLRequest *)request } @end -@interface CWebViewPlugin : NSObject +@interface CWebViewPlugin : NSObject { WKWebView *webView; } @@ -97,6 +98,7 @@ @implementation CWebViewPlugin static WKProcessPool *_sharedProcessPool; static NSMutableArray *_instances = [[NSMutableArray alloc] init]; static CWebViewPlugin *__delegate = nil; +static ASWebAuthenticationSession *_authSession; - (id)initWithUa:(const char *)ua { @@ -343,6 +345,33 @@ - (void)evaluateJS:(const char *)js NSString *jsStr = [NSString stringWithUTF8String:js]; [webView evaluateJavaScript:jsStr completionHandler:^(NSString *result, NSError *error) {}]; } + +- (void)launchAuthURL:(const char *)url +{ + NSURL *URL = [[NSURL alloc] initWithString: [NSString stringWithUTF8String:url]]; + NSString *scheme = NSBundle.mainBundle.bundleIdentifier; + + _authSession = [[ASWebAuthenticationSession alloc] initWithURL:URL callbackURLScheme:scheme completionHandler:^(NSURL * _Nullable callbackURL, NSError * _Nullable error) { + _authSession = nil; + + if (error != nil && error.code == 1) { + // Cancelled + [self sendUnityCallback:"CallFromAuthCallbackError" message: ""]; + } else if (error != nil) { + [self sendUnityCallback:"CallFromAuthCallbackError" message:error.localizedDescription.UTF8String]; + } else { + [self sendUnityCallback:"CallFromAuthCallback" message: callbackURL.absoluteString.UTF8String]; + } + }]; + + _authSession.presentationContextProvider = self; + [_authSession start]; +} + +- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session +{ + return UIApplication.sharedApplication.windows.firstObject; +} @end extern "C" { @@ -351,6 +380,7 @@ - (void)evaluateJS:(const char *)js void _CWebViewPlugin_LoadURL(void *instance, const char *url); void _CWebViewPlugin_EvaluateJS(void *instance, const char *url); void _CWebViewPlugin_SetDelegate(DelegateCallbackFunction callback); + void _CWebViewPlugin_LaunchAuthURL(void *instance, const char *url); } void _CWebViewPlugin_SetDelegate(DelegateCallbackFunction callback) { @@ -389,3 +419,11 @@ void _CWebViewPlugin_EvaluateJS(void *instance, const char *js) CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; [webViewPlugin evaluateJS:js]; } + +void _CWebViewPlugin_LaunchAuthURL(void *instance, const char *url) +{ + if (instance == NULL) + return; + CWebViewPlugin *webViewPlugin = (__bridge CWebViewPlugin *)instance; + [webViewPlugin launchAuthURL:url]; +} diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/IWebBrowserClient.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/IWebBrowserClient.cs index c0502a73..e29a25c5 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/IWebBrowserClient.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/IWebBrowserClient.cs @@ -3,7 +3,11 @@ namespace Immutable.Browser.Core public interface IWebBrowserClient { event OnUnityPostMessageDelegate OnUnityPostMessage; + event OnUnityPostMessageDelegate OnAuthPostMessage; + event OnUnityPostMessageErrorDelegate OnPostMessageError; void ExecuteJs(string js); + + void LaunchAuthURL(string url); } } \ No newline at end of file diff --git a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/OnUnityPostMessage.cs b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/OnUnityPostMessage.cs index 3e313f9b..6a4a2817 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/OnUnityPostMessage.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/OnUnityPostMessage.cs @@ -1,4 +1,5 @@ namespace Immutable.Browser.Core { public delegate void OnUnityPostMessageDelegate(string data); + public delegate void OnUnityPostMessageErrorDelegate(string id, string error); } \ No newline at end of file diff --git a/src/Packages/Passport/Runtime/ThirdParty/UnityWebBrowser/Runtime/Core/WebBrowserClient.cs b/src/Packages/Passport/Runtime/ThirdParty/UnityWebBrowser/Runtime/Core/WebBrowserClient.cs index 664e0c1f..c6d7ed7e 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/UnityWebBrowser/Runtime/Core/WebBrowserClient.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/UnityWebBrowser/Runtime/Core/WebBrowserClient.cs @@ -592,6 +592,8 @@ internal void InvokeLoadProgressChange(double progress) /// Invoked when the browser goes in or out of fullscreen /// public event OnFullscreenChange OnFullscreen; + public event OnUnityPostMessageDelegate OnAuthPostMessage; + public event OnUnityPostMessageErrorDelegate OnPostMessageError; internal void InvokeFullscreen(bool fullscreen) { @@ -737,6 +739,11 @@ public void ExecuteJs(string js) communicationsManager.ExecuteJs(js); } + public void LaunchAuthURL(string url) + { + Application.OpenURL(url); + } + [DebuggerStepThrough] private void CheckIfIsReadyAndConnected() { diff --git a/src/Packages/Passport/Tests/Runtime/Scripts/Core/BrowserCommunicationsManagerTests.cs b/src/Packages/Passport/Tests/Runtime/Scripts/Core/BrowserCommunicationsManagerTests.cs index 41b370c0..11043806 100644 --- a/src/Packages/Passport/Tests/Runtime/Scripts/Core/BrowserCommunicationsManagerTests.cs +++ b/src/Packages/Passport/Tests/Runtime/Scripts/Core/BrowserCommunicationsManagerTests.cs @@ -152,6 +152,9 @@ public void CallAndResponse_Success_BrowserReady() internal class MockBrowserClient : IWebBrowserClient { public event OnUnityPostMessageDelegate? OnUnityPostMessage; + public event OnUnityPostMessageDelegate? OnAuthPostMessage; + public event OnUnityPostMessageErrorDelegate? OnPostMessageError; + public BrowserRequest? request = null; public BrowserResponse? browserResponse = null; public bool setRequestId = true; @@ -191,5 +194,10 @@ private string Between(string value, string a, string b) } return value[adjustedPosA..posB]; } + + public void LaunchAuthURL(string url) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/Packages/Passport/Tests/Runtime/Scripts/PassportTests.cs b/src/Packages/Passport/Tests/Runtime/Scripts/PassportTests.cs index 6a86bd45..33f2f433 100644 --- a/src/Packages/Passport/Tests/Runtime/Scripts/PassportTests.cs +++ b/src/Packages/Passport/Tests/Runtime/Scripts/PassportTests.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Cysharp.Threading.Tasks; using Newtonsoft.Json; +using Immutable.Browser.Core; namespace Immutable.Passport { @@ -66,6 +67,9 @@ internal class MockBrowserCommsManager : IBrowserCommunicationsManager public string response = ""; public string fxName = ""; public string? data = ""; + public event OnUnityPostMessageDelegate? OnAuthPostMessage; + public event OnUnityPostMessageErrorDelegate? OnPostMessageError; + public UniTask Call(string fxName, string? data = null, bool ignoreTimeout = false) { this.fxName = fxName; @@ -73,6 +77,11 @@ public UniTask Call(string fxName, string? data = null, bool ignoreTimeo return UniTask.FromResult(response); } + public void LaunchAuthURL(string url) + { + throw new NotImplementedException(); + } + public void SetCallTimeout(int ms) { }