Skip to content

Commit

Permalink
Cancel detection support & other bugfixes (#43)
Browse files Browse the repository at this point in the history
Closes #20.
Closes #31.
Closes #33.
Closes #37.

Co-authored-by: Darrell Wu <[email protected]>
Co-authored-by: Stefan Erkenberg <[email protected]>
Co-authored-by: Michael Manzinger <[email protected]>
  • Loading branch information
4 people authored Jan 14, 2025
1 parent 9ce3f01 commit 90d899d
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 28 deletions.
21 changes: 21 additions & 0 deletions example/.postinstall.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Copyright 2024 Darryl Pogue
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

const fs = require('node:fs');

fs.cpSync('../www/', 'node_modules/cordova-plugin-oauth/www/', { recursive: true, force: true });
fs.cpSync('../src/', 'node_modules/cordova-plugin-oauth/src/', { recursive: true, force: true });
fs.cpSync('../plugin.xml', 'node_modules/cordova-plugin-oauth/plugin.xml', { force: true });
3 changes: 2 additions & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"cordova-plugin-oauth": ">= 4.0.0"
},
"scripts": {
"build": "cordova prepare && cordova build --debug --emulator"
"build": "cordova prepare && cordova build --debug --emulator",
"postinstall": "node .postinstall.js"
}
}
6 changes: 6 additions & 0 deletions example/www/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ <h1>Welcome!</h1>
<button id="login-button" class="login">Sign in with OAuth</button>
</div>

<div class="app" slot="login_cancelled">
<h1>Login Cancelled!</h1>

<button id="login-button" class="login">Sign in with OAuth</button>
</div>

<div class="app" slot="authenticated">
<h1>Logged In</h1>

