cordova-plugin-oauth 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,11 +38,11 @@ Installation
38
38
  ------------
39
39
 
40
40
  ```
41
- cordova plugin add cordova-plugin-oauth [--variable URL_SCHEME=mycoolapp]
41
+ cordova plugin add cordova-plugin-oauth [--variable URL_SCHEME=mycoolapp] [--variable URL_HOSTNAME=callback]
42
42
  ```
43
43
 
44
44
  By default, the plugin registers the app ID as a scheme to be used as the
45
- OAuth callback URL, and expects a host of `oauth_callback` (i.e., if your
45
+ OAuth callback URL, and expects a default host of `oauth_callback` (i.e., if your
46
46
  app's ID is `com.example.foo`, your OAuth redirect URL should be
47
47
  `com.example.foo://oauth_callback`).
48
48
 
@@ -50,6 +50,9 @@ The scheme for the OAuth callback URL can be changed by providing a
50
50
  `URL_SCHEME` variable when installing. If your `URL_SCHEME` is `mycoolapp`,
51
51
  then your OAuth redirect URL should be `mycoolapp://oauth_callback`.
52
52
 
53
+ The hostname endpoint can be changed by providing a `URL_HOSTNAME` variable
54
+ when installing.
55
+
53
56
 
54
57
  Supported Platforms
55
58
  -------------------
@@ -69,13 +72,22 @@ Usage
69
72
  window.open(endpoint, 'oauth:google', '');
70
73
  ```
71
74
 
75
+ Alternatively, you can include `oauth=yes` in the window features string:
76
+
77
+ ```javascript
78
+ var endpoint = 'https://accounts.google.com/o/oauth2/v2/auth?....';
79
+ window.open(endpoint, '_self', 'oauth=yes');
80
+ ```
81
+
72
82
  2. The plugin will open the OAuth login page in a new browser window.
73
83
 
74
84
  3. When the OAuth process is complete and it redirects to your app scheme, the
75
85
  plugin will send a message to the Cordova app.
76
86
 
77
87
  The message begins with `oauth::` and is followed by a JSON object
78
- containing all of the key/value pairs from the OAuth redirect query string.
88
+ containing all of the key/value pairs from the OAuth redirect query string
89
+ or fragment parameters. The complete OAuth callback URL is available in a
90
+ `oauth_callback_url` property.
79
91
 
80
92
  ```javascript
81
93
  // Called from a callback URL like
@@ -113,4 +125,9 @@ Licence
113
125
  Released under the Apache 2.0 Licence.
114
126
  Copyright © 2020-2022 Ayogo Health Inc.
115
127
 
128
+ This project includes the [EventTarget polyfill][etpoly], copyright © 2018,
129
+ Andrea Giammarchi under the [ISC Licence][isc].
130
+
116
131
  [coc]: https://github.com/AyogoHealth/cordova-plugin-oauth/blob/main/CODE_OF_CONDUCT.md
132
+ [etpoly]: https://github.com/ungap/event-target
133
+ [isc]: https://github.com/ungap/event-target/blob/master/LICENSE
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cordova-plugin-oauth",
3
- "version": "4.0.1",
3
+ "version": "4.1.0",
4
4
  "author": "Ayogo Health Inc. <info@ayogo.com>",
5
5
  "contributors": [
6
6
  "Darryl Pogue <darryl@dpogue.ca>",
@@ -9,10 +9,7 @@
9
9
  ],
10
10
  "description": "Cordova plugin for performing OAuth login flows.",
11
11
  "license": "Apache-2.0",
12
- "repository": {
13
- "type": "git",
14
- "url": "git+https://github.com/AyogoHealth/cordova-plugin-oauth.git"
15
- },
12
+ "repository": "github:AyogoHealth/cordova-plugin-oauth.git",
16
13
  "keywords": [
17
14
  "cordova",
18
15
  "ecosystem:cordova",
@@ -24,21 +21,44 @@
24
21
  "www",
25
22
  "plugin.xml"
26
23
  ],
24
+ "scripts": {
25
+ "version": "sed -i -e \"s/\\(<plugin [^>]*id=\\\"$npm_package_name\\\" [^>]*version=\\\"\\)[^\\\"]*\\\"/\\1$npm_package_version\\\"/\" plugin.xml && git add plugin.xml"
26
+ },
27
27
  "cordova": {
28
+ "id": "cordova-plugin-oauth",
28
29
  "platforms": [
29
30
  "ios",
30
31
  "android"
31
32
  ]
32
33
  },
33
- "engines": [
34
- {
35
- "name": "cordova-android",
36
- "version": ">= 9.0.0"
37
- },
38
- {
39
- "name": "cordova-ios",
40
- "version": ">= 6.1.0"
34
+ "engines": {
35
+ "cordovaDependencies": {
36
+ "1.0.0": {
37
+ "cordova-android": ">= 8.0.0",
38
+ "cordova-ios": ">= 5.0.0"
39
+ },
40
+ "2.0.0": {
41
+ "cordova-android": ">= 8.0.0",
42
+ "cordova-ios": ">= 5.0.0"
43
+ },
44
+ "3.0.0": {
45
+ "cordova-android": ">= 8.0.0",
46
+ "cordova-ios": ">= 6.1.0"
47
+ },
48
+ "4.0.0": {
49
+ "cordova-android": ">= 9.0.0",
50
+ "cordova-ios": ">= 6.1.0"
51
+ },
52
+ "5.0.0": {
53
+ "cordova": ">100"
54
+ }
41
55
  }
42
- ],
43
- "dependencies": {}
56
+ },
57
+ "dependencies": {},
58
+ "devEngines": {
59
+ "packageManager": {
60
+ "name": "npm",
61
+ "onFail": "ignore"
62
+ }
63
+ }
44
64
  }
package/plugin.xml CHANGED
@@ -14,7 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  See the License for the specific language governing permissions and
15
15
  limitations under the License.
16
16
  -->
17
- <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-oauth" version="4.0.1">
17
+ <plugin xmlns="http://apache.org/cordova/ns/plugins/1.0" xmlns:android="http://schemas.android.com/apk/res/android" id="cordova-plugin-oauth" version="4.1.0">
18
18
  <name>cordova-plugin-oauth</name>
19
19
  <description>Cordova plugin for performing OAuth login flows.</description>
20
20
  <keywords>cordova,ios,android,oauth</keywords>
@@ -23,6 +23,7 @@ limitations under the License.
23
23
  <issues>https://github.com/AyogoHealth/cordova-plugin-oauth/issues</issues>
24
24
 
25
25
  <preference name="URL_SCHEME" default="$PACKAGE_NAME" />
26
+ <preference name="URL_HOSTNAME" default="oauth_callback" />
26
27
 
27
28
  <engines>
28
29
  <engine name="cordova-ios" version=">= 6.1.0" />
@@ -35,6 +36,7 @@ limitations under the License.
35
36
 
36
37
  <config-file target="config.xml" parent="/*">
37
38
  <preference name="OAuthScheme" value="$URL_SCHEME"/>
39
+ <preference name="OAuthHostname" value="$URL_HOSTNAME"/>
38
40
  </config-file>
39
41
 
40
42
  <platform name="ios">
@@ -66,6 +68,7 @@ limitations under the License.
66
68
  <config-file target="res/xml/config.xml" parent="/*">
67
69
  <feature name="OAuth">
68
70
  <param name="android-package" value="com.ayogo.cordova.oauth.OAuthPlugin" />
71
+ <param name="onload" value="true" />
69
72
  </feature>
70
73
  </config-file>
71
74
 
@@ -76,7 +79,7 @@ limitations under the License.
76
79
  <category android:name="android.intent.category.DEFAULT" />
77
80
  <category android:name="android.intent.category.BROWSABLE" />
78
81
 
79
- <data android:scheme="$URL_SCHEME" android:host="oauth_callback" />
82
+ <data android:scheme="$URL_SCHEME" android:host="$URL_HOSTNAME" />
80
83
  </intent-filter>
81
84
  </config-file>
82
85
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2019 Ayogo Health Inc.
2
+ * Copyright 2019 - 2022 Ayogo Health Inc.
3
3
  *
4
4
  * Licensed under the Apache License, Version 2.0 (the "License");
5
5
  * you may not use this file except in compliance with the License.
@@ -16,20 +16,17 @@
16
16
 
17
17
  package com.ayogo.cordova.oauth;
18
18
 
19
- import android.app.Activity;
20
- import android.content.Context;
21
19
  import android.content.Intent;
22
- import android.content.IntentFilter;
23
- import android.content.pm.PackageManager;
24
- import android.content.pm.ResolveInfo;
25
20
  import android.net.Uri;
26
21
  import androidx.browser.customtabs.CustomTabsIntent;
27
22
  import android.text.TextUtils;
23
+ import java.net.URLDecoder;
28
24
 
29
25
  import java.util.ArrayList;
30
26
  import java.util.List;
31
27
 
32
28
  import org.apache.cordova.CallbackContext;
29
+ import org.apache.cordova.CordovaArgs;
33
30
  import org.apache.cordova.CordovaInterface;
34
31
  import org.apache.cordova.CordovaPlugin;
35
32
  import org.apache.cordova.CordovaWebView;
@@ -37,29 +34,15 @@ import org.apache.cordova.CordovaWebViewEngine;
37
34
  import org.apache.cordova.LOG;
38
35
  import org.apache.cordova.PluginResult;
39
36
 
40
- import org.json.JSONArray;
41
37
  import org.json.JSONException;
42
38
  import org.json.JSONObject;
43
39
 
44
40
 
45
41
  public class OAuthPlugin extends CordovaPlugin {
46
42
  private final String TAG = "OAuthPlugin";
47
-
48
- // Taken from Google's CustomTabsHelper
49
- // https://github.com/GoogleChrome/custom-tabs-client/blob/da65829d7d4b80c00809c6c4aa7f61f88fc7ca26/shared/src/main/java/org/chromium/customtabsclient/shared/CustomTabsHelper.java
50
- static final String STABLE_PACKAGE = "com.android.chrome";
51
- static final String BETA_PACKAGE = "com.chrome.beta";
52
- static final String DEV_PACKAGE = "com.chrome.dev";
53
- static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
54
- private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE";
55
- private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
56
-
57
-
58
- /**
59
- * The name of the package to use for the custom tab service.
60
- */
61
- private String tabProvider = null;
62
-
43
+ private CallbackContext oauthCallback = null;
44
+ private boolean didFinishLoading = false;
45
+ private String lastOAuthResult = null;
63
46
 
64
47
  /**
65
48
  * Executes the request.
@@ -76,14 +59,14 @@ public class OAuthPlugin extends CordovaPlugin {
76
59
  * @return Whether the action was valid.
77
60
  */
78
61
  @Override
79
- public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
62
+ public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) {
80
63
  if ("startOAuth".equals(action)) {
81
64
  try {
82
65
  String authEndpoint = args.getString(0);
66
+ oauthCallback = callbackContext;
83
67
 
84
68
  this.startOAuth(authEndpoint);
85
69
 
86
- callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
87
70
  return true;
88
71
  } catch (JSONException e) {
89
72
  callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR));
@@ -96,6 +79,14 @@ public class OAuthPlugin extends CordovaPlugin {
96
79
  }
97
80
 
98
81
 
82
+ /**
83
+ * Called when the activity is becoming visible to the user.
84
+ */
85
+ @Override
86
+ public void onStart() {
87
+ onNewIntent(cordova.getActivity().getIntent());
88
+ }
89
+
99
90
 
100
91
  /**
101
92
  * Called when the activity receives a new intent.
@@ -110,26 +101,40 @@ public class OAuthPlugin extends CordovaPlugin {
110
101
  }
111
102
 
112
103
  final Uri uri = intent.getData();
104
+ String callbackHost = preferences.getString("oauthhostname", "oauth_callback");
113
105
 
114
- if (uri.getHost().equals("oauth_callback")) {
106
+ if (uri.getHost().equals(callbackHost)) {
115
107
  LOG.i(TAG, "OAuth called back with parameters.");
116
108
 
117
109
  try {
118
110
  JSONObject jsobj = new JSONObject();
111
+ jsobj.put("oauth_callback_url", uri.toString());
112
+
113
+ // Parse fragment parameters
114
+ if (uri.getFragment() != null) {
115
+ String fragment = uri.getFragment();
116
+ String[] pairs = fragment.split("&");
117
+ for (String pair : pairs) {
118
+ String[] keyValue = pair.split("=");
119
+ if (keyValue.length == 2) {
120
+ // Decode the fragment parameter before adding it to the JSONObject
121
+ String key = keyValue[0];
122
+ String value = keyValue[1];
123
+ jsobj.put(key, value);
124
+ }
125
+ }
126
+ }
119
127
 
128
+ // Parse query parameters
120
129
  for (String queryKey : uri.getQueryParameterNames()) {
121
130
  jsobj.put(queryKey, uri.getQueryParameter(queryKey));
122
131
  }
123
132
 
124
- final String msg = jsobj.toString();
125
- CordovaWebViewEngine engine = this.webView.getEngine();
126
- final String jsCode = "window.dispatchEvent(new MessageEvent('message', { data: 'oauth::" + msg + "' }));";
127
- if (engine != null) {
128
- engine.evaluateJavascript(jsCode, null);
133
+ if (this.didFinishLoading) {
134
+ dispatchOAuthMessage(jsobj.toString());
129
135
  } else {
130
- this.webView.sendJavascript(jsCode);
136
+ this.lastOAuthResult = jsobj.toString();
131
137
  }
132
-
133
138
  } catch (JSONException e) {
134
139
  LOG.e(TAG, "JSON Serialization failed");
135
140
  e.printStackTrace();
@@ -137,119 +142,76 @@ public class OAuthPlugin extends CordovaPlugin {
137
142
  }
138
143
  }
139
144
 
140
-
141
145
  /**
142
- * Launches the custom tab with the OAuth endpoint URL.
146
+ * Called when the activity will start interacting with the user.
143
147
  *
144
- * @param url The URL of the OAuth endpoint.
148
+ * We use this method to indicate to the JavaScript side that the OAuth
149
+ * window has closed (regardless of login status).
145
150
  */
146
- private void startOAuth(String url) {
147
- String customTabsBrowser = findCustomTabProvider();
148
-
149
- CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
150
- CustomTabsIntent customTabsIntent = builder.build();
151
+ @Override
152
+ public void onResume(boolean multitasking) {
153
+ super.onResume(multitasking);
151
154
 
152
- String packageName = this.findCustomTabProvider();
153
- if (packageName != null) {
154
- customTabsIntent.intent.setPackage(packageName);
155
+ if (oauthCallback != null) {
156
+ oauthCallback.sendPluginResult(new PluginResult(PluginResult.Status.OK));
157
+ oauthCallback = null;
155
158
  }
156
-
157
- customTabsIntent.launchUrl(this.cordova.getActivity(), Uri.parse(url));
158
159
  }
159
160
 
160
161
 
161
162
  /**
162
- * Goes through all apps that handle VIEW intents and have a warmup service.
163
+ * Called when a message is sent to plugin.
163
164
  *
164
- * Picks the one chosen by the user if there is one, otherwise makes a best
165
- * effort to return a valid package name.
166
- *
167
- * This is <strong>not</strong> threadsafe.
168
- *
169
- * @return The package name recommended to use for connecting to custom
170
- * tabs related components.
165
+ * @param id The message id
166
+ * @param data The message data
167
+ * @return Object to stop propagation or null
171
168
  */
172
- private String findCustomTabProvider() {
173
- if (this.tabProvider != null) {
174
- return this.tabProvider;
175
- }
176
-
177
- PackageManager pm = this.cordova.getActivity().getPackageManager();
178
-
179
- // Get default VIEW intent handler.
180
- Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
181
- ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
182
- String defaultViewHandlerPackageName = null;
183
-
184
- if (defaultViewHandlerInfo != null) {
185
- defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
186
- }
187
-
188
-
189
- // Get all apps that can handle VIEW intents.
190
- List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, PackageManager.MATCH_ALL);
191
- List<String> packagesSupportingCustomTabs = new ArrayList<>();
192
-
193
- for (ResolveInfo info : resolvedActivityList) {
194
- Intent serviceIntent = new Intent();
195
- serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
196
- serviceIntent.setPackage(info.activityInfo.packageName);
169
+ @Override
170
+ public Object onMessage(String id, Object data) {
171
+ if (id.equals("onPageFinished")) {
172
+ this.didFinishLoading = true;
197
173
 
198
- if (pm.resolveService(serviceIntent, 0) != null) {
199
- packagesSupportingCustomTabs.add(info.activityInfo.packageName);
174
+ if (this.lastOAuthResult != null) {
175
+ this.dispatchOAuthMessage(this.lastOAuthResult);
176
+ this.lastOAuthResult = null;
200
177
  }
201
178
  }
202
-
203
- // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
204
- // and service calls.
205
- if (packagesSupportingCustomTabs.isEmpty()) {
206
- this.tabProvider = null;
207
- } else if (packagesSupportingCustomTabs.size() == 1) {
208
- this.tabProvider = packagesSupportingCustomTabs.get(0);
209
- } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) && !this.hasSpecializedHandlerIntents(activityIntent) && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
210
- this.tabProvider = defaultViewHandlerPackageName;
211
- } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
212
- this.tabProvider = STABLE_PACKAGE;
213
- } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
214
- this.tabProvider = BETA_PACKAGE;
215
- } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
216
- this.tabProvider = DEV_PACKAGE;
217
- } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
218
- this.tabProvider = LOCAL_PACKAGE;
219
- }
220
-
221
- return this.tabProvider;
179
+ return null;
222
180
  }
