From 1626b03f2800b0feee974b49bb4ea5b0a05eafb1 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. --- .../Passport/Editor/PassportPostprocess.cs | 6 ++- .../Core/BrowserCommunicationsManager.cs | 22 ++++++++-- .../Runtime/Scripts/Private/PassportImpl.cs | 4 +- .../Runtime/Scripts/Public/Passport.cs | 2 +- .../Gree/Assets/Plugins/GreeBrowserClient.cs | 20 ++++++++- .../Gree/Assets/Plugins/WebViewObject.cs | 42 +++++++++++++++++++ .../Gree/Assets/Plugins/iOS/WebView.mm | 37 +++++++++++++++- .../ImmutableBrowserCore/IWebBrowserClient.cs | 3 ++ .../Runtime/Core/WebBrowserClient.cs | 6 +++ .../Core/BrowserCommunicationsManagerTests.cs | 7 ++++ .../Tests/Runtime/Scripts/PassportTests.cs | 8 ++++ 11 files changed, 149 insertions(+), 8 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..d376eb6b 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserCommunicationsManager.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/Core/BrowserCommunicationsManager.cs @@ -19,7 +19,9 @@ namespace Immutable.Passport.Core public interface IBrowserCommunicationsManager { + public event OnUnityPostMessageDelegate? OnAuthPostMessage; public void SetCallTimeout(int ms); + public void LaunchAuthURL(string url); public UniTask Call(string fxName, string? data = null, bool ignoreTimeout = false); } @@ -34,6 +36,7 @@ public class BrowserCommunicationsManager : IBrowserCommunicationsManager private readonly IDictionary> requestTaskMap = new Dictionary>(); private readonly IWebBrowserClient webBrowserClient; public event OnBrowserReadyDelegate? OnReady; + public event OnUnityPostMessageDelegate? OnAuthPostMessage; /// /// Timeout time for waiting for each call to respond in milliseconds @@ -45,6 +48,7 @@ public BrowserCommunicationsManager(IWebBrowserClient webBrowserClient) { this.webBrowserClient = webBrowserClient; this.webBrowserClient.OnUnityPostMessage += OnUnityPostMessage; + this.webBrowserClient.OnAuthPostMessage += onUnityPostMessage; } #region Unity to Browser @@ -84,9 +88,11 @@ private void CallFunction(string requestId, string fxName, string? data = null) webBrowserClient.ExecuteJs(js); } - #endregion - - #region Browser to Unity + public void LaunchAuthURL(string url) + { + Debug.Log($"{TAG} LaunchAuthURL"); + webBrowserClient.LaunchAuthURL(url); + } private void OnUnityPostMessage(string message) { @@ -94,6 +100,16 @@ private void OnUnityPostMessage(string message) HandleResponse(message); } + #endregion + + #region Browser to Unity + + private void onUnityPostMessage(string message) + { + Debug.Log($"{TAG} onUnityPostMessage: {message}"); + OnAuthPostMessage?.Invoke(message); + } + private void HandleResponse(string message) { Debug.Log($"{TAG} HandleResponse message: " + message); diff --git a/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs b/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs index 2dae7707..ef3b889d 100644 --- a/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs +++ b/src/Packages/Passport/Runtime/Scripts/Private/PassportImpl.cs @@ -36,6 +36,7 @@ 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; InitRequest request = new() { ClientId = clientId, Environment = environment, RedirectUri = redirectUri }; string response = await communicationsManager.Call( @@ -87,6 +88,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); } @@ -107,7 +109,7 @@ private async UniTask LaunchAuthUrl() if (response?.Success == true && response?.Result != null) { - Application.OpenURL(response.Result.Replace(" ", "+")); + communicationsManager.LaunchAuthURL(response.Result.Replace(" ", "+")); return; } else 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..93ca3c5b 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/GreeBrowserClient.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/GreeBrowserClient.cs @@ -11,6 +11,7 @@ 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 GreeBrowserClient() { @@ -24,7 +25,8 @@ public GreeBrowserClient() err: (msg) => { Debug.LogError($"{TAG} err: {msg}"); - } + }, + 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 +44,17 @@ private void _cb(string msg) InvokeOnUnityPostMessage(msg); } + private void _onAuthPostMessage(string url) + { + Debug.Log($"Received auth url: {url}"); + InvokeOnAuthPostMessage(url); + } + + internal void InvokeOnAuthPostMessage(string message) + { + OnAuthPostMessage.Invoke(message); + } + internal void InvokeOnUnityPostMessage(string message) { OnUnityPostMessage?.Invoke(message); @@ -51,5 +64,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 10d3abc3..7e264239 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/WebViewObject.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/Gree/Assets/Plugins/WebViewObject.cs @@ -48,6 +48,7 @@ public class Singleton public Callback onJS; public Callback onError; public Callback onHttpError; + public Callback onAuth; public static Singleton Instance { @@ -69,6 +70,7 @@ public class WebViewObject Callback onJS; Callback onError; Callback onHttpError; + Callback onAuth; #if UNITY_ANDROID class AndroidCallback : AndroidJavaProxy { @@ -101,6 +103,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")] @@ -155,6 +159,22 @@ private static void delegateMessageReceived(string key, string message) { return; } + if (key == "CallFromAuthCallbackError") { + if (Singleton.Instance.onError != null) { + Debug.Log($"{TAG} ==== onError callback running message: " + message); + Singleton.Instance.onError(message); + } + return; + } + + if (key == "CallFromAuthCallback") { + if (Singleton.Instance.onAuth != null) { + Debug.Log($"{TAG} ==== CallFromAuthCallback callback running message: " + message); + Singleton.Instance.onAuth(message); + } + return; + } + Debug.Log($"{TAG} delegateMessageReceived unsupported key " + key); } #endif @@ -182,6 +202,7 @@ public void Init( Callback cb = null, Callback err = null, Callback 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 +211,7 @@ public void Init( onJS = cb; onError = err; onHttpError = httpErr; + onAuth = auth; #if UNITY_WEBGL #if !UNITY_EDITOR _gree_unity_webview_init(); @@ -204,6 +226,7 @@ public void Init( Singleton.Instance.onJS = ((message) => CallFromJS(message)); Singleton.Instance.onError = ((message) => CallOnError(message)); Singleton.Instance.onHttpError = ((message) => CallOnHttpError(message)); + Singleton.Instance.onAuth = ((message) => CallOnAuth(message)); _CWebViewPlugin_SetDelegate(delegateMessageReceived); #elif UNITY_ANDROID webView = new AndroidJavaObject("net.gree.unitywebview.CWebViewPluginNoUi"); @@ -258,6 +281,17 @@ public void EvaluateJS(string js) #endif } + 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 error) { if (onError != null) @@ -274,6 +308,14 @@ public void CallOnHttpError(string error) } } + public void CallOnAuth(string url) + { + if (onAuth != null) + { + onAuth(url); + } + } + public void CallFromJS(string message) { if (onJS != null) 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..d138dd93 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,30 @@ - (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 != ASAuthorizationErrorCanceled) { + [self sendUnityCallback:"CallFromAuthCallbackError" message:error.localizedDescription.UTF8String]; + } else { + [self sendUnityCallback:"CallFromAuthCallback" message: callbackURL != nil ? callbackURL.absoluteString.UTF8String : ""]; + } + }]; + + _authSession.presentationContextProvider = self; + [_authSession start]; +} + +- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session +{ + return UIApplication.sharedApplication.windows.firstObject; +} @end extern "C" { @@ -351,6 +377,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 +416,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..21193b20 100644 --- a/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/IWebBrowserClient.cs +++ b/src/Packages/Passport/Runtime/ThirdParty/ImmutableBrowserCore/IWebBrowserClient.cs @@ -3,7 +3,10 @@ namespace Immutable.Browser.Core public interface IWebBrowserClient { event OnUnityPostMessageDelegate OnUnityPostMessage; + event OnUnityPostMessageDelegate OnAuthPostMessage; void ExecuteJs(string js); + + void LaunchAuthURL(string url); } } \ 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..5c2f92ff 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,7 @@ internal void InvokeLoadProgressChange(double progress) /// Invoked when the browser goes in or out of fullscreen /// public event OnFullscreenChange OnFullscreen; + public event OnUnityPostMessageDelegate OnAuthPostMessage; internal void InvokeFullscreen(bool fullscreen) { @@ -737,6 +738,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..2f1bad10 100644 --- a/src/Packages/Passport/Tests/Runtime/Scripts/Core/BrowserCommunicationsManagerTests.cs +++ b/src/Packages/Passport/Tests/Runtime/Scripts/Core/BrowserCommunicationsManagerTests.cs @@ -152,6 +152,8 @@ public void CallAndResponse_Success_BrowserReady() internal class MockBrowserClient : IWebBrowserClient { public event OnUnityPostMessageDelegate? OnUnityPostMessage; + public event OnUnityPostMessageDelegate? OnAuthPostMessage; + public BrowserRequest? request = null; public BrowserResponse? browserResponse = null; public bool setRequestId = true; @@ -191,5 +193,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..d0e8295b 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,8 @@ internal class MockBrowserCommsManager : IBrowserCommunicationsManager public string response = ""; public string fxName = ""; public string? data = ""; + public event OnUnityPostMessageDelegate? OnAuthPostMessage; + public UniTask Call(string fxName, string? data = null, bool ignoreTimeout = false) { this.fxName = fxName; @@ -73,6 +76,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) { }