Expand Down
26 changes: 20 additions & 6 deletions example/www/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,23 @@ const authElements = new Set();
/**
* Click handler for the login button to start the OAuth flow.
*/
document.getElementById('login-button').addEventListener('click', () => {
// Our fake OAuth redirecting page
const url = 'https://ayogohealth.github.io/cordova-plugin-oauth/example/oauth_access_token.html';

// Open a window with the "oauth:" prefix to trigger the plugin
window.open(url, 'oauth:testpage');
document.querySelectorAll('.login').forEach((btn) => {
btn.addEventListener('click', () => {
// Our fake OAuth redirecting page
const url = 'https://ayogohealth.github.io/cordova-plugin-oauth/example/oauth_access_token.html';

// Open a window with the "oauth:" prefix to trigger the plugin
const hwnd = window.open(url, 'oauth:testpage');

hwnd.addEventListener('close', (evt) => {
if (!localStorage.getItem('access_token')) {
localStorage.setItem('login_cancelled', 'true');

// Re-evaluate the authentication elements
authElements.forEach((el) => el.evaluateState());
}
});
})
});


Expand Down Expand Up @@ -80,9 +91,12 @@ class AuthRouterElement extends HTMLElement {

evaluateState() {
const access_token = localStorage.getItem('access_token');
const login_cancelled = localStorage.getItem('login_cancelled');

if (access_token && access_token != '') {
this.shadowRoot.innerHTML = '<slot name="authenticated"></slot>';
} else if (login_cancelled && login_cancelled != '') {
this.shadowRoot.innerHTML = '<slot name="login_cancelled"></slot>';
} else {
this.shadowRoot.innerHTML = '<slot name="unauthenticated"></slot>';
}
Expand Down
26 changes: 23 additions & 3 deletions src/android/OAuthPlugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

public class OAuthPlugin extends CordovaPlugin {
private final String TAG = "OAuthPlugin";
private CallbackContext oauthCallback = null;

/**
* Executes the request.
Expand All @@ -55,10 +56,10 @@ public boolean execute(String action, CordovaArgs args, CallbackContext callback
if ("startOAuth".equals(action)) {
try {
String authEndpoint = args.getString(0);
oauthCallback = callbackContext;

this.startOAuth(authEndpoint);

callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
return true;
} catch (JSONException e) {
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR));
Expand All @@ -85,12 +86,14 @@ public void onNewIntent(Intent intent) {
}

final Uri uri = intent.getData();
String callbackHost = preferences.getString("oauthhostname", "oauth_callback");

if (uri.getHost().equals("oauth_callback")) {
if (uri.getHost().equals(callbackHost)) {
LOG.i(TAG, "OAuth called back with parameters.");

try {
JSONObject jsobj = new JSONObject();
jsobj.put("oauth_callback_url", uri.toString());

for (String queryKey : uri.getQueryParameterNames()) {
jsobj.put(queryKey, uri.getQueryParameter(queryKey));
Expand All @@ -104,6 +107,22 @@ public void onNewIntent(Intent intent) {
}
}

/**
* Called when the activity will start interacting with the user.
*
* We use this method to indicate to the JavaScript side that the OAuth
* window has closed (regardless of login status).
*/
@Override
public void onResume(boolean multitasking) {
super.onResume(multitasking);

if (oauthCallback != null) {
oauthCallback.sendPluginResult(new PluginResult(PluginResult.Status.OK));
oauthCallback = null;
}
}


/**
* Launches the custom tab with the OAuth endpoint URL.
Expand All @@ -118,7 +137,8 @@ private void startOAuth(String url) {

@SuppressWarnings("deprecation")
private void dispatchOAuthMessage(final String msg) {
final String jsCode = "window.dispatchEvent(new MessageEvent('message', { data: 'oauth::" + msg + "' }));";
final String msgData = msg.replace("'", "\\'").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
final String jsCode = "window.dispatchEvent(new MessageEvent('message', { data: 'oauth::" + msgData + "' }));";

CordovaWebViewEngine engine = this.webView.getEngine();
if (engine != null) {
Expand Down
48 changes: 43 additions & 5 deletions src/ios/OAuthPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import Foundation
import AuthenticationServices
import SafariServices

extension NSNotification.Name {
static let CDVPluginOAuthCancelled = NSNotification.Name("CDVPluginOAuthCancelledNotification");
}

@objc protocol OAuthSessionProvider {
init(_ endpoint : URL, callbackScheme : String)
func start() -> Void
Expand All @@ -41,6 +45,8 @@ class ASWebAuthenticationSessionOAuthSessionProvider : OAuthSessionProvider {
self.aswas = ASWebAuthenticationSession(url: endpoint, callbackURLScheme: callbackURLScheme, completionHandler: { (callBack:URL?, error:Error?) in
if let incomingUrl = callBack {
NotificationCenter.default.post(name: NSNotification.Name.CDVPluginHandleOpenURL, object: incomingUrl)
} else {
NotificationCenter.default.post(name: NSNotification.Name.CDVPluginOAuthCancelled, object: nil)
}
})
}
Expand Down Expand Up @@ -68,6 +74,8 @@ class SFAuthenticationSessionOAuthSessionProvider : OAuthSessionProvider {
self.sfas = SFAuthenticationSession(url: endpoint, callbackURLScheme: callbackScheme, completionHandler: { (callBack:URL?, error:Error?) in
if let incomingUrl = callBack {
NotificationCenter.default.post(name: NSNotification.Name.CDVPluginHandleOpenURL, object: incomingUrl)
} else {
NotificationCenter.default.post(name: NSNotification.Name.CDVPluginOAuthCancelled, object: nil)
}
})
}
Expand All @@ -90,6 +98,9 @@ class SFSafariViewControllerOAuthSessionProvider : OAuthSessionProvider {

required init(_ endpoint : URL, callbackScheme : String) {
self.sfvc = SFSafariViewController(url: endpoint)
if #available(iOS 11.0, *) {
self.sfvc.dismissButtonStyle = .cancel
}
}

func start() {
Expand Down Expand Up @@ -127,13 +138,15 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
static var forcedVersion : UInt32 = UInt32.max

var authSystem : OAuthSessionProvider?
var closeCallbackId : String?
var callbackScheme : String?
var logger : OSLog?

override func pluginInitialize() {
let urlScheme = self.commandDelegate.settings["oauthscheme"] as! String
let urlHostname = self.commandDelegate.settings["oauthhostname"] as? String ?? "oauth_callback";

self.callbackScheme = "\(urlScheme)://oauth_callback"
self.callbackScheme = "\(urlScheme)://\(urlHostname)"
if #available(iOS 10.0, *) {
self.logger = OSLog(subsystem: urlScheme, category: "Cordova")
}
Expand All @@ -142,6 +155,11 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
selector: #selector(OAuthPlugin._handleOpenURL(_:)),
name: NSNotification.Name.CDVPluginHandleOpenURL,
object: nil)

NotificationCenter.default.addObserver(self,
selector: #selector(OAuthPlugin._handleCancel(_:)),
name: NSNotification.Name.CDVPluginOAuthCancelled,
object: nil)
}


Expand All @@ -156,6 +174,8 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
return
}

self.closeCallbackId = command.callbackId

if OAuthPlugin.forcedVersion >= 12, #available(iOS 12.0, *) {
self.authSystem = ASWebAuthenticationSessionOAuthSessionProvider(url, callbackScheme:self.callbackScheme!)

Expand All @@ -179,7 +199,6 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati

self.authSystem?.start()

self.commandDelegate.send(CDVPluginResult(status: .ok), callbackId: command.callbackId)
return
}

Expand All @@ -188,7 +207,7 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
self.authSystem?.cancel()
self.authSystem = nil

var jsobj : [String : String] = [:]
var jsobj : [String : String] = ["oauth_callback_url": url.absoluteString]
let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems

queryItems?.forEach {
Expand All @@ -204,6 +223,10 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
do {
let data = try JSONSerialization.data(withJSONObject: jsobj)
let msg = String(data: data, encoding: .utf8)!
.replacingOccurrences(of: "'", with: "\\'")
.replacingOccurrences(of: "\n", with: "\\n")
.replacingOccurrences(of: "\r", with: "\\r")
.replacingOccurrences(of: "\t", with: "\\t")

self.webViewEngine.evaluateJavaScript("window.dispatchEvent(new MessageEvent('message', { data: 'oauth::\(msg)' }));", completionHandler: nil)
} catch {
Expand All @@ -227,13 +250,28 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
}

self.parseToken(from: url)

if let cb = self.closeCallbackId {
self.commandDelegate.send(CDVPluginResult(status: .ok), callbackId: cb)
}
self.closeCallbackId = nil
}


@objc internal func _handleCancel(_ notification : NSNotification) {
if let cb = self.closeCallbackId {
self.commandDelegate.send(CDVPluginResult(status: .ok), callbackId: cb)
}
self.closeCallbackId = nil
}


@available(iOS 9.0, *)
func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
self.authSystem?.cancel()
self.authSystem = nil
self.authSystem?.cancel()
self.authSystem = nil

NotificationCenter.default.post(name: NSNotification.Name.CDVPluginOAuthCancelled, object: nil)
}

@available(iOS 13.0, *)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import androidx.test.uiautomator.UiObjectNotFoundException;
import androidx.test.uiautomator.UiObject2;
import androidx.test.uiautomator.Until;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
Expand Down Expand Up @@ -60,6 +61,16 @@ public void waitForAppLaunch() {
device.wait(Until.hasObject(By.pkg(TEST_PACKAGE).depth(0)), TIMEOUT);
}

@After
public void ensureLogout() {
UiObject2 logoutBtn = device.wait(Until.findObject(By.text("Logout")), TIMEOUT);
if (logoutBtn != null) {
logoutBtn.click();

device.waitForIdle();
}
}

@Test
public void testOAuthFlow() {
assertNotNull(device);
Expand All @@ -81,6 +92,28 @@ public void testOAuthFlow() {
device.wait(Until.findObject(By.text("LOGGED IN")), TIMEOUT);
}

@Test
public void testOAuthCancellation() {
assertNotNull(device);

device.wait(Until.findObject(By.clazz(WebView.class)), TIMEOUT);

UiObject2 loginBtn = device.wait(Until.findObject(By.text("Sign in with OAuth")), TIMEOUT);
assertNotNull(loginBtn);
loginBtn.click();

// Now we have to close the Chrome Custom Tab without logging in
UiObject2 oauthBtn = device.wait(Until.findObject(By.text("Click Here to Login")), TIMEOUT);
assertNotNull(oauthBtn);

device.pressBack();

// Should be back in the app now
device.wait(Until.findObject(By.clazz(WebView.class)), TIMEOUT);

device.wait(Until.findObject(By.text("LOGIN CANCELLED!")), TIMEOUT);
}

/**
* Uses package manager to find the package name of the device launcher. Usually this package
* is "com.android.launcher" but can be different at times. This is a generic solution which
Expand Down
Loading

0 comments on commit 90d899d

Please sign in to comment.