223
181
 
224
182
 
225
183
  /**
226
- * Used to check whether there is a specialized handler for a given intent.
184
+ * Launches the custom tab with the OAuth endpoint URL.
227
185
  *
228
- * @param intent The intent to check with.
229
- * @return Whether there is a specialized handler for the given intent.
186
+ * @param url The URL of the OAuth endpoint.
230
187
  */
231
- private boolean hasSpecializedHandlerIntents(Intent intent) {
232
- try {
233
- PackageManager pm = this.cordova.getActivity().getPackageManager();
234
- List<ResolveInfo> handlers = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
188
+ private void startOAuth(String url) {
189
+ CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
235
190
 
236
- if (handlers == null || handlers.size() == 0) {
237
- return false;
238
- }
191
+ CustomTabsIntent customTabsIntent = builder.build();
192
+ customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
193
+ customTabsIntent.intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
194
+ customTabsIntent.intent.putExtra("android.support.customtabs.extra.ENABLE_URLBAR_HIDING", true);
195
+ customTabsIntent.intent.putExtra("android.support.customtabs.extra.EXTRA_ENABLE_INSTANT_APPS", false);
196
+ customTabsIntent.intent.putExtra("android.support.customtabs.extra.SEND_TO_EXTERNAL_HANDLER", false);
197
+ customTabsIntent.intent.putExtra("androidx.browser.customtabs.extra.SHARE_STATE", 2);
198
+ customTabsIntent.intent.putExtra("androidx.browser.customtabs.extra.DISABLE_BACKGROUND_INTERACTION", false);
199
+ customTabsIntent.intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_DOWNLOAD_BUTTON", true);
200
+ customTabsIntent.intent.putExtra("org.chromium.chrome.browser.customtabs.EXTRA_DISABLE_STAR_BUTTON", true);
239
201
 
240
- for (ResolveInfo resolveInfo : handlers) {
241
- IntentFilter filter = resolveInfo.filter;
202
+ customTabsIntent.launchUrl(this.cordova.getActivity(), Uri.parse(url));
203
+ }
242
204
 
243
- if (filter == null || filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0 || resolveInfo.activityInfo == null) {
244
- continue;
245
- }
205
+ @SuppressWarnings("deprecation")
206
+ private void dispatchOAuthMessage(final String msg) {
207
+ final String msgData = msg.replace("'", "\\'").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
208
+ final String jsCode = "window.dispatchEvent(new MessageEvent('message', { data: 'oauth::" + msgData + "' }));";
246
209
 
247
- return true;
248
- }
249
- } catch (RuntimeException e) {
250
- LOG.e(TAG, "Runtime exception while getting specialized handlers");
210
+ CordovaWebViewEngine engine = this.webView.getEngine();
211
+ if (engine != null) {
212
+ engine.evaluateJavascript(jsCode, null);
213
+ } else {
214
+ this.webView.sendJavascript(jsCode);
251
215
  }
252
-
253
- return false;
254
216
  }
255
217
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright 2019 Ayogo Health Inc.
2
+ * Copyright 2019 - 2022 Ayogo Health Inc.
3
3
  *
4
4
  * Licensed under the Apache License, Version 2.0 (the "License");
5
5
  * you may not use this file except in compliance with the License.
@@ -14,11 +14,19 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
+ #if canImport(Cordova)
18
+ import Cordova
19
+ #endif
20
+
17
21
  import os.log
18
22
  import Foundation
19
23
  import AuthenticationServices
20
24
  import SafariServices
21
25
 
26
+ extension NSNotification.Name {
27
+ static let CDVPluginOAuthCancelled = NSNotification.Name("CDVPluginOAuthCancelledNotification");
28
+ }
29
+
22
30
  @objc protocol OAuthSessionProvider {
23
31
  init(_ endpoint : URL, callbackScheme : String)
24
32
  func start() -> Void
@@ -37,6 +45,8 @@ class ASWebAuthenticationSessionOAuthSessionProvider : OAuthSessionProvider {
37
45
  self.aswas = ASWebAuthenticationSession(url: endpoint, callbackURLScheme: callbackURLScheme, completionHandler: { (callBack:URL?, error:Error?) in
38
46
  if let incomingUrl = callBack {
39
47
  NotificationCenter.default.post(name: NSNotification.Name.CDVPluginHandleOpenURL, object: incomingUrl)
48
+ } else {
49
+ NotificationCenter.default.post(name: NSNotification.Name.CDVPluginOAuthCancelled, object: nil)
40
50
  }
41
51
  })
42
52
  }
@@ -64,6 +74,8 @@ class SFAuthenticationSessionOAuthSessionProvider : OAuthSessionProvider {
64
74
  self.sfas = SFAuthenticationSession(url: endpoint, callbackURLScheme: callbackScheme, completionHandler: { (callBack:URL?, error:Error?) in
65
75
  if let incomingUrl = callBack {
66
76
  NotificationCenter.default.post(name: NSNotification.Name.CDVPluginHandleOpenURL, object: incomingUrl)
77
+ } else {
78
+ NotificationCenter.default.post(name: NSNotification.Name.CDVPluginOAuthCancelled, object: nil)
67
79
  }
68
80
  })
69
81
  }
@@ -86,6 +98,9 @@ class SFSafariViewControllerOAuthSessionProvider : OAuthSessionProvider {
86
98
 
87
99
  required init(_ endpoint : URL, callbackScheme : String) {
88
100
  self.sfvc = SFSafariViewController(url: endpoint)
101
+ if #available(iOS 11.0, *) {
102
+ self.sfvc.dismissButtonStyle = .cancel
103
+ }
89
104
  }
90
105
 
91
106
  func start() {
@@ -119,14 +134,19 @@ class SafariAppOAuthSessionProvider : OAuthSessionProvider {
119
134
 
120
135
  @objc(CDVOAuthPlugin)
121
136
  class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticationPresentationContextProviding {
137
+ /** This exists for testing purposes */
138
+ static var forcedVersion : UInt32 = UInt32.max
139
+
122
140
  var authSystem : OAuthSessionProvider?
141
+ var closeCallbackId : String?
123
142
  var callbackScheme : String?
124
143
  var logger : OSLog?
125
144
 
126
145
  override func pluginInitialize() {
127
146
  let urlScheme = self.commandDelegate.settings["oauthscheme"] as! String
147
+ let urlHostname = self.commandDelegate.settings["oauthhostname"] as? String ?? "oauth_callback";
128
148
 
129
- self.callbackScheme = "\(urlScheme)://oauth_callback"
149
+ self.callbackScheme = "\(urlScheme)://\(urlHostname)"
130
150
  if #available(iOS 10.0, *) {
131
151
  self.logger = OSLog(subsystem: urlScheme, category: "Cordova")
132
152
  }
@@ -135,6 +155,11 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
135
155
  selector: #selector(OAuthPlugin._handleOpenURL(_:)),
136
156
  name: NSNotification.Name.CDVPluginHandleOpenURL,
137
157
  object: nil)
158
+
159
+ NotificationCenter.default.addObserver(self,
160
+ selector: #selector(OAuthPlugin._handleCancel(_:)),
161
+ name: NSNotification.Name.CDVPluginOAuthCancelled,
162
+ object: nil)
138
163
  }
139
164
 
140
165
 
@@ -149,7 +174,9 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
149
174
  return
150
175
  }
151
176
 
152
- if #available(iOS 12.0, *) {
177
+ self.closeCallbackId = command.callbackId
178
+
179
+ if OAuthPlugin.forcedVersion >= 12, #available(iOS 12.0, *) {
153
180
  self.authSystem = ASWebAuthenticationSessionOAuthSessionProvider(url, callbackScheme:self.callbackScheme!)
154
181
 
155
182
  if #available(iOS 13.0, *) {
@@ -157,9 +184,9 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
157
184
  aswas.delegate = self
158
185
  }
159
186
  }
160
- } else if #available(iOS 11.0, *) {
187
+ } else if OAuthPlugin.forcedVersion >= 11, #available(iOS 11.0, *) {
161
188
  self.authSystem = SFAuthenticationSessionOAuthSessionProvider(url, callbackScheme:self.callbackScheme!)
