cordova-plugin-oauth 4.0.0 → 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.0",
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,31 +9,56 @@
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",
19
16
  "cordova-android",
20
17
  "cordova-ios"
21
18
  ],
19
+ "files": [
20
+ "src",
21
+ "www",
22
+ "plugin.xml"
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
+ },
22
27
  "cordova": {
28
+ "id": "cordova-plugin-oauth",
23
29
  "platforms": [
24
30
  "ios",
25
31
  "android"
26
32
  ]
27
33
  },
28
- "engines": [
29
- {
30
- "name": "cordova-android",
31
- "version": ">= 9.0.0"
32
- },
33
- {
34
- "name": "cordova-ios",
35
- "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
+ }
36
55
  }
37
- ],
38
- "dependencies": {}
56
+ },
57
+ "dependencies": {},
58
+ "devEngines": {
59
+ "packageManager": {
60
+ "name": "npm",
61
+ "onFail": "ignore"
62
+ }
63
+ }
39
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.0">
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,49 +16,33 @@
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;
33
+ import org.apache.cordova.CordovaWebViewEngine;
36
34
  import org.apache.cordova.LOG;
37
35
  import org.apache.cordova.PluginResult;
38
36
 
39
- import org.json.JSONArray;
40
37
  import org.json.JSONException;
41
38
  import org.json.JSONObject;
42
39
 
43
40
 
44
41
  public class OAuthPlugin extends CordovaPlugin {
45
42
  private final String TAG = "OAuthPlugin";
46
-
47
- // Taken from Google's CustomTabsHelper
48
- // https://github.com/GoogleChrome/custom-tabs-client/blob/da65829d7d4b80c00809c6c4aa7f61f88fc7ca26/shared/src/main/java/org/chromium/customtabsclient/shared/CustomTabsHelper.java
49
- static final String STABLE_PACKAGE = "com.android.chrome";
50
- static final String BETA_PACKAGE = "com.chrome.beta";
51
- static final String DEV_PACKAGE = "com.chrome.dev";
52
- static final String LOCAL_PACKAGE = "com.google.android.apps.chrome";
53
- private static final String EXTRA_CUSTOM_TABS_KEEP_ALIVE = "android.support.customtabs.extra.KEEP_ALIVE";
54
- private static final String ACTION_CUSTOM_TABS_CONNECTION = "android.support.customtabs.action.CustomTabsService";
55
-
56
-
57
- /**
58
- * The name of the package to use for the custom tab service.
59
- */
60
- private String tabProvider = null;
61
-
43
+ private CallbackContext oauthCallback = null;
44
+ private boolean didFinishLoading = false;
45
+ private String lastOAuthResult = null;
62
46
 
63
47
  /**
64
48
  * Executes the request.
@@ -75,14 +59,14 @@ public class OAuthPlugin extends CordovaPlugin {
75
59
  * @return Whether the action was valid.
76
60
  */
77
61
  @Override
78
- public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
62
+ public boolean execute(String action, CordovaArgs args, CallbackContext callbackContext) {
79
63
  if ("startOAuth".equals(action)) {
80
64
  try {
81
65
  String authEndpoint = args.getString(0);
66
+ oauthCallback = callbackContext;
82
67
 
83
68
  this.startOAuth(authEndpoint);
84
69
 
85
- callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK));
86
70
  return true;
87
71
  } catch (JSONException e) {
88
72
  callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.ERROR));
@@ -95,6 +79,14 @@ public class OAuthPlugin extends CordovaPlugin {
95
79
  }
96
80
 
97
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
+
98
90
 
99
91
  /**
100
92
  * Called when the activity receives a new intent.
@@ -109,19 +101,40 @@ public class OAuthPlugin extends CordovaPlugin {
109
101
  }
110
102
 
111
103
  final Uri uri = intent.getData();
104
+ String callbackHost = preferences.getString("oauthhostname", "oauth_callback");
112
105
 
113
- if (uri.getHost().equals("oauth_callback")) {
106
+ if (uri.getHost().equals(callbackHost)) {
114
107
  LOG.i(TAG, "OAuth called back with parameters.");
115
108
 
116
109
  try {
117
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
+ }
118
127
 
128
+ // Parse query parameters
119
129
  for (String queryKey : uri.getQueryParameterNames()) {
120
130
  jsobj.put(queryKey, uri.getQueryParameter(queryKey));
121
131
  }
122
132
 
123
- final String msg = jsobj.toString();
124
- this.webView.getEngine().evaluateJavascript("window.dispatchEvent(new MessageEvent('message', { data: 'oauth::" + msg + "' }));", null);
133
+ if (this.didFinishLoading) {
134
+ dispatchOAuthMessage(jsobj.toString());
135
+ } else {
136
+ this.lastOAuthResult = jsobj.toString();
137
+ }
125
138
  } catch (JSONException e) {
126
139
  LOG.e(TAG, "JSON Serialization failed");
127
140
  e.printStackTrace();
@@ -129,119 +142,76 @@ public class OAuthPlugin extends CordovaPlugin {
129
142
  }
130
143
  }
131
144
 
132
-
133
145
  /**
134
- * Launches the custom tab with the OAuth endpoint URL.
146
+ * Called when the activity will start interacting with the user.
135
147
  *
136
- * @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).
137
150
  */
