diff --git a/CHANGELOG.md b/CHANGELOG.md
index d3260897..5683ce8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,21 +2,30 @@
## [Unreleased]
-## [3.0.0] - 2021-07-xx
+### Added
+* Added `logoutUrl` to base options [#97](https://github.com/moberwasserlechner/capacitor-oauth2/issues/97)
+
+### Fixed
+* iOS: Fix boolean param inheritance (#111) [#111](https://github.com/moberwasserlechner/capacitor-oauth2/issues/111)
+
+## [3.0.0] - 2021-08-02
### Breaking
-* Minimum Capacitor version is **3.0.0** [#138](https://github.com/moberwasserlechner/capacitor-oauth2/issues/138) [#140](https://github.com/moberwasserlechner/capacitor-oauth2/pull/140)
+* Minimum Capacitor version is **3.0.0**. Only this plugin version supports Capacitor `3.x`! [#138](https://github.com/moberwasserlechner/capacitor-oauth2/issues/138) [#140](https://github.com/moberwasserlechner/capacitor-oauth2/pull/140)
### Added
* Web: Add a new option `windowReplace` that defaults to undefined. Used in `window.open()` 4th param.
This will fix https://bugs.chromium.org/p/chromium/issues/detail?id=1164959 [#153](https://github.com/moberwasserlechner/capacitor-oauth2/issues/153)
-* Web: Add "authorization_response" and "access_token_response" to "resource response" [#154](https://github.com/moberwasserlechner/capacitor-oauth2/issues/154)
+* Web, Android: Add "authorization_response" and "access_token_response" to the result returned to JS. On iOS it is not possible to extract the authorization response because of the used lib. [#154](https://github.com/moberwasserlechner/capacitor-oauth2/issues/154)
+* Web, Android: Added `additionalResourceHeaders` to base options
+* Web, Android, iOS: Added `logsEnabled` to base options. If enabled extensive logs are written. All logs are prefixed with `I/Capacitor/OAuth2ClientPlugin` across all platforms.
### Changed
* Use `window.crypto` if available to generate random strings [#138](https://github.com/moberwasserlechner/capacitor-oauth2/issues/138) [#140](https://github.com/moberwasserlechner/capacitor-oauth2/pull/140)
### Fixed
* Web: # in URL causes parser to ignore ? [#132](https://github.com/moberwasserlechner/capacitor-oauth2/issues/132) [#133](https://github.com/moberwasserlechner/capacitor-oauth2/pull/133)
+* Android: Fix boolean param inheritance (#162) [#162](https://github.com/moberwasserlechner/capacitor-oauth2/issues/162)
## [2.1.0] - 2020-08-27
@@ -102,6 +111,7 @@ This is controlled by Android specific parameters `handleResultOnNewIntent` for
- Fix github security error by updating Jest lib
[Unreleased]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/3.0.0...master
+[3.1.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/3.0.0...3.1.0
[3.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/2.1.0...3.0.0
[2.1.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/2.0.0...2.1.0
[2.0.0]: https://github.com/moberwasserlechner/capacitor-oauth2/compare/1.1.0...2.0.0
diff --git a/README.md b/README.md
index b8c0dc4b..4b1dedf3 100644
--- a/README.md
+++ b/README.md
@@ -21,17 +21,17 @@ Actively maintained: YES
## Install
```bash
-npm install @capacitor-community/oauth2
+npm i @byteowls/capacitor-oauth2
npx cap sync
```
## Versions
-| Plugin | Minimum Capacitor | Docs | Notes |
+| Plugin | Capacitor | Docs | Notes |
|--------|-------------------|----------------------------------------------------------------------------------------|--------------------------------|
-| 3.x | 3.0.0 | **(NOT RELEASED YET)** [README](https://github.com/moberwasserlechner/oauth2/blob/master/README.md) | Breaking changes see Changelog. XCode 12.0 needs this version |
-| 2.x | 2.0.0 | [README](https://github.com/moberwasserlechner/capacitor-oauth2/blob/2.1.0/README.md) | Breaking changes see Changelog. XCode 11.4 needs this version |
-| 1.x | 1.0.0 | [README](https://github.com/moberwasserlechner/capacitor-oauth2/blob/1.1.0/README.md) | |
+| 3.x | 3.x.x | [README](https://github.com/moberwasserlechner/oauth2/blob/master/README.md) | Breaking changes see Changelog. XCode 12.0 needs this version |
+| 2.x | 2.x.x | [README](https://github.com/moberwasserlechner/capacitor-oauth2/blob/2.1.0/README.md) | Breaking changes see Changelog. XCode 11.4 needs this version |
+| 1.x | 1.x.x | [README](https://github.com/moberwasserlechner/capacitor-oauth2/blob/1.1.0/README.md) | |
For further details on what has changed see the [CHANGELOG](https://github.com/moberwasserlechner/capacitor-oauth2/blob/master/CHANGELOG.md).
@@ -187,18 +187,20 @@ Example:
These parameters are overrideable in every platform
-| parameter | default | required | description | since |
-|---------------------- |--------- |---------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |------- |
-| appId | | yes | aka clientId, serviceId, ... | |
-| authorizationBaseUrl | | yes | | |
-| responseType | | yes | | |
-| redirectUrl | | yes | | 2.0.0 |
-| accessTokenEndpoint | | | If empty the authorization response incl code is returned. Known issue: Not on iOS! | |
-| resourceUrl | | | If empty the tokens are return instead. If you need just the `id_token` you have to set both `accessTokenEndpoint` and `resourceUrl` to `null` or empty ``. | |
-| pkceEnabled | `false` | | Enable PKCE if you need it. | |
-| scope | | | | |
-| state | | | The plugin always uses a state.
If you don't provide one we generate it. | |
-| additionalParameters | | | Additional parameters for anything you might miss, like `none`, `response_mode`.
Just create a key value pair.
```{ "key1": "value", "key2": "value, "response_mode": "value"}``` | |
+| parameter | default | required | description | since |
+|---------------------- |--------- |---------- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |------- |
+| appId | | yes | aka clientId, serviceId, ... | |
+| authorizationBaseUrl | | yes | | |
+| responseType | | yes | | |
+| redirectUrl | | yes | | 2.0.0 |
+| accessTokenEndpoint | | | If empty the authorization response incl code is returned. Known issue: Not on iOS! | |
+| resourceUrl | | | If empty the tokens are return instead. If you need just the `id_token` you have to set both `accessTokenEndpoint` and `resourceUrl` to `null` or empty ``. | |
+| additionalResourceHeaders | | | Additional headers for the resource request | 3.0.0 |
+| pkceEnabled | `false` | | Enable PKCE if you need it. Note: On iOS because of #111 boolean values are not overwritten. You have to explicitly define the param in the subsection. | |
+| logsEnabled | `false` | | Enable extensive logging. All plugin outputs are prefixed with `I/Capacitor/OAuth2ClientPlugin: ` across all platforms. Note: On iOS because of #111 boolean values are not overwritten. You have to explicitly define the param in the subsection. | 3.0.0 |
+| scope | | | | |
+| state | | | The plugin always uses a state.
If you don't provide one we generate it. | |
+| additionalParameters | | | Additional parameters for anything you might miss, like `none`, `response_mode`.
Just create a key value pair.
```{ "key1": "value", "key2": "value, "response_mode": "value"}``` | |
**Platform Web**
@@ -403,7 +405,7 @@ These are some of the providers that can be configured with this plugin. I'm hap
|-----------|------------------------|-------|
| Google | [see below](#google) | |
| Facebook | [see below](#facebook) | |
-| Azure B2C | [see below](#azure-b2c)| |
+| Azure AD B2C | [see below](#azure-b2c)| |
| Apple | [see below](#apple) | ios only |
@@ -501,16 +503,45 @@ not supported
### Azure B2C
-In case of problems please read [#91](https://github.com/moberwasserlechner/capacitor-oauth2/issues/91)
-and [#96](https://github.com/moberwasserlechner/capacitor-oauth2/issues/96)
-
-See this [example repo](https://github.com/loonix/capacitor-oauth2-azure-example) by @loonix.
+It's important to use the urls you see in the Azure config for the specific platform.
#### PWA
-See these 2 configs that should work.
+Setting up Azure B2C in July 2021 presents me with `microsoftonline.com` urls, so the config looks like:
-It's important to use the urls you see in the Azure config for the specific platform.
+```typescript
+import {OAuth2AuthenticateOptions, OAuth2Client} from "@byteowls/capacitor-oauth2";
+
+export class AuthService {
+
+ getAzureB2cOAuth2Options(): OAuth2AuthenticateOptions {
+ return {
+ appId: environment.oauthAppId.azureBc2.appId,
+ authorizationBaseUrl: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/authorize`,
+ scope: "https://graph.microsoft.com/User.Read", // See Azure Portal -> API permission
+ accessTokenEndpoint: `https://login.microsoftonline.com/${environment.oauthAppId.azureBc2.tenantId}/oauth2/v2.0/token`,
+ resourceUrl: "https://graph.microsoft.com/v1.0/me/",
+ responseType: "code",
+ pkceEnabled: true,
+ logsEnabled: true,
+ web: {
+ redirectUrl: environment.redirectUrl,
+ windowOptions: "height=600,left=0,top=0",
+ },
+ android: {
+ redirectUrl: "msauth://{package-name}/{url-encoded-signature-hash}" // See Azure Portal -> Authentication -> Android Configuration "Redirect URI"
+ },
+ ios: {
+ pkceEnabled: true, // workaround for bug #111
+ redirectUrl: "msauth.{package-name}://auth"
+ }
+ };
+ }
+}
+```
+
+
+Other configs that works in prior versions
```typescript
import {OAuth2Client} from "@byteowls/capacitor-oauth2";
@@ -574,8 +605,53 @@ azureLogin() {
}
```
+
+
#### Android
+If you have **only** Azure B2C as identity provider you have to add a new `intent-filter` to your main activity in `AndroidManifest.xml`.
+
+```xml
+
+
+
+
+
+
+
+```
+
+If you have **multiple** identity providers you have to create a new Activity in `AndroidManifest.xml`.
+
+In my case I had Google and Azure AD B2C.
+
+Without this extra activity the result was always `RESULT_CANCELED`.
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Example values
+* @string/azure_b2c_scheme ... `msauth`
+* @string/package_name ... `com.company.project`
+* azure_b2c_signature_hash ... `/your-signature-hash` ... The leading slash is required. Copied from Azure Portal Android Config "Signature hash" field
+
See [Android Default Config](#android-default-config)
#### iOS
@@ -588,13 +664,20 @@ Open `Info.plist` in XCode by Right Click on that file -> Open as -> Source Code
CFBundleURLSchemes
- msauth.BUNDLE_ID
+
+ msauth.com.yourcompany.yourproject
```
-Do not enter `://` and part of your redirect url after those chars.
+Do not enter `://` and part of your redirect url.
+
+#### Troubleshooting
+In case of problems please read [#91](https://github.com/moberwasserlechner/capacitor-oauth2/issues/91)
+and [#96](https://github.com/moberwasserlechner/capacitor-oauth2/issues/96)
+
+See this [example repo](https://github.com/loonix/capacitor-oauth2-azure-example) by @loonix.
### Google
diff --git a/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2ClientPlugin.java b/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2ClientPlugin.java
index b45f15e2..879b8bac 100644
--- a/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2ClientPlugin.java
+++ b/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2ClientPlugin.java
@@ -4,6 +4,7 @@
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
+import android.os.AsyncTask;
import android.util.Log;
import androidx.activity.result.ActivityResult;
@@ -25,6 +26,7 @@
import net.openid.appauth.AuthorizationServiceConfiguration;
import net.openid.appauth.GrantTypeValues;
import net.openid.appauth.TokenRequest;
+import net.openid.appauth.TokenResponse;
import org.json.JSONException;
@@ -43,6 +45,7 @@ public class OAuth2ClientPlugin extends Plugin {
private static final String PARAM_ACCESS_TOKEN_ENDPOINT = "accessTokenEndpoint";
private static final String PARAM_PKCE_ENABLED = "pkceEnabled";
private static final String PARAM_RESOURCE_URL = "resourceUrl";
+ private static final String PARAM_ADDITIONAL_RESOURCE_HEADERS = "additionalResourceHeaders";
private static final String PARAM_ADDITIONAL_PARAMETERS = "additionalParameters";
private static final String PARAM_ANDROID_CUSTOM_HANDLER_CLASS = "android.customHandlerClass";
// Activity result handling
@@ -154,13 +157,16 @@ public void authenticate(final PluginCall call) {
disposeAuthService();
oauth2Options = buildAuthenticateOptions(call.getData());
if (oauth2Options.getCustomHandlerClass() != null) {
+ if (oauth2Options.isLogsEnabled()) {
+ Log.i(getLogTag(), "Entering custom handler: " + oauth2Options.getCustomHandlerClass().getClass().getName());
+ }
try {
Class handlerClass = (Class) Class.forName(oauth2Options.getCustomHandlerClass());
OAuth2CustomHandler handler = handlerClass.newInstance();
handler.getAccessToken(getActivity(), call, new AccessTokenCallback() {
@Override
public void onSuccess(String accessToken) {
- new ResourceUrlAsyncTask(call, oauth2Options, getLogTag()).execute(accessToken);
+ new ResourceUrlAsyncTask(call, oauth2Options, getLogTag(), null, null).execute(accessToken);
}
@Override
@@ -316,10 +322,12 @@ protected void handleOnNewIntent(Intent intent) {
@ActivityCallback
private void handleIntentResult(PluginCall call, ActivityResult result) {
- if (result.getResultCode() == Activity.RESULT_CANCELED) {
- call.reject(USER_CANCELLED);
- } else {
- handleAuthorizationRequestActivity(result.getData(), call);
+ if (this.oauth2Options != null && this.oauth2Options.isHandleResultOnActivityResult()) {
+ if (result.getResultCode() == Activity.RESULT_CANCELED) {
+ call.reject(USER_CANCELLED);
+ } else {
+ handleAuthorizationRequestActivity(result.getData(), call);
+ }
}
}
@@ -341,6 +349,12 @@ void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) {
if (error.code == AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW.code) {
savedCall.reject(USER_CANCELLED);
} else if (error.code == AuthorizationException.AuthorizationRequestErrors.STATE_MISMATCH.code) {
+ if (oauth2Options.isLogsEnabled()) {
+ Log.i(getLogTag(), "State from web options: " + oauth2Options.getState());
+ if (authorizationResponse != null) {
+ Log.i(getLogTag(), "State returned from provider: " + authorizationResponse.state);
+ }
+ }
savedCall.reject(ERR_STATES_NOT_MATCH);
} else {
savedCall.reject(ERR_GENERAL, error);
@@ -350,6 +364,9 @@ void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) {
// this response may contain the authorizationCode but also idToken and accessToken depending on the flow chosen by responseType
if (authorizationResponse != null) {
+ if (oauth2Options.isLogsEnabled()) {
+ Log.i(getLogTag(), "Authorization response:\n" + authorizationResponse.jsonSerializeString());
+ }
// if there is a tokenEndpoint configured try to get the accessToken from it.
// it might be already in the authorizationResponse but tokenEndpoint might deliver other tokens.
if (oauth2Options.getAccessTokenEndpoint() != null) {
@@ -363,14 +380,22 @@ void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) {
savedCall.reject(ERR_AUTHORIZATION_FAILED, exception);
} else {
if (accessTokenResponse != null) {
- if (oauth2Options.getResourceUrl() != null) {
- authState.performActionWithFreshTokens(authService, (accessToken, idToken, ex1)
- -> new ResourceUrlAsyncTask(savedCall, oauth2Options, getLogTag()).execute(accessToken));
- } else {
- createJsObjAndResolve(savedCall, accessTokenResponse.jsonSerializeString());
+ if (oauth2Options.isLogsEnabled()) {
+ Log.i(getLogTag(), "Access token response:\n" + accessTokenResponse.jsonSerializeString());
}
+ authState.performActionWithFreshTokens(authService,
+ (accessToken, idToken, ex1) -> {
+ AsyncTask asyncTask =
+ new ResourceUrlAsyncTask(
+ savedCall,
+ oauth2Options,
+ getLogTag(),
+ authorizationResponse,
+ accessTokenResponse);
+ asyncTask.execute(accessToken);
+ });
} else {
- savedCall.reject(ERR_NO_ACCESS_TOKEN);
+ resolveAuthorizationResponse(savedCall, authorizationResponse);
}
}
});
@@ -378,7 +403,7 @@ void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) {
savedCall.reject(ERR_NO_AUTHORIZATION_CODE, e);
}
} else {
- createJsObjAndResolve(savedCall, authorizationResponse.jsonSerializeString());
+ resolveAuthorizationResponse(savedCall, authorizationResponse);
}
} else {
savedCall.reject(ERR_NO_AUTHORIZATION_CODE);
@@ -391,13 +416,10 @@ void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) {
}
}
- void createJsObjAndResolve(PluginCall call, String jsonStr) {
- try {
- JSObject json = new JSObject(jsonStr);
- call.resolve(json);
- } catch (JSONException e) {
- call.reject(ERR_GENERAL, e);
- }
+ private void resolveAuthorizationResponse(PluginCall savedCall, AuthorizationResponse authorizationResponse) {
+ JSObject json = new JSObject();
+ OAuth2Utils.assignResponses(json, null, authorizationResponse, null);
+ savedCall.resolve(json);
}
OAuth2Options buildAuthenticateOptions(JSObject callData) {
@@ -442,6 +464,7 @@ OAuth2Options buildAuthenticateOptions(JSObject callData) {
}
}
}
+ o.setAdditionalResourceHeaders(ConfigUtils.getOverwrittenAndroidParamMap(callData, PARAM_ADDITIONAL_RESOURCE_HEADERS));
// android only
o.setCustomHandlerClass(ConfigUtils.trimToNull(ConfigUtils.getParamString(callData, PARAM_ANDROID_CUSTOM_HANDLER_CLASS)));
o.setHandleResultOnNewIntent(ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_NEW_INTENT, false));
diff --git a/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2Options.java b/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2Options.java
index d5624377..98c437bd 100644
--- a/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2Options.java
+++ b/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2Options.java
@@ -19,6 +19,7 @@ public class OAuth2Options {
private String accessTokenEndpoint;
private String resourceUrl;
+ private Map additionalResourceHeaders;
private boolean pkceEnabled;
private boolean logsEnabled;
@@ -197,4 +198,21 @@ public boolean isHandleResultOnActivityResult() {
public void setHandleResultOnActivityResult(boolean handleResultOnActivityResult) {
this.handleResultOnActivityResult = handleResultOnActivityResult;
}
+
+ public Map getAdditionalResourceHeaders() {
+ return additionalResourceHeaders;
+ }
+
+ public void setAdditionalResourceHeaders(Map additionalResourceHeaders) {
+ this.additionalResourceHeaders = additionalResourceHeaders;
+ }
+
+ public void addAdditionalResourceHeader(String key, String value) {
+ if (key != null && value != null) {
+ if (this.additionalResourceHeaders == null) {
+ this.additionalResourceHeaders = new HashMap<>();
+ }
+ this.additionalResourceHeaders.put(key, value);
+ }
+ }
}
diff --git a/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2Utils.java b/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2Utils.java
new file mode 100644
index 00000000..9ca96769
--- /dev/null
+++ b/android/src/main/java/com/byteowls/capacitor/oauth2/OAuth2Utils.java
@@ -0,0 +1,22 @@
+package com.byteowls.capacitor.oauth2;
+
+import com.getcapacitor.JSObject;
+
+import net.openid.appauth.AuthorizationResponse;
+import net.openid.appauth.TokenResponse;
+
+public abstract class OAuth2Utils {
+
+ public static void assignResponses(JSObject resp, String accessToken, AuthorizationResponse authorizationResponse, TokenResponse accessTokenResponse) {
+ // #154
+ if (authorizationResponse != null) {
+ resp.put("authorization_response", authorizationResponse.jsonSerializeString());
+ }
+ if (accessTokenResponse != null) {
+ resp.put("access_token_response", accessTokenResponse.jsonSerializeString());
+ }
+ if (accessToken != null) {
+ resp.put("access_token", accessToken);
+ }
+ }
+}
diff --git a/android/src/main/java/com/byteowls/capacitor/oauth2/ResourceUrlAsyncTask.java b/android/src/main/java/com/byteowls/capacitor/oauth2/ResourceUrlAsyncTask.java
index c432f73b..9228eaf3 100644
--- a/android/src/main/java/com/byteowls/capacitor/oauth2/ResourceUrlAsyncTask.java
+++ b/android/src/main/java/com/byteowls/capacitor/oauth2/ResourceUrlAsyncTask.java
@@ -4,6 +4,10 @@
import android.util.Log;
import com.getcapacitor.JSObject;
import com.getcapacitor.PluginCall;
+
+import net.openid.appauth.AuthorizationResponse;
+import net.openid.appauth.TokenResponse;
+
import org.json.JSONException;
import java.io.BufferedReader;
@@ -13,6 +17,7 @@
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
+import java.util.Map;
/**
* @author m.oberwasserlechner@byteowls.com
@@ -20,66 +25,98 @@
public class ResourceUrlAsyncTask extends AsyncTask {
private static final String ERR_GENERAL = "ERR_GENERAL";
- private PluginCall pluginCall;
- private OAuth2Options options;
- private String logTag;
+ private static final String ERR_NO_ACCESS_TOKEN = "ERR_NO_ACCESS_TOKEN";
+ private static final String MSG_RETURNED_TO_JS = "Returned to JS:\n";
- ResourceUrlAsyncTask(PluginCall pluginCall, OAuth2Options options, String logTag) {
+ private final PluginCall pluginCall;
+ private final OAuth2Options options;
+ private final String logTag;
+ private final AuthorizationResponse authorizationResponse;
+ private final TokenResponse accessTokenResponse;
+
+ public ResourceUrlAsyncTask(PluginCall pluginCall, OAuth2Options options, String logTag, AuthorizationResponse authorizationResponse, TokenResponse accessTokenResponse) {
this.pluginCall = pluginCall;
this.options = options;
this.logTag = logTag;
+ this.authorizationResponse = authorizationResponse;
+ this.accessTokenResponse = accessTokenResponse;
}
@Override
protected ResourceCallResult doInBackground(String... tokens) {
- String resourceUrl = options.getResourceUrl();
ResourceCallResult result = new ResourceCallResult();
- String accessToken = tokens[0];
-
- if (resourceUrl == null) {
- JSObject json = new JSObject();
- json.put("access_token", accessToken);
- result.setResponse(json);
- return result;
- }
- try {
- URL url = new URL(resourceUrl);
- HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.addRequestProperty("Authorization", String.format("Bearer %s", accessToken));
- try {
- InputStream is;
+ String resourceUrl = options.getResourceUrl();
+ String accessToken = tokens[0];
+ if (resourceUrl != null) {
+ Log.i(logTag, "Resource url: GET " + resourceUrl);
+ if (accessToken != null) {
+ Log.i(logTag, "Access token:\n" + accessToken);
+ try {
+ URL url = new URL(resourceUrl);
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.addRequestProperty("Authorization", String.format("Bearer %s", accessToken));
+ // additional headers
+ if (options.getAdditionalResourceHeaders() != null) {
+ for (Map.Entry entry : options.getAdditionalResourceHeaders().entrySet()) {
+ conn.addRequestProperty(entry.getKey(), entry.getValue());
+ }
+ }
- if (conn.getResponseCode() >= HttpURLConnection.HTTP_OK
- && conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE) {
- is = conn.getInputStream();
- } else {
- is = conn.getErrorStream();
- result.setError(true);
+ InputStream is = null;
+ try {
+ if (conn.getResponseCode() >= HttpURLConnection.HTTP_OK
+ && conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE) {
+ is = conn.getInputStream();
+ } else {
+ is = conn.getErrorStream();
+ result.setError(true);
+ }
+ String resourceResponseBody = readInputStream(is);
+ if (!result.isError()) {
+ JSObject resultJson = new JSObject(resourceResponseBody);
+ if (options.isLogsEnabled()) {
+ Log.i(logTag, "Resource response:\n" + resourceResponseBody);
+ }
+ OAuth2Utils.assignResponses(resultJson, accessToken, this.authorizationResponse, this.accessTokenResponse);
+ if (options.isLogsEnabled()) {
+ Log.i(logTag, MSG_RETURNED_TO_JS + resultJson);
+ }
+ result.setResponse(resultJson);
+ } else {
+ result.setErrorMsg(resourceResponseBody);
+ }
+ } catch (IOException e) {
+ Log.e(logTag, "", e);
+ } catch (JSONException e) {
+ Log.e(logTag, "Resource response no valid json.", e);
+ } finally {
+ conn.disconnect();
+ if (is != null) {
+ is.close();
+ }
+ }
+ } catch (MalformedURLException e) {
+ Log.e(logTag, "Invalid resource url '" + resourceUrl + "'", e);
+ } catch (IOException e) {
+ Log.e(logTag, "Unexpected error", e);
}
- String jsonBody = readInputStream(is);
- if (!result.isError()) {
- JSObject json = new JSObject(jsonBody);
- json.put("access_token", accessToken);
- result.setResponse(json);
- } else {
- result.setErrorMsg(jsonBody);
+ } else {
+ if (options.isLogsEnabled()) {
+ Log.i(logTag, "No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config.");
}
- return result;
- } catch (IOException e) {
- Log.e(logTag, "", e);
- } catch (JSONException e) {
- Log.e(logTag, "Resource response no valid json.", e);
- } finally {
- conn.disconnect();
+ pluginCall.reject(ERR_NO_ACCESS_TOKEN);
}
- } catch (MalformedURLException e) {
- Log.e(logTag, "Invalid resource url '" + resourceUrl + "'", e);
- } catch (IOException e) {
- Log.e(logTag, "Unexpected error", e);
+ } else {
+ JSObject json = new JSObject();
+ OAuth2Utils.assignResponses(json, accessToken, this.authorizationResponse, this.accessTokenResponse);
+ if (options.isLogsEnabled()) {
+ Log.i(logTag, MSG_RETURNED_TO_JS + json);
+ }
+ result.setResponse(json);
}
- return null;
+ return result;
}
@Override
@@ -89,7 +126,7 @@ protected void onPostExecute(ResourceCallResult response) {
pluginCall.resolve(response.getResponse());
} else {
Log.e(logTag, response.getErrorMsg());
- pluginCall.reject(ERR_GENERAL);
+ pluginCall.reject(ERR_GENERAL, response.getErrorMsg());
}
} else {
pluginCall.reject(ERR_GENERAL);
diff --git a/ios/ByteowlsCapacitorOauth2/Source/ByteowlsCapacitorOauth2.swift b/ios/ByteowlsCapacitorOauth2/Source/ByteowlsCapacitorOauth2.swift
index dac9491f..fe2da729 100644
--- a/ios/ByteowlsCapacitorOauth2/Source/ByteowlsCapacitorOauth2.swift
+++ b/ios/ByteowlsCapacitorOauth2/Source/ByteowlsCapacitorOauth2.swift
@@ -12,6 +12,8 @@ public class OAuth2ClientPlugin: CAPPlugin {
var savedPluginCall: CAPPluginCall?
let JSON_KEY_ACCESS_TOKEN = "access_token"
+ let JSON_KEY_AUTHORIZATION_RESPONSE = "authorization_response"
+ let JSON_KEY_ACCESS_TOKEN_RESPONSE = "access_token_response"
let PARAM_REFRESH_TOKEN = "refreshToken"
@@ -23,6 +25,7 @@ public class OAuth2ClientPlugin: CAPPlugin {
// controlling
let PARAM_ACCESS_TOKEN_ENDPOINT = "accessTokenEndpoint"
let PARAM_RESOURCE_URL = "resourceUrl"
+ let PARAM_ADDITIONAL_RESOURCE_HEADERS = "additionalResourceHeaders"
let PARAM_ADDITIONAL_PARAMETERS = "additionalParameters"
let PARAM_CUSTOM_HANDLER_CLASS = "ios.customHandlerClass"
@@ -30,6 +33,8 @@ public class OAuth2ClientPlugin: CAPPlugin {
let PARAM_STATE = "state"
let PARAM_PKCE_ENABLED = "pkceEnabled"
let PARAM_IOS_USE_SCOPE = "ios.siwaUseScope"
+ let PARAM_LOGOUT_URL = "logoutUrl"
+ let PARAM_LOGS_ENABLED = "logsEnabled"
let ERR_GENERAL = "ERR_GENERAL"
@@ -164,13 +169,15 @@ public class OAuth2ClientPlugin: CAPPlugin {
return
}
let resourceUrl = getOverwritableString(call, self.PARAM_RESOURCE_URL)
- // Github issue #71
+ let logsEnabled: Bool = getOverwritable(call, self.PARAM_LOGS_ENABLED) as? Bool ?? false
+ // #71
self.oauth2SafariDelegate = OAuth2SafariDelegate(call)
// ######### Custom Handler ########
if let handlerClassName = getString(call, PARAM_CUSTOM_HANDLER_CLASS) {
if let handlerInstance = self.getOrLoadHandlerInstance(className: handlerClassName) {
+ log("Entering custom handler: " + handlerClassName)
handlerInstance.getAccessToken(viewController: (bridge?.viewController)!, call: call, success: { (accessToken) in
if resourceUrl != nil {
@@ -206,7 +213,9 @@ public class OAuth2ClientPlugin: CAPPlugin {
}, cancelled: {
call.reject(SharedConstants.ERR_USER_CANCELLED)
}, failure: { error in
- self.log("Login failed because '\(error)'")
+ if logsEnabled {
+ self.log("Login failed because '\(error)'")
+ }
call.reject(self.ERR_CUSTOM_HANDLER_LOGIN)
})
} else {
@@ -260,16 +269,7 @@ public class OAuth2ClientPlugin: CAPPlugin {
// additional parameters #18
let callParameter: [String: Any] = getOverwritable(call, PARAM_ADDITIONAL_PARAMETERS) as? [String: Any] ?? [:]
- var additionalParameters: [String: String] = [:]
- for (key, value) in callParameter {
- // only non empty string values are allowed
- if !key.isEmpty && value is String {
- let str = value as! String;
- if !str.isEmpty {
- additionalParameters[key] = str
- }
- }
- }
+ let additionalParameters = buildStringDict(callParameter);
let requestState = getOverwritableString(call, PARAM_STATE) ?? generateRandom(withLength: 20)
let pkceEnabled: Bool = getOverwritable(call, PARAM_PKCE_ENABLED) as? Bool ?? false
@@ -285,7 +285,7 @@ public class OAuth2ClientPlugin: CAPPlugin {
codeChallenge: pkceCodeChallenge,
codeVerifier: pkceCodeVerifier,
parameters: additionalParameters) { result in
- self.handleAuthorizationResult(result, call, responseType, requestState, resourceUrl)
+ self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl)
}
} else {
oauthSwift.authorize(
@@ -293,7 +293,7 @@ public class OAuth2ClientPlugin: CAPPlugin {
scope: getOverwritableString(call, PARAM_SCOPE) ?? "",
state: requestState,
parameters: additionalParameters) { result in
- self.handleAuthorizationResult(result, call, responseType, requestState, resourceUrl)
+ self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl)
}
}
}
@@ -331,33 +331,71 @@ public class OAuth2ClientPlugin: CAPPlugin {
// ### Helper functions
// #################################
- private func handleAuthorizationResult(_ result: Result, _ call: CAPPluginCall, _ responseType: String, _ requestState: String, _ resourceUrl: String?) {
+ private func handleAuthorizationResult(_ result: Result,
+ _ call: CAPPluginCall,
+ _ responseType: String,
+ _ requestState: String,
+ _ logsEnabled: Bool,
+ _ resourceUrl: String?) {
switch result {
case .success(let (credential, response, parameters)):
+ if logsEnabled, let accessTokenResponse = response {
+ logDataObj("Authorization or Access token response:", accessTokenResponse.data)
+ }
+
// state is aready checked by the lib
if resourceUrl != nil && !resourceUrl!.isEmpty {
- self.oauthSwift!.client.get(
- resourceUrl!,
- parameters: parameters) { result in
+ if logsEnabled {
+ log("Resource url: \(resourceUrl!)")
+ log("Access token:\n\(credential.oauthToken)")
+ }
+ // resource url request headers
+ let callParameter: [String: Any] = getOverwritable(call, PARAM_ADDITIONAL_RESOURCE_HEADERS) as? [String: Any] ?? [:]
+ let additionalHeadersDict = buildStringDict(callParameter);
+
+ self.oauthSwift!.client.get(resourceUrl!,
+ headers: additionalHeadersDict) { result in
switch result {
- case .success(let response):
+ case .success(let resourceResponse):
do {
- var jsonObj = try JSONSerialization.jsonObject(with: response.data, options: []) as! JSObject
+ if logsEnabled {
+ self.logDataObj("Resource response:", resourceResponse.data)
+ }
+
+ var jsonObj = try JSONSerialization.jsonObject(with: resourceResponse.data, options: []) as! JSObject
// send the access token to the caller so e.g. it can be stored on a backend
+ // #154
+ if let accessTokenResponse = response {
+ let accessTokenJsObject = try? JSONSerialization.jsonObject(with: accessTokenResponse.data, options: []) as? JSObject
+ jsonObj.updateValue(accessTokenJsObject!, forKey: self.JSON_KEY_ACCESS_TOKEN_RESPONSE)
+ }
+
jsonObj.updateValue(credential.oauthToken, forKey: self.JSON_KEY_ACCESS_TOKEN)
+
+ if logsEnabled {
+ self.log("Returned to JS:\n\(jsonObj)")
+ }
+
call.resolve(jsonObj)
} catch {
- self.log("Invalid json in resource response \(error.localizedDescription)")
+ self.log("Invalid json in resource response:\n \(error.localizedDescription)")
call.reject(self.ERR_GENERAL)
}
case .failure(let error):
- self.log("Access resource request failed with \(error.localizedDescription)");
+ self.log("Resource url request failed:\n\(error.description)");
call.reject(self.ERR_GENERAL)
}
}
+ // no resource url
} else if let responseData = response?.data {
do {
- let jsonObj = try JSONSerialization.jsonObject(with: responseData, options: []) as! JSObject
+ var jsonObj = JSObject()
+ let accessTokenJsObject = try? JSONSerialization.jsonObject(with: responseData, options: []) as? JSObject
+ jsonObj.updateValue(accessTokenJsObject!, forKey: self.JSON_KEY_ACCESS_TOKEN_RESPONSE)
+
+ if logsEnabled {
+ self.log("Returned to JS:\n\(jsonObj)")
+ }
call.resolve(jsonObj)
} catch {
self.log("Invalid json in response \(error.localizedDescription)")
@@ -447,7 +485,26 @@ public class OAuth2ClientPlugin: CAPPlugin {
}
private func log(_ msg: String) {
- print("@byteowls/capacitor-oauth2: \(msg).")
+ print("I/Capacitor/OAuth2ClientPlugin: \(msg)")
+ }
+
+ private func logDataObj(_ msg: String, _ data: Data) {
+ let json = try? JSONSerialization.jsonObject(with: data, options: [])
+ log("\(msg)\n\(json ?? "")")
+ }
+
+ private func buildStringDict(_ callParameter: [String: Any]) -> [String: String] {
+ var dict: [String: String] = [:]
+ for (key, value) in callParameter {
+ // only non empty string values are allowed
+ if !key.isEmpty && value is String {
+ let str = value as! String;
+ if !str.isEmpty {
+ dict[key] = str
+ }
+ }
+ }
+ return dict;
}
private func loadHandlerInstance(className: String) -> OAuth2CustomHandler? {
diff --git a/package-lock.json b/package-lock.json
index aeedf64e..cc9c4d92 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -521,9 +521,9 @@
"dev": true
},
"@eslint/eslintrc": {
- "version": "0.4.2",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.2.tgz",
- "integrity": "sha512-8nmGq/4ycLpIwzvhI4tNDmQztZ8sp+hI7cyG8i1nQDhkAbRzHpXPidRAHlNvCZQpJTKw5ItIpMw9RSToGF00mg==",
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz",
+ "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==",
"dev": true,
"requires": {
"ajv": "^6.12.4",
@@ -537,6 +537,23 @@
"strip-json-comments": "^3.1.1"
}
},
+ "@humanwhocodes/config-array": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz",
+ "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^1.2.0",
+ "debug": "^4.1.1",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "@humanwhocodes/object-schema": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz",
+ "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==",
+ "dev": true
+ },
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -1046,9 +1063,9 @@
}
},
"@types/jest": {
- "version": "26.0.23",
- "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz",
- "integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==",
+ "version": "26.0.24",
+ "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.24.tgz",
+ "integrity": "sha512-E/X5Vib8BWqZNRlDxj9vYXhsDwPYbPINqKF9BsnSoon4RQ0D9moEuLD8txgyypFLH7J4+Lho9Nr/c8H0Fi+17w==",
"dev": true,
"requires": {
"jest-diff": "^26.0.0",
@@ -1074,9 +1091,9 @@
"dev": true
},
"@types/yargs": {
- "version": "15.0.13",
- "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz",
- "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==",
+ "version": "15.0.14",
+ "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz",
+ "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
@@ -1111,9 +1128,9 @@
}
},
"acorn-jsx": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz",
- "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true
},
"acorn-walk": {
@@ -1711,13 +1728,14 @@
}
},
"eslint": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.29.0.tgz",
- "integrity": "sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==",
+ "version": "7.32.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz",
+ "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==",
"dev": true,
"requires": {
"@babel/code-frame": "7.12.11",
- "@eslint/eslintrc": "^0.4.2",
+ "@eslint/eslintrc": "^0.4.3",
+ "@humanwhocodes/config-array": "^0.5.0",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.2",
@@ -2000,9 +2018,9 @@
}
},
"flatted": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.1.1.tgz",
- "integrity": "sha512-zAoAQiudy+r5SvnSw3KJy5os/oRJYHzrzja/tBDqrZtNhUw8bt6y8OBzMWcjWr+8liV8Eb6yOhw8WZ7VFZ5ZzA==",
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz",
+ "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==",
"dev": true
},
"form-data": {
@@ -2089,9 +2107,9 @@
}
},
"globals": {
- "version": "13.9.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-13.9.0.tgz",
- "integrity": "sha512-74/FduwI/JaIrr1H8e71UbDE+5x7pIPs1C2rrwC52SszOo043CsWOZEMW7o2Y58xwm9b+0RBKDxY5n2sUpEFxA==",
+ "version": "13.10.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.10.0.tgz",
+ "integrity": "sha512-piHC3blgLGFjvOuMmWZX60f+na1lXFDhQXBf1UYp2fXPXqvEUbOhNwi6BsQ0bQishwedgnjkwv1d9zKf+MWw3g==",
"dev": true,
"requires": {
"type-fest": "^0.20.2"
@@ -4306,9 +4324,9 @@
},
"dependencies": {
"ajv": {
- "version": "8.6.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.0.tgz",
- "integrity": "sha512-cnUG4NSBiM4YFBxgZIj/In3/6KX+rQ2l2YPRVcvAMQGWEPKuXoPIhxzwqh31jA3IPbI4qEOp/5ILI4ynioXsGQ==",
+ "version": "8.6.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz",
+ "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==",
"dev": true,
"requires": {
"fast-deep-equal": "^3.1.1",
@@ -4400,9 +4418,9 @@
}
},
"ts-jest": {
- "version": "27.0.3",
- "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.3.tgz",
- "integrity": "sha512-U5rdMjnYam9Ucw+h0QvtNDbc5+88nxt7tbIvqaZUhFrfG4+SkWhMXjejCLVGcpILTPuV+H3W/GZDZrnZFpPeXw==",
+ "version": "27.0.4",
+ "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-27.0.4.tgz",
+ "integrity": "sha512-c4E1ECy9Xz2WGfTMyHbSaArlIva7Wi2p43QOMmCqjSSjHP06KXv+aT+eSY+yZMuqsMi3k7pyGsGj2q5oSl5WfQ==",
"dev": true,
"requires": {
"bs-logger": "0.x",
@@ -4454,9 +4472,9 @@
}
},
"typescript": {
- "version": "4.1.6",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.6.tgz",
- "integrity": "sha512-pxnwLxeb/Z5SP80JDRzVjh58KsM6jZHRAOtTpS7sXLS4ogXNKC9ANxHHZqLLeVHZN35jCtI4JdmLLbLiC1kBow==",
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz",
+ "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==",
"dev": true
},
"universalify": {
diff --git a/package.json b/package.json
index d192afda..fed9acfc 100644
--- a/package.json
+++ b/package.json
@@ -57,11 +57,11 @@
"@capacitor/android": "3.0.2",
"@capacitor/core": "3.0.2",
"@capacitor/ios": "3.0.2",
- "@types/jest": "26.0.23",
+ "@types/jest": "26.0.24",
"jest": "27.0.6",
- "ts-jest": "27.0.3",
- "eslint": "^7.11.0",
- "rimraf": "^3.0.0",
- "typescript": "~4.1.5"
+ "ts-jest": "27.0.4",
+ "eslint": "7.32.0",
+ "rimraf": "3.0.2",
+ "typescript": "4.1.5"
}
}
diff --git a/src/definitions.ts b/src/definitions.ts
index 05a8ee47..c7014f2c 100644
--- a/src/definitions.ts
+++ b/src/definitions.ts
@@ -70,7 +70,7 @@ export interface OAuth2AuthenticateBaseOptions {
*/
accessTokenEndpoint?: string;
/**
- * Protected resource url. For authentification you only need the basic user details.
+ * Protected resource url. For authentication you only need the basic user details.
*/
resourceUrl?: string;
/**
@@ -95,6 +95,16 @@ export interface OAuth2AuthenticateBaseOptions {
* @since 3.0.0
*/
logsEnabled?: boolean;
+ /**
+ * @since 3.1.0 ... not implemented yet!
+ */
+ logoutUrl?: string;
+
+ /**
+ * Additional headers for resource url request
+ * @since 3.0.0
+ */
+ additionalResourceHeaders?: { [key: string]: string }
}
export interface OAuth2AuthenticateOptions extends OAuth2AuthenticateBaseOptions {
@@ -110,7 +120,7 @@ export interface OAuth2AuthenticateOptions extends OAuth2AuthenticateBaseOptions
/**
* Custom options for the platform "ios"
*/
- ios?: IosOptions,
+ ios?: IosOptions
}
export interface WebOption extends OAuth2AuthenticateBaseOptions {
diff --git a/src/web-utils.test.ts b/src/web-utils.test.ts
index 71c44203..f24730f4 100644
--- a/src/web-utils.test.ts
+++ b/src/web-utils.test.ts
@@ -264,3 +264,36 @@ describe("Crypto utils", () => {
});
});
+describe("additional resource headers", () => {
+ const headerKey = "Access-Control-Allow-Origin";
+
+ const options: OAuth2AuthenticateOptions = {
+ appId: "appId",
+ authorizationBaseUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
+ accessTokenEndpoint: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+ scope: "files.readwrite offline_access",
+ responseType: "code",
+ additionalResourceHeaders: {
+ "Access-Control-Allow-Origin": "will-be-overwritten",
+ },
+ web: {
+ redirectUrl: "https://oauth2.byteowls.com/authorize",
+ pkceEnabled: false,
+ additionalResourceHeaders: {
+ "Access-Control-Allow-Origin": "*",
+ }
+ }
+ };
+
+ it('should be defined', async () => {
+ const webOptions = await WebUtils.buildWebOptions(options);
+ expect(webOptions.additionalResourceHeaders[headerKey]).toBeDefined();
+ });
+
+ it('should equal *', async () => {
+ const webOptions = await WebUtils.buildWebOptions(options);
+ expect(webOptions.additionalResourceHeaders[headerKey]).toEqual("*");
+ });
+
+});
+
diff --git a/src/web-utils.ts b/src/web-utils.ts
index b4e6f3e4..79d16913 100644
--- a/src/web-utils.ts
+++ b/src/web-utils.ts
@@ -153,18 +153,30 @@ export class WebUtils {
if (!webOptions.state || webOptions.state.length === 0) {
webOptions.state = this.randomString(20);
}
- let mapHelper = this.getOverwritableValue<{ [key: string]: string }>(configOptions, "additionalParameters");
- if (mapHelper) {
+ let parametersMapHelper = this.getOverwritableValue<{ [key: string]: string }>(configOptions, "additionalParameters");
+ if (parametersMapHelper) {
webOptions.additionalParameters = {};
- for (const key in mapHelper) {
+ for (const key in parametersMapHelper) {
if (key && key.trim().length > 0) {
- let value = mapHelper[key];
+ let value = parametersMapHelper[key];
if (value && value.trim().length > 0) {
webOptions.additionalParameters[key] = value;
}
}
}
}
+ let headersMapHelper = this.getOverwritableValue<{ [key: string]: string }>(configOptions, "additionalResourceHeaders");
+ if (headersMapHelper) {
+ webOptions.additionalResourceHeaders = {};
+ for (const key in headersMapHelper) {
+ if (key && key.trim().length > 0) {
+ let value = headersMapHelper[key];
+ if (value && value.trim().length > 0) {
+ webOptions.additionalResourceHeaders[key] = value;
+ }
+ }
+ }
+ }
webOptions.logsEnabled = this.getOverwritableValue(configOptions, "logsEnabled");
if (configOptions.web) {
@@ -257,5 +269,6 @@ export class WebOptions {
pkceCodeChallengeMethod: string;
additionalParameters: { [key: string]: string };
+ additionalResourceHeaders: { [key: string]: string };
}
diff --git a/src/web.ts b/src/web.ts
index 5ad8b69f..cf5a60ab 100644
--- a/src/web.ts
+++ b/src/web.ts
@@ -39,7 +39,7 @@ export class OAuth2ClientPluginWeb extends WebPlugin implements OAuth2ClientPlug
// open window
const authorizationUrl = WebUtils.getAuthorizationUrl(this.webOptions);
if (this.webOptions.logsEnabled) {
- console.log("AuthorizationUrl: " + authorizationUrl);
+ this.doLog("Authorization url: " + authorizationUrl);
}
this.windowHandle = window.open(
authorizationUrl,
@@ -63,12 +63,12 @@ export class OAuth2ClientPluginWeb extends WebPlugin implements OAuth2ClientPlug
if (href != null && href.indexOf(this.webOptions.redirectUrl) >= 0) {
if (this.webOptions.logsEnabled) {
- console.log("Url from Provider: " + href);
+ this.doLog("Url from Provider: " + href);
}
let authorizationRedirectUrlParamObj = WebUtils.getUrlParams(href);
if (authorizationRedirectUrlParamObj) {
if (this.webOptions.logsEnabled) {
- console.log("Url Params: ", authorizationRedirectUrlParamObj);
+ this.doLog("Authorization response:", authorizationRedirectUrlParamObj);
}
window.clearInterval(this.intervalId);
// check state
@@ -81,11 +81,15 @@ export class OAuth2ClientPluginWeb extends WebPlugin implements OAuth2ClientPlug
tokenRequest.onload = function () {
if (this.status === 200) {
let accessTokenResponse = JSON.parse(this.response);
+ if (self.webOptions.logsEnabled) {
+ self.doLog("Access token response:", accessTokenResponse);
+ }
self.requestResource(accessTokenResponse.access_token, resolve, reject, authorizationRedirectUrlParamObj, accessTokenResponse);
}
};
tokenRequest.onerror = function () {
- console.log("ERR_GENERAL: See client logs. It might be CORS. Status text: " + this.statusText);
+ // always log error because of CORS hint
+ self.doLog("ERR_GENERAL: See client logs. It might be CORS. Status text: " + this.statusText);
reject(new Error("ERR_GENERAL"));
};
tokenRequest.open("POST", this.webOptions.accessTokenEndpoint, true);
@@ -103,8 +107,8 @@ export class OAuth2ClientPluginWeb extends WebPlugin implements OAuth2ClientPlug
}
} else {
if (this.webOptions.logsEnabled) {
- console.log("State from web options: " + this.webOptions.state);
- console.log("State returned from provider: " + authorizationRedirectUrlParamObj.state);
+ this.doLog("State from web options: " + this.webOptions.state);
+ this.doLog("State returned from provider: " + authorizationRedirectUrlParamObj.state);
}
reject(new Error("ERR_STATES_NOT_MATCH"));
this.closeWindow();
@@ -118,30 +122,31 @@ export class OAuth2ClientPluginWeb extends WebPlugin implements OAuth2ClientPlug
});
}
+ private readonly MSG_RETURNED_TO_JS = "Returned to JS:";
+
private requestResource(accessToken: string, resolve: any, reject: (reason?: any) => void, authorizationResponse: any, accessTokenResponse: any = null) {
if (this.webOptions.resourceUrl) {
+ const logsEnabled = this.webOptions.logsEnabled;
+ if (logsEnabled) {
+ this.doLog("Resource url: " + this.webOptions.resourceUrl);
+ }
if (accessToken) {
- const logsEnabled = this.webOptions.logsEnabled;
if (logsEnabled) {
- console.log("Access token: " + accessToken);
+ this.doLog("Access token:", accessToken);
}
const self = this;
const request = new XMLHttpRequest();
request.onload = function () {
if (this.status === 200) {
let resp = JSON.parse(this.response);
+ if (logsEnabled) {
+ self.doLog("Resource response:", resp);
+ }
if (resp) {
- // #154
- if (authorizationResponse) {
- resp["authorization_response"] = authorizationResponse;
- }
- if (accessTokenResponse) {
- resp["access_token_response"] = authorizationResponse;
- }
- resp["access_token"] = accessToken;
+ self.assignResponses(resp, accessToken, authorizationResponse, accessTokenResponse);
}
if (logsEnabled) {
- console.log("Resource response: ", resp);
+ self.doLog(self.MSG_RETURNED_TO_JS, resp);
}
resolve(resp);
} else {
@@ -151,28 +156,49 @@ export class OAuth2ClientPluginWeb extends WebPlugin implements OAuth2ClientPlug
};
request.onerror = function () {
if (logsEnabled) {
- console.log("ERR_GENERAL: " + this.statusText);
+ self.doLog("ERR_GENERAL: " + this.statusText);
}
reject(new Error("ERR_GENERAL"));
self.closeWindow();
};
- if (logsEnabled) {
- console.log("Resource url: GET " + this.webOptions.resourceUrl);
- }
request.open("GET", this.webOptions.resourceUrl, true);
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
+ if (this.webOptions.additionalResourceHeaders) {
+ for (const key in this.webOptions.additionalResourceHeaders) {
+ request.setRequestHeader(key, this.webOptions.additionalResourceHeaders[key]);
+ }
+ }
request.send();
} else {
+ if (logsEnabled) {
+ this.doLog("No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config.");
+ }
reject(new Error("ERR_NO_ACCESS_TOKEN"));
this.closeWindow();
}
} else {
// if no resource url exists just return the accessToken response
- resolve(accessToken);
+ const resp = {};
+ this.assignResponses(resp, accessToken, authorizationResponse, accessTokenResponse);
+ if (this.webOptions.logsEnabled) {
+ this.doLog(this.MSG_RETURNED_TO_JS, resp);
+ }
+ resolve(resp);
this.closeWindow();
}
}
+ assignResponses(resp: any, accessToken: string, authorizationResponse: any, accessTokenResponse: any = null): void {
+ // #154
+ if (authorizationResponse) {
+ resp["authorization_response"] = authorizationResponse;
+ }
+ if (accessTokenResponse) {
+ resp["access_token_response"] = accessTokenResponse;
+ }
+ resp["access_token"] = accessToken;
+ }
+
async logout(options: OAuth2AuthenticateOptions): Promise {
return new Promise((resolve, _reject) => {
localStorage.removeItem(WebUtils.getAppId(options));
@@ -182,7 +208,15 @@ export class OAuth2ClientPluginWeb extends WebPlugin implements OAuth2ClientPlug
private closeWindow() {
window.clearInterval(this.intervalId);
+ // #164 if the provider's login page is opened in the same tab or window it must not be closed
+ // if (this.webOptions.windowTarget !== "_self") {
+ // this.windowHandle?.close();
+ // }
this.windowHandle?.close();
this.windowClosedByPlugin = true;
}
+
+ private doLog(msg: string, obj: any = null) {
+ console.log("I/Capacitor/OAuth2ClientPlugin: " + msg, obj);
+ }
}