162
- } else if #available(iOS 9.0, *) {
189
+ } else if OAuthPlugin.forcedVersion >= 9, #available(iOS 9.0, *) {
163
190
  self.authSystem = SFSafariViewControllerOAuthSessionProvider(url, callbackScheme:self.callbackScheme!)
164
191
 
165
192
  if let sfvc = self.authSystem as? SFSafariViewControllerOAuthSessionProvider {
@@ -172,7 +199,6 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
172
199
 
173
200
  self.authSystem?.start()
174
201
 
175
- self.commandDelegate.send(CDVPluginResult(status: .ok), callbackId: command.callbackId)
176
202
  return
177
203
  }
178
204
 
@@ -181,24 +207,38 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
181
207
  self.authSystem?.cancel()
182
208
  self.authSystem = nil
183
209
 
184
- var jsobj : [String : String] = [:]
210
+ var jsobj : [String : String] = ["oauth_callback_url": url.absoluteString]
185
211
  let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems
186
-
187
- queryItems?.forEach {
188
- jsobj[$0.name] = $0.value
212
+ let fragment = url.fragment
213
+
214
+ // Parse fragment parameters
215
+ if let fragment = fragment {
216
+ let pairs = fragment.split(separator: "&")
217
+ for pair in pairs {
218
+ let keyValue = pair.split(separator: "=")
219
+ if keyValue.count == 2,
220
+ let key = keyValue[0].removingPercentEncoding,
221
+ let value = keyValue[1].removingPercentEncoding {
222
+ jsobj[String(key)] = String(value)
223
+ }
224
+ }
189
225
  }
190
226
 
191
- if #available(iOS 10.0, *) {
192
- os_log("OAuth called back with parameters.", log: self.logger!, type: .info)
193
- } else {
194
- NSLog("OAuth called back with parameters.")
227
+ queryItems?.forEach {
228
+ jsobj[$0.name] = $0.value
195
229
  }
196
230
 
197
231
  do {
198
232
  let data = try JSONSerialization.data(withJSONObject: jsobj)
199
- let msg = String(data: data, encoding: .utf8)!
200
233
 
201
- self.webViewEngine.evaluateJavaScript("window.dispatchEvent(new MessageEvent('message', { data: 'oauth::\(msg)' }));", completionHandler: nil)
234
+ if let msg = String(data: data, encoding: .utf8) {
235
+ let jsMsg = msg.replacingOccurrences(of: "'", with: "\\'")
236
+ .replacingOccurrences(of: "\n", with: "\\n")
237
+ .replacingOccurrences(of: "\r", with: "\\r")
238
+ .replacingOccurrences(of: "\t", with: "\\t")
239
+
240
+ self.webViewEngine.evaluateJavaScript("window.dispatchEvent(new MessageEvent('message', { data: 'oauth::\(jsMsg)' }));", completionHandler: nil)
241
+ }
202
242
  } catch {
203
243
  let errStr = "JSON Serialization failed: \(error)"
204
244
  if #available(iOS 10.0, *) {
@@ -220,13 +260,28 @@ class OAuthPlugin : CDVPlugin, SFSafariViewControllerDelegate, ASWebAuthenticati
220
260
  }
221
261
 
222
262
  self.parseToken(from: url)
263
+
264
+ if let cb = self.closeCallbackId {
265
+ self.commandDelegate.send(CDVPluginResult(status: .ok), callbackId: cb)
266
+ }
267
+ self.closeCallbackId = nil
268
+ }
269
+
270
+
271
+ @objc internal func _handleCancel(_ notification : NSNotification) {
272
+ if let cb = self.closeCallbackId {
273
+ self.commandDelegate.send(CDVPluginResult(status: .ok), callbackId: cb)
274
+ }
275
+ self.closeCallbackId = nil
223
276
  }