138
- private void startOAuth(String url) {
139
- String customTabsBrowser = findCustomTabProvider();
140
-
141
- CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
142
- CustomTabsIntent customTabsIntent = builder.build();
151
+ @Override
152
+ public void onResume(boolean multitasking) {
153
+ super.onResume(multitasking);
143
154
 
144
- String packageName = this.findCustomTabProvider();
145
- if (packageName != null) {
146
- customTabsIntent.intent.setPackage(packageName);
155
+ if (oauthCallback != null) {
156
+ oauthCallback.sendPluginResult(new PluginResult(PluginResult.Status.OK));
157
+ oauthCallback = null;
147
158
  }
148
-
149
- customTabsIntent.launchUrl(this.cordova.getActivity(), Uri.parse(url));
150
159
  }
151
160
 
152
161
 
153
162
  /**
154
- * Goes through all apps that handle VIEW intents and have a warmup service.
163
+ * Called when a message is sent to plugin.
155
164
  *
156
- * Picks the one chosen by the user if there is one, otherwise makes a best
157
- * effort to return a valid package name.
158
- *
159
- * This is <strong>not</strong> threadsafe.
160
- *
161
- * @return The package name recommended to use for connecting to custom
162
- * tabs related components.
165
+ * @param id The message id
166
+ * @param data The message data
167
+ * @return Object to stop propagation or null
163
168
  */
164
- private String findCustomTabProvider() {
165
- if (this.tabProvider != null) {
166
- return this.tabProvider;
167
- }
168
-
169
- PackageManager pm = this.cordova.getActivity().getPackageManager();
170
-
171
- // Get default VIEW intent handler.
172
- Intent activityIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.example.com"));
173
- ResolveInfo defaultViewHandlerInfo = pm.resolveActivity(activityIntent, 0);
174
- String defaultViewHandlerPackageName = null;
175
-
176
- if (defaultViewHandlerInfo != null) {
177
- defaultViewHandlerPackageName = defaultViewHandlerInfo.activityInfo.packageName;
178
- }
179
-
180
-
181
- // Get all apps that can handle VIEW intents.
182
- List<ResolveInfo> resolvedActivityList = pm.queryIntentActivities(activityIntent, PackageManager.MATCH_ALL);
183
- List<String> packagesSupportingCustomTabs = new ArrayList<>();
184
-
185
- for (ResolveInfo info : resolvedActivityList) {
186
- Intent serviceIntent = new Intent();
187
- serviceIntent.setAction(ACTION_CUSTOM_TABS_CONNECTION);
188
- serviceIntent.setPackage(info.activityInfo.packageName);
169
+ @Override
170
+ public Object onMessage(String id, Object data) {
171
+ if (id.equals("onPageFinished")) {
172
+ this.didFinishLoading = true;
189
173
 
190
- if (pm.resolveService(serviceIntent, 0) != null) {
191
- packagesSupportingCustomTabs.add(info.activityInfo.packageName);
174
+ if (this.lastOAuthResult != null) {
175
+ this.dispatchOAuthMessage(this.lastOAuthResult);
176
+ this.lastOAuthResult = null;
192
177
  }
193
178
  }
194
-
195
- // Now packagesSupportingCustomTabs contains all apps that can handle both VIEW intents
196
- // and service calls.
197
- if (packagesSupportingCustomTabs.isEmpty()) {
198
- this.tabProvider = null;
199
- } else if (packagesSupportingCustomTabs.size() == 1) {
200
- this.tabProvider = packagesSupportingCustomTabs.get(0);
201
- } else if (!TextUtils.isEmpty(defaultViewHandlerPackageName) && !this.hasSpecializedHandlerIntents(activityIntent) && packagesSupportingCustomTabs.contains(defaultViewHandlerPackageName)) {
202
- this.tabProvider = defaultViewHandlerPackageName;
203
- } else if (packagesSupportingCustomTabs.contains(STABLE_PACKAGE)) {
204
- this.tabProvider = STABLE_PACKAGE;
205
- } else if (packagesSupportingCustomTabs.contains(BETA_PACKAGE)) {
206
- this.tabProvider = BETA_PACKAGE;
207
- } else if (packagesSupportingCustomTabs.contains(DEV_PACKAGE)) {
208
- this.tabProvider = DEV_PACKAGE;
209
- } else if (packagesSupportingCustomTabs.contains(LOCAL_PACKAGE)) {
210
- this.tabProvider = LOCAL_PACKAGE;
211
- }
212
-
213
- return this.tabProvider;
179
+ return null;
214
180
  }
215
181
 
216
182
 
217
183
  /**
218
- * Used to check whether there is a specialized handler for a given intent.
184
+ * Launches the custom tab with the OAuth endpoint URL.
219
185
  *
220
- * @param intent The intent to check with.
221
- * @return Whether there is a specialized handler for the given intent.
186
+ * @param url The URL of the OAuth endpoint.
222
187
  */
223
- private boolean hasSpecializedHandlerIntents(Intent intent) {
224
- try {
225
- PackageManager pm = this.cordova.getActivity().getPackageManager();
226
- List<ResolveInfo> handlers = pm.queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER);
188
+ private void startOAuth(String url) {
189
+ CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
227
190
 
228
- if (handlers == null || handlers.size() == 0) {
229
- return false;
230
- }
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);
231
201
 
232
- for (ResolveInfo resolveInfo : handlers) {
233
- IntentFilter filter = resolveInfo.filter;
202
+ customTabsIntent.launchUrl(this.cordova.getActivity(), Uri.parse(url));
203
+ }
234
204
 
235
- if (filter == null || filter.countDataAuthorities() == 0 || filter.countDataPaths() == 0 || resolveInfo.activityInfo == null) {
236
- continue;
237
- }
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 + "' }));";
238
209
 
239
- return true;
240
- }
241
- } catch (RuntimeException e) {
242
- 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);
243
215
  }
244
-
245
- return false;
246
216
  }
247
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);
package/.editorconfig DELETED
@@ -1,20 +0,0 @@
1
- # Copyright 2017 Ayogo Health Inc
2
- root = true
3
-
4
- [*]
5
- end_of_line = lf
6
- indent_style = space
7
- insert_final_newline = true
8
- charset = utf-8
9
- trim_trailing_whitespace = true
10
- indent_size = 2
11
-
12
- [*.java]
13
- indent_size = 4
14
-
15
- [*.swift]
16
- indent_size = 4
17
-
18
- [*.md]
19
- indent_size = 4
20
- trim_trailing_whitespace = false
@@ -1,77 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as
6
- contributors and maintainers pledge to making participation in our project and
7
- our community a harassment-free experience for everyone, regardless of age, body
8
- size, disability, ethnicity, sex characteristics, gender identity and expression,
9
- level of experience, education, socio-economic status, nationality, personal
10
- appearance, race, religion, or sexual identity and orientation.
11
-
12
- ## Our Standards
13
-
14
- Examples of behavior that contributes to creating a positive environment
15
- include:
16
-
17
- * Using welcoming and inclusive language
18
- * Being respectful of differing viewpoints and experiences
19
- * Gracefully accepting constructive criticism
20
- * Focusing on what is best for the community
21
- * Showing empathy towards other community members
22
-
23
- Examples of unacceptable behavior by participants include:
24
-
25
- * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
27
- * Trolling, insulting/derogatory comments, and personal or political attacks
28
- * Public or private harassment
29
- * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
- * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
33
-
34
- ## Our Responsibilities
35
-
36
- Project maintainers are responsible for clarifying the standards of acceptable
37
- behavior and are expected to take appropriate and fair corrective action in
38
- response to any instances of unacceptable behavior.
39
-
40
- Project maintainers have the right and responsibility to remove, edit, or
41
- reject comments, commits, code, wiki edits, issues, and other contributions
42
- that are not aligned to this Code of Conduct, or to ban temporarily or
43
- permanently any contributor for other behaviors that they deem inappropriate,
44
- threatening, offensive, or harmful.
45
-
46
- ## Scope
47
-
48
- This Code of Conduct applies both within project spaces and in public spaces
49
- when an individual is representing the project or its community. Examples of
50
- representing a project or community include using an official project e-mail
51
- address, posting via an official social media account, or acting as an appointed
52
- representative at an online or offline event. Representation of a project may be
53
- further defined and clarified by project maintainers.
54
-
55
- ## Enforcement
56
-
57
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at opensource@ayogo.com. All
59
- complaints will be reviewed and investigated and will result in a response that
60
- is deemed necessary and appropriate to the circumstances. The project team is
61
- obligated to maintain confidentiality with regard to the reporter of an incident.
62
- Further details of specific enforcement policies may be posted separately.
63
-
64
- Project maintainers who do not follow or enforce the Code of Conduct in good
65
- faith may face temporary or permanent repercussions as determined by other
66
- members of the project's leadership.
67
-
68
- ## Attribution
69
-
70
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
-
73
- [homepage]: https://www.contributor-covenant.org
74
-
75
- For answers to common questions about this code of conduct, see
76
- https://www.contributor-covenant.org/faq
77
-