224
277
 
225
278
 
226
279
  @available(iOS 9.0, *)
227
280
  func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
228
- self.authSystem?.cancel()
229
- self.authSystem = nil
281
+ self.authSystem?.cancel()
282
+ self.authSystem = nil
283
+
284
+ NotificationCenter.default.post(name: NSNotification.Name.CDVPluginOAuthCancelled, object: nil)
230
285
  }
231
286
 
232
287
  @available(iOS 13.0, *)
package/www/oauth.js CHANGED
@@ -18,10 +18,115 @@ var exec = require('cordova/exec');
18
18
  var modulemapper = require('cordova/modulemapper');
19
19
  var noop = function() { };
20
20
 
21
+ // https://github.com/ungap/event-target
22
+ /**
23
+ * Copyright (c) 2018, Andrea Giammarchi, @WebReflection
24
+ *
25
+ * Permission to use, copy, modify, and/or distribute this software for any
26
+ * purpose with or without fee is hereby granted, provided that the above
27
+ * copyright notice and this permission notice appear in all copies.
28
+ *
29
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
30
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
31
+ * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
32
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
33
+ * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
34
+ * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
35
+ * PERFORMANCE OF THIS SOFTWARE.
36
+ */
37
+ var EventTargetPolyfill = (function(Object, wm) {
38
+ var create = Object.create;
39
+ var defineProperty = Object.defineProperty;
40
+ var proto = EventTarget.prototype;
41
+ define(proto, 'addEventListener', function (type, listener, options) {
42
+ for (var
43
+ secret = wm.get(this),
44
+ listeners = secret[type] || (secret[type] = []),
45
+ i = 0, length = listeners.length; i < length; i++
46
+ ) {
47
+ if (listeners[i].listener === listener)
48
+ return;
49
+ }
50
+ listeners.push({target: this, listener: listener, options: options});
51
+ });
52
+ define(proto, 'dispatchEvent', function (event) {
53
+ var secret = wm.get(this);
54
+ var listeners = secret[event.type];
55
+ if (listeners) {
56
+ define(event, 'target', this);
57
+ define(event, 'currentTarget', this);
58
+ listeners.slice(0).some(dispatch, event);
59
+ delete event.currentTarget;
60
+ delete event.target;
61
+ }
62
+ return true;
63
+ });
64
+ define(proto, 'removeEventListener', function (type, listener) {
65
+ for (var
66
+ secret = wm.get(this),
67
+ /* istanbul ignore next */
68
+ listeners = secret[type] || (secret[type] = []),
69
+ i = 0, length = listeners.length; i < length; i++
70
+ ) {
71
+ if (listeners[i].listener === listener) {
72
+ listeners.splice(i, 1);
73
+ return;
74
+ }
75
+ }
76
+ });
77
+ return EventTarget;
78
+
79
+ function EventTarget() {'use strict';
80
+ wm.set(this, create(null));
81
+ }
82
+ function define(target, name, value) {
83
+ defineProperty(
84
+ target,
85
+ name,
86
+ {
87
+ configurable: true,
88
+ writable: true,
89
+ value: value
90
+ }
91
+ );
92
+ }
93
+ function dispatch(info) {
94
+ var options = info.options;
95
+ if (options && options.once)
96
+ info.target.removeEventListener(this.type, info.listener);
97
+ if (typeof info.listener === 'function')
98
+ info.listener.call(info.target, this);
99
+ else
100
+ info.listener.handleEvent(this);
101
+ return this._stopImmediatePropagationFlag;
102
+ }
103
+ })(Object, new WeakMap());
104
+
21
105
 
22
106
  module.exports = function(url, name, features) {
23
- if (name && name.match && name.match(/^oauth:/)) {
24
- cordova.exec(noop, noop, 'OAuth', 'startOAuth', [url]);
107
+ var nameMatch = name && name.match && name.match(/^oauth:/);
108
+ var featureMatch = features && features.match && features.match(/^(?:.+,)?(oauth)(?:[=,].*)?$/i);
109
+
110
+ if (nameMatch || featureMatch) {
111
+ var wnd = null;
112
+ if (window.EventTarget) {
113
+ wnd = new EventTarget();
114
+ } else {
115
+ wnd = new EventTargetPolyfill();
116
+ }
117
+
118
+ function success() {
119
+ if (wnd) {
120
+ if (wnd.onclose) {
121
+ wnd.onclose();
122
+ }
123
+ wnd.dispatchEvent(new Event('close'));
124
+ }
125
+ }
126
+
127
+ cordova.exec(success, noop, 'OAuth', 'startOAuth', [url]);
128
+
129
+ return wnd;
25
130
  } else {
26
131
  var originalWindowOpen = modulemapper.getOriginalSymbol(window, 'open');
27
132
  return originalWindowOpen.apply(window, arguments);