From 2e048e9f04aa6a3361d5033ca345bcae4632c3c5 Mon Sep 17 00:00:00 2001 From: Tafel <35837839+tafelnl@users.noreply.github.com> Date: Fri, 3 May 2024 09:36:13 +0200 Subject: [PATCH] style: run `npm run fmt` (#259) --- .../community/genericoauth2/ConfigUtils.java | 84 +++++++-- .../genericoauth2/GenericOAuth2Plugin.java | 168 ++++++++++-------- .../genericoauth2/OAuth2Options.java | 2 - .../community/genericoauth2/OAuth2Utils.java | 8 +- .../genericoauth2/ResourceUrlAsyncTask.java | 29 +-- .../handler/AccessTokenCallback.java | 8 +- .../handler/OAuth2CustomHandler.java | 2 - .../genericoauth2/ConfigUtilsTest.java | 11 +- .../GenericOAuth2PluginTest.java | 38 ++-- ios/Plugin/GenericOAuth2Plugin.swift | 130 +++++++------- ios/Plugin/OAuth2SafariDelegate.swift | 6 +- src/definitions.ts | 8 +- src/web-utils.test.ts | 108 +++++++---- src/web-utils.ts | 162 +++++++++++------ src/web.ts | 135 +++++++++++--- 15 files changed, 571 insertions(+), 328 deletions(-) diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java index f14b9680..8ac716d0 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/ConfigUtils.java @@ -1,14 +1,12 @@ package com.getcapacitor.community.genericoauth2; import com.getcapacitor.JSObject; - -import org.json.JSONException; -import org.json.JSONObject; - import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Random; +import org.json.JSONException; +import org.json.JSONObject; public abstract class ConfigUtils { @@ -50,8 +48,7 @@ public static T getParam(Class clazz, JSObject data, String key, T defaul return defaultValue; } return (T) value; - } catch (Exception ignore) { - } + } catch (Exception ignore) {} } return defaultValue; } @@ -73,8 +70,7 @@ public static Map getParamMap(JSObject data, String key) { } catch (JSONException ignore) {} } } - } catch (Exception ignore) { - } + } catch (Exception ignore) {} } return map; } @@ -116,12 +112,70 @@ public static Map getOverwrittenAndroidParamMap(JSObject data, S } public static String getRandomString(int len) { - char[] ch = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', - 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', - 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', - 'w', 'x', 'y', 'z'}; + char[] ch = { + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z' + }; char[] c = new char[len]; Random random = new Random(); @@ -137,6 +191,4 @@ public static String trimToNull(String value) { } return value; } - - } diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java index 4e3eb068..eb2c9fd9 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/GenericOAuth2Plugin.java @@ -6,17 +6,16 @@ import android.net.Uri; import android.os.AsyncTask; import android.util.Log; - import androidx.activity.result.ActivityResult; - -import com.getcapacitor.community.genericoauth2.handler.AccessTokenCallback; -import com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler; import com.getcapacitor.JSObject; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; import com.getcapacitor.annotation.ActivityCallback; import com.getcapacitor.annotation.CapacitorPlugin; +import com.getcapacitor.community.genericoauth2.handler.AccessTokenCallback; +import com.getcapacitor.community.genericoauth2.handler.OAuth2CustomHandler; +import java.util.Map; import net.openid.appauth.AuthState; import net.openid.appauth.AuthorizationException; import net.openid.appauth.AuthorizationRequest; @@ -28,11 +27,8 @@ import net.openid.appauth.GrantTypeValues; import net.openid.appauth.TokenRequest; import net.openid.appauth.TokenResponse; - import org.json.JSONException; -import java.util.Map; - @CapacitorPlugin(name = "GenericOAuth2") public class GenericOAuth2Plugin extends Plugin { @@ -93,10 +89,9 @@ public class GenericOAuth2Plugin extends Plugin { private AuthState authState; private String callbackId; - public GenericOAuth2Plugin() { - } + public GenericOAuth2Plugin() {} - @PluginMethod() + @PluginMethod public void refreshToken(final PluginCall call) { disposeAuthService(); OAuth2RefreshTokenOptions oAuth2RefreshTokenOptions = buildRefreshTokenOptions(call.getData()); @@ -127,36 +122,36 @@ public void refreshToken(final PluginCall call) { this.authState = new AuthState(config); } - TokenRequest tokenRequest = new TokenRequest.Builder( - config, - oAuth2RefreshTokenOptions.getAppId() - ).setGrantType(GrantTypeValues.REFRESH_TOKEN) + TokenRequest tokenRequest = new TokenRequest.Builder(config, oAuth2RefreshTokenOptions.getAppId()) + .setGrantType(GrantTypeValues.REFRESH_TOKEN) .setScope(oAuth2RefreshTokenOptions.getScope()) .setRefreshToken(oAuth2RefreshTokenOptions.getRefreshToken()) .build(); - this.authService.performTokenRequest(tokenRequest, (response1, ex) -> { - this.authState.update(response1, ex); - if (ex != null) { - String message = ex.error != null ? ex.error : ERR_GENERAL; - call.reject(message, String.valueOf(ex.code), ex); - } else { - if (response1 != null) { - try { - JSObject json = new JSObject(response1.jsonSerializeString()); - call.resolve(json); - } catch (JSONException e) { - call.reject(ERR_GENERAL, e); + this.authService.performTokenRequest( + tokenRequest, + (response1, ex) -> { + this.authState.update(response1, ex); + if (ex != null) { + String message = ex.error != null ? ex.error : ERR_GENERAL; + call.reject(message, String.valueOf(ex.code), ex); + } else { + if (response1 != null) { + try { + JSObject json = new JSObject(response1.jsonSerializeString()); + call.resolve(json); + } catch (JSONException e) { + call.reject(ERR_GENERAL, e); + } + } else { + call.reject(ERR_NO_ACCESS_TOKEN); + } } - - } else { - call.reject(ERR_NO_ACCESS_TOKEN); } - } - }); + ); } - @PluginMethod() + @PluginMethod public void authenticate(final PluginCall call) { this.callbackId = call.getCallbackId(); disposeAuthService(); @@ -168,29 +163,32 @@ public void authenticate(final PluginCall call) { 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(), null, null).execute(accessToken); - } + handler.getAccessToken( + getActivity(), + call, + new AccessTokenCallback() { + @Override + public void onSuccess(String accessToken) { + new ResourceUrlAsyncTask(call, oauth2Options, getLogTag(), null, null).execute(accessToken); + } - @Override - public void onCancel() { - call.reject(USER_CANCELLED); - } + @Override + public void onCancel() { + call.reject(USER_CANCELLED); + } - @Override - public void onError(Exception error) { - call.reject(ERR_CUSTOM_HANDLER_LOGIN, error); + @Override + public void onError(Exception error) { + call.reject(ERR_CUSTOM_HANDLER_LOGIN, error); + } } - }); + ); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { call.reject(ERR_CUSTOM_HANDLER_LOGIN, e); } catch (Exception e) { call.reject(ERR_GENERAL, e); } } else { - // ################################### // ### Validate required parameter ### // ################################### @@ -287,7 +285,7 @@ public void onError(Exception error) { } } - @PluginMethod() + @PluginMethod public void logout(final PluginCall call) { String customHandlerClassname = ConfigUtils.getParam(String.class, call.getData(), PARAM_ANDROID_CUSTOM_HANDLER_CLASS); if (customHandlerClassname != null && customHandlerClassname.length() > 0) { @@ -328,11 +326,10 @@ public void logout(final PluginCall call) { AuthorizationServiceConfiguration config = new AuthorizationServiceConfiguration(authorizationUri, accessTokenUri); - EndSessionRequest endSessionRequest = - new EndSessionRequest.Builder(config) - .setIdTokenHint(idToken) - .setPostLogoutRedirectUri(logoutUri) - .build(); + EndSessionRequest endSessionRequest = new EndSessionRequest.Builder(config) + .setIdTokenHint(idToken) + .setPostLogoutRedirectUri(logoutUri) + .build(); this.authService = new AuthorizationService(getContext()); @@ -439,31 +436,36 @@ void handleAuthorizationRequestActivity(Intent intent, PluginCall savedCall) { TokenRequest tokenExchangeRequest; try { tokenExchangeRequest = authorizationResponse.createTokenExchangeRequest(); - this.authService.performTokenRequest(tokenExchangeRequest, (accessTokenResponse, exception) -> { - authState.update(accessTokenResponse, exception); - if (exception != null) { - savedCall.reject(ERR_AUTHORIZATION_FAILED, String.valueOf(exception.code), exception); - } else { - if (accessTokenResponse != null) { - if (oauth2Options.isLogsEnabled()) { - Log.i(getLogTag(), "Access token response:\n" + accessTokenResponse.jsonSerializeString()); + this.authService.performTokenRequest( + tokenExchangeRequest, + (accessTokenResponse, exception) -> { + authState.update(accessTokenResponse, exception); + if (exception != null) { + savedCall.reject(ERR_AUTHORIZATION_FAILED, String.valueOf(exception.code), exception); + } else { + if (accessTokenResponse != null) { + 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 { + resolveAuthorizationResponse(savedCall, authorizationResponse); + } } - authState.performActionWithFreshTokens(authService, - (accessToken, idToken, ex1) -> { - AsyncTask asyncTask = - new ResourceUrlAsyncTask( - savedCall, - oauth2Options, - getLogTag(), - authorizationResponse, - accessTokenResponse); - asyncTask.execute(accessToken); - }); - } else { - resolveAuthorizationResponse(savedCall, authorizationResponse); } - } - }); + ); } catch (Exception e) { savedCall.reject(ERR_NO_AUTHORIZATION_CODE, e); } @@ -491,7 +493,9 @@ OAuth2Options buildAuthenticateOptions(JSObject callData) { OAuth2Options o = new OAuth2Options(); // required o.setAppId(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_APP_ID))); - o.setAuthorizationBaseUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_AUTHORIZATION_BASE_URL))); + o.setAuthorizationBaseUrl( + ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_AUTHORIZATION_BASE_URL)) + ); o.setResponseType(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RESPONSE_TYPE))); o.setRedirectUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REDIRECT_URL))); @@ -499,7 +503,9 @@ OAuth2Options buildAuthenticateOptions(JSObject callData) { Boolean logsEnabled = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, callData, PARAM_LOGS_ENABLED); o.setLogsEnabled(logsEnabled != null && logsEnabled); o.setResourceUrl(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_RESOURCE_URL))); - o.setAccessTokenEndpoint(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT))); + o.setAccessTokenEndpoint( + ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT)) + ); Boolean pkceEnabledObj = ConfigUtils.getOverwrittenAndroidParam(Boolean.class, callData, PARAM_PKCE_ENABLED); o.setPkceEnabled(pkceEnabledObj != null && pkceEnabledObj); if (o.isPkceEnabled()) { @@ -533,7 +539,9 @@ OAuth2Options buildAuthenticateOptions(JSObject callData) { // 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)); - o.setHandleResultOnActivityResult(ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_ACTIVITY_RESULT, false)); + o.setHandleResultOnActivityResult( + ConfigUtils.getParam(Boolean.class, callData, PARAM_ANDROID_HANDLE_RESULT_ON_ACTIVITY_RESULT, false) + ); if (!o.isHandleResultOnNewIntent() && !o.isHandleResultOnActivityResult()) { o.setHandleResultOnActivityResult(true); } @@ -543,7 +551,9 @@ OAuth2Options buildAuthenticateOptions(JSObject callData) { OAuth2RefreshTokenOptions buildRefreshTokenOptions(JSObject callData) { OAuth2RefreshTokenOptions o = new OAuth2RefreshTokenOptions(); o.setAppId(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_APP_ID))); - o.setAccessTokenEndpoint(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT))); + o.setAccessTokenEndpoint( + ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_ACCESS_TOKEN_ENDPOINT)) + ); o.setScope(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_SCOPE))); o.setRefreshToken(ConfigUtils.trimToNull(ConfigUtils.getOverwrittenAndroidParam(String.class, callData, PARAM_REFRESH_TOKEN))); return o; diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java index 02badc2a..9bd064a0 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Options.java @@ -35,7 +35,6 @@ public class OAuth2Options { private String logoutUrl; - public String getAppId() { return appId; } @@ -181,7 +180,6 @@ public void setResponseMode(String responseMode) { this.responseMode = responseMode; } - public boolean isHandleResultOnNewIntent() { return handleResultOnNewIntent; } diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java index 9b14e6f2..f6052e33 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/OAuth2Utils.java @@ -1,13 +1,17 @@ package com.getcapacitor.community.genericoauth2; 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) { + public static void assignResponses( + JSObject resp, + String accessToken, + AuthorizationResponse authorizationResponse, + TokenResponse accessTokenResponse + ) { // #154 if (authorizationResponse != null) { resp.put("authorization_response", authorizationResponse.jsonSerialize()); diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java index 4e0c992f..80db0409 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/ResourceUrlAsyncTask.java @@ -4,12 +4,6 @@ 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; import java.io.IOException; import java.io.InputStream; @@ -18,6 +12,9 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Map; +import net.openid.appauth.AuthorizationResponse; +import net.openid.appauth.TokenResponse; +import org.json.JSONException; public class ResourceUrlAsyncTask extends AsyncTask { @@ -31,7 +28,13 @@ public class ResourceUrlAsyncTask extends AsyncTask= HttpURLConnection.HTTP_OK - && conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE) { + if ( + conn.getResponseCode() >= HttpURLConnection.HTTP_OK && + conn.getResponseCode() < HttpURLConnection.HTTP_MULT_CHOICE + ) { is = conn.getInputStream(); } else { is = conn.getErrorStream(); @@ -101,7 +106,10 @@ protected ResourceCallResult doInBackground(String... tokens) { } } else { if (options.isLogsEnabled()) { - Log.i(logTag, "No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config."); + Log.i( + logTag, + "No accessToken was provided although you configured a resourceUrl. Remove the resourceUrl from the config." + ); } pluginCall.reject(ERR_NO_ACCESS_TOKEN); } @@ -141,5 +149,4 @@ private static String readInputStream(InputStream in) throws IOException { return sb.toString(); } } - } diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java index 4ffe86eb..688ab3c0 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/handler/AccessTokenCallback.java @@ -1,11 +1,9 @@ package com.getcapacitor.community.genericoauth2.handler; public interface AccessTokenCallback { + void onSuccess(String accessToken); - void onSuccess(String accessToken); - - void onCancel(); - - void onError(Exception error); + void onCancel(); + void onError(Exception error); } diff --git a/android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java b/android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java index 1bb48d0e..6504d389 100644 --- a/android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java +++ b/android/src/main/java/com/getcapacitor/community/genericoauth2/handler/OAuth2CustomHandler.java @@ -4,9 +4,7 @@ import com.getcapacitor.PluginCall; public interface OAuth2CustomHandler { - void getAccessToken(Activity activity, PluginCall pluginCall, final AccessTokenCallback callback); boolean logout(Activity activity, PluginCall pluginCall); - } diff --git a/android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java b/android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java index f845d5e5..651e12a1 100644 --- a/android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java +++ b/android/src/test/java/com/getcapacitor/community/genericoauth2/ConfigUtilsTest.java @@ -1,9 +1,9 @@ package com.getcapacitor.community.genericoauth2; import android.util.Log; - import com.getcapacitor.JSObject; - +import java.util.Map; +import java.util.stream.Stream; import org.json.JSONException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; @@ -12,12 +12,10 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import java.util.Map; -import java.util.stream.Stream; - public class ConfigUtilsTest { - private final static String BASE_JSON = "{\n" + + private static final String BASE_JSON = + "{\n" + " \"doubleValue\": 123.4567,\n" + " \"floatValue\": 123.4,\n" + " \"intValue\": 1,\n" + @@ -108,7 +106,6 @@ public void getOverwrittenAndroidParam() { String overwrittenString = ConfigUtils.getOverwrittenAndroidParam(String.class, jsObject, "stringValue"); Assertions.assertEquals("stringAndroid", overwrittenString); - int intValue = ConfigUtils.getOverwrittenAndroidParam(Integer.class, jsObject, "intValue"); Assertions.assertEquals(1, intValue); } diff --git a/android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java b/android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java index 6059458c..b0c3048e 100644 --- a/android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java +++ b/android/src/test/java/com/getcapacitor/community/genericoauth2/GenericOAuth2PluginTest.java @@ -1,9 +1,7 @@ package com.getcapacitor.community.genericoauth2; import android.util.Log; - import com.getcapacitor.JSObject; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -20,7 +18,8 @@ public void setup() { @Test public void allBooleanValues() { - JSObject jsObject = loadJson("{\n" + + JSObject jsObject = loadJson( + "{\n" + " \"appId\": \"CLIENT_ID\",\n" + " \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" + " \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" + @@ -33,7 +32,9 @@ public void allBooleanValues() { " \"windowOptions\": \"height=600,left=0,top=0\"\n" + " },\n" + " \"android\": {\n" + - " \"appId\": \"" + CLIENT_ID_ANDROID + "\",\n" + + " \"appId\": \"" + + CLIENT_ID_ANDROID + + "\",\n" + " \"redirectUrl\": \"com.company.project:/\",\n" + " \"handleResultMethod\": \"TEST\",\n" + " \"logsEnabled\": false,\n" + @@ -46,7 +47,8 @@ public void allBooleanValues() { " \"responseType\": \"code\",\n" + " \"redirectUrl\": \"com.company.project:/\"\n" + " }\n" + - "}\n"); + "}\n" + ); OAuth2Options options = plugin.buildAuthenticateOptions(jsObject); Assertions.assertNotNull(options); Assertions.assertTrue(options.isPkceEnabled()); @@ -57,7 +59,8 @@ public void allBooleanValues() { @Test public void responseTypeToken() { - JSObject jsObject = loadJson("{\n" + + JSObject jsObject = loadJson( + "{\n" + " \"appId\": \"CLIENT_ID\",\n" + " \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" + " \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" + @@ -69,7 +72,9 @@ public void responseTypeToken() { " \"windowOptions\": \"height=600,left=0,top=0\"\n" + " },\n" + " \"android\": {\n" + - " \"appId\": \"" + CLIENT_ID_ANDROID + "\",\n" + + " \"appId\": \"" + + CLIENT_ID_ANDROID + + "\",\n" + " \"redirectUrl\": \"com.company.project:/\",\n" + " \"handleResultMethod\": \"TEST\",\n" + " \"responseType\": \"TOKEN\"\n" + @@ -79,7 +84,8 @@ public void responseTypeToken() { " \"responseType\": \"code\",\n" + " \"redirectUrl\": \"com.company.project:/\"\n" + " }\n" + - "}\n"); + "}\n" + ); OAuth2Options options = plugin.buildAuthenticateOptions(jsObject); Assertions.assertNotNull(options); Assertions.assertEquals(CLIENT_ID_ANDROID, options.getAppId()); @@ -89,7 +95,8 @@ public void responseTypeToken() { @Test public void serverAuthorizationHandling() { - JSObject jsObject = loadJson("{\n" + + JSObject jsObject = loadJson( + "{\n" + " \"appId\": \"CLIENT_ID\",\n" + " \"authorizationBaseUrl\": \"https://accounts.google.com/o/oauth2/auth\",\n" + " \"responseType\": \"code id_token\",\n" + @@ -100,12 +107,15 @@ public void serverAuthorizationHandling() { " \"windowOptions\": \"height=600,left=0,top=0\"\n" + " },\n" + " \"android\": {\n" + - " \"appId\": \"" + CLIENT_ID_ANDROID + "\"\n" + + " \"appId\": \"" + + CLIENT_ID_ANDROID + + "\"\n" + " },\n" + " \"ios\": {\n" + " \"appId\": \"CLIENT_ID_IOS\"\n" + " }\n" + - "}\n"); + "}\n" + ); OAuth2Options options = plugin.buildAuthenticateOptions(jsObject); Assertions.assertNotNull(options.getAppId()); Assertions.assertEquals(CLIENT_ID_ANDROID, options.getAppId()); @@ -116,12 +126,14 @@ public void serverAuthorizationHandling() { @Test public void buildRefreshTokenOptions() { - JSObject jsObject = loadJson("{\n" + + JSObject jsObject = loadJson( + "{\n" + " \"appId\": \"CLIENT_ID\",\n" + " \"accessTokenEndpoint\": \"https://www.googleapis.com/oauth2/v4/token\",\n" + " \"refreshToken\": \"ss4f6sd5f4\",\n" + " \"scope\": \"email profile\"\n" + - "}"); + "}" + ); OAuth2RefreshTokenOptions options = plugin.buildRefreshTokenOptions(jsObject); Assertions.assertNotNull(options); Assertions.assertNotNull(options.getAppId()); diff --git a/ios/Plugin/GenericOAuth2Plugin.swift b/ios/Plugin/GenericOAuth2Plugin.swift index b6f3f966..944fbd11 100644 --- a/ios/Plugin/GenericOAuth2Plugin.swift +++ b/ios/Plugin/GenericOAuth2Plugin.swift @@ -4,7 +4,7 @@ import OAuthSwift import CommonCrypto import AuthenticationServices -typealias JSObject = [String:Any] +typealias JSObject = [String: Any] /** * Please read the Capacitor iOS Plugin Development Guide @@ -88,7 +88,7 @@ public class GenericOAuth2Plugin: CAPPlugin { classes.deallocate() } - public override func load() { + override public func load() { NotificationCenter.default.addObserver(self, selector: #selector(self.handleRedirect(notification:)), name: .capacitorOpenURL, object: nil) registerHandlers() } @@ -100,7 +100,7 @@ public class GenericOAuth2Plugin: CAPPlugin { guard let url = object["url"] as? URL else { return } - OAuth2Swift.handle(url: url); + OAuth2Swift.handle(url: url) } /* @@ -132,11 +132,11 @@ public class GenericOAuth2Plugin: CAPPlugin { self.oauthSwift = oauthSwift - let scope = getOverwritableString(call, PARAM_SCOPE) ?? nil; - var parameters: OAuthSwift.Parameters = [:]; + let scope = getOverwritableString(call, PARAM_SCOPE) ?? nil + var parameters: OAuthSwift.Parameters = [:] if scope != nil { - parameters["scope"] = scope; + parameters["scope"] = scope } oauthSwift.renewAccessToken(withRefreshToken: refreshToken, parameters: parameters) { result in @@ -151,27 +151,27 @@ public class GenericOAuth2Plugin: CAPPlugin { } case .failure(let error): switch error { - case .cancelled, .accessDenied(_, _): + case .cancelled, .accessDenied: call.reject(SharedConstants.ERR_USER_CANCELLED) - case .stateNotEqual( _, _): + case .stateNotEqual: self.log("The given state does not match the one in the respond!") call.reject(self.ERR_STATES_NOT_MATCH) case .requestError(let underlyingError, _): - let nsError = (underlyingError as NSError); - let errorCode = nsError.code; - let responseBodyString = (nsError.userInfo["Response-Body"]) as? String; - self.log("Authorization failed with requestError \(responseBodyString ?? "")"); + let nsError = (underlyingError as NSError) + let errorCode = nsError.code + let responseBodyString = (nsError.userInfo["Response-Body"]) as? String + self.log("Authorization failed with requestError \(responseBodyString ?? "")") do { - let responseBody = Data((responseBodyString ?? "").utf8); + let responseBody = Data((responseBodyString ?? "").utf8) if let json = try JSONSerialization.jsonObject(with: responseBody, options: []) as? [String: Any] { call.reject(json["error"] as? String ?? self.ERR_GENERAL, String(errorCode), underlyingError, json) } - } catch { + } catch { call.reject(self.ERR_GENERAL, String(errorCode), underlyingError) } default: - self.log("Authorization failed with \(error.localizedDescription)"); + self.log("Authorization failed with \(error.localizedDescription)") call.reject(self.ERR_AUTHORIZATION_FAILED) } } @@ -219,7 +219,7 @@ public class GenericOAuth2Plugin: CAPPlugin { } case .failure(let error): self.log("Resource url request error '\(error)'") - call.reject(self.ERR_CUSTOM_HANDLER_LOGIN); + call.reject(self.ERR_CUSTOM_HANDLER_LOGIN) } } } else { @@ -260,7 +260,6 @@ public class GenericOAuth2Plugin: CAPPlugin { return } - var oauthSwift: OAuth2Swift if let accessTokenEndpoint = getOverwritableString(call, PARAM_ACCESS_TOKEN_ENDPOINT), !accessTokenEndpoint.isEmpty { oauthSwift = OAuth2Swift( @@ -287,7 +286,7 @@ public class GenericOAuth2Plugin: CAPPlugin { // additional parameters #18 let callParameter: [String: Any] = getOverwritable(call, PARAM_ADDITIONAL_PARAMETERS) as? [String: Any] ?? [:] - let additionalParameters = buildStringDict(callParameter); + let additionalParameters = buildStringDict(callParameter) let requestState = getOverwritableString(call, PARAM_STATE) ?? generateRandom(withLength: 20) let pkceEnabled: Bool = getOverwritable(call, PARAM_PKCE_ENABLED) as? Bool ?? false @@ -303,7 +302,7 @@ public class GenericOAuth2Plugin: CAPPlugin { codeChallenge: pkceCodeChallenge, codeVerifier: pkceCodeVerifier, parameters: additionalParameters) { result in - self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl) + self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl) } } else { oauthSwift.authorize( @@ -311,12 +310,11 @@ public class GenericOAuth2Plugin: CAPPlugin { scope: getOverwritableString(call, PARAM_SCOPE) ?? "", state: requestState, parameters: additionalParameters) { result in - self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl) + self.handleAuthorizationResult(result, call, responseType, requestState, logsEnabled, resourceUrl) } } } - } } @@ -328,7 +326,7 @@ public class GenericOAuth2Plugin: CAPPlugin { if let handlerInstance = self.getOrLoadHandlerInstance(className: handlerClassName) { let success: Bool! = handlerInstance.logout(viewController: (bridge?.viewController!)!, call: call) if success { - call.resolve(); + call.resolve() } else { self.log("Custom handler logout failed!") call.reject(self.ERR_CUSTOM_HANDLER_LOGOUT) @@ -369,42 +367,42 @@ public class GenericOAuth2Plugin: CAPPlugin { } // resource url request headers let callParameter: [String: Any] = getOverwritable(call, PARAM_ADDITIONAL_RESOURCE_HEADERS) as? [String: Any] ?? [:] - let additionalHeadersDict = buildStringDict(callParameter); + let additionalHeadersDict = buildStringDict(callParameter) self.oauthSwift!.client.get(resourceUrl!, headers: additionalHeadersDict) { result in - switch result { - case .success(let resourceResponse): - do { - 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) - } + switch result { + case .success(let resourceResponse): + do { + if logsEnabled { + self.logDataObj("Resource response:", resourceResponse.data) + } - jsonObj.updateValue(credential.oauthToken, forKey: self.JSON_KEY_ACCESS_TOKEN) + 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) + } - if logsEnabled { - self.log("Returned to JS:\n\(jsonObj)") - } + jsonObj.updateValue(credential.oauthToken, forKey: self.JSON_KEY_ACCESS_TOKEN) - call.resolve(jsonObj) - } catch { - self.log("Invalid json in resource response:\n \(error.localizedDescription)") - call.reject(self.ERR_GENERAL) + if logsEnabled { + self.log("Returned to JS:\n\(jsonObj)") } - case .failure(let error): - self.log("Resource url request failed:\n\(error.description)"); + + call.resolve(jsonObj) + } catch { + self.log("Invalid json in resource response:\n \(error.localizedDescription)") call.reject(self.ERR_GENERAL) } + case .failure(let error): + self.log("Resource url request failed:\n\(error.description)") + call.reject(self.ERR_GENERAL) + } } - // no resource url + // no resource url } else if let responseData = response?.data { do { var jsonObj = JSObject() @@ -427,25 +425,25 @@ public class GenericOAuth2Plugin: CAPPlugin { } case .failure(let error): switch error { - case .cancelled, .accessDenied(_, _): + case .cancelled, .accessDenied: call.reject(SharedConstants.ERR_USER_CANCELLED) - case .stateNotEqual(_, _): + case .stateNotEqual: self.log("The given state does not match the one in the respond!") call.reject(self.ERR_STATES_NOT_MATCH) default: - self.log("Authorization failed with \(error.localizedDescription)"); + self.log("Authorization failed with \(error.localizedDescription)") call.reject(self.ERR_NO_AUTHORIZATION_CODE) } } } - private func getConfigObjectDeepest(_ options: [AnyHashable: Any?]!, key: String) -> [AnyHashable:Any?]? { + private func getConfigObjectDeepest(_ options: [AnyHashable: Any?]!, key: String) -> [AnyHashable: Any?]? { let parts = key.split(separator: ".") var o = options for (_, k) in parts[0.. Any? { @@ -474,7 +472,7 @@ public class GenericOAuth2Plugin: CAPPlugin { if ios != nil { base = ios } - return base; + return base } private func getValue(_ call: CAPPluginCall, _ key: String) -> Any? { @@ -511,18 +509,18 @@ public class GenericOAuth2Plugin: CAPPlugin { log("\(msg)\n\(json ?? "")") } - private func buildStringDict(_ callParameter: [String: Any]) -> [String: String] { + 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; + let str = value as! String if !str.isEmpty { dict[key] = str } } } - return dict; + return dict } private func loadHandlerInstance(className: String) -> OAuth2CustomHandler? { @@ -558,7 +556,7 @@ public class GenericOAuth2Plugin: CAPPlugin { extension String { func sha256() -> Data { let data = self.data(using: .utf8)! - var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) + var buffer = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH)) data.withUnsafeBytes { _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &buffer) } @@ -587,11 +585,11 @@ extension GenericOAuth2Plugin: ASAuthorizationControllerDelegate { if let _: Bool = getValue(call, PARAM_IOS_USE_SCOPE) as? Bool { if let scopeStr = getOverwritableString(call, PARAM_SCOPE), !scopeStr.isEmpty { - var scopeArr: Array = [] + var scopeArr: [ASAuthorization.Scope] = [] if scopeStr.localizedCaseInsensitiveContains("fullName") - || scopeStr.localizedCaseInsensitiveContains("name") { - scopeArr.append(.fullName) - } + || scopeStr.localizedCaseInsensitiveContains("name") { + scopeArr.append(.fullName) + } if scopeStr.localizedCaseInsensitiveContains("email") { scopeArr.append(.email) @@ -634,7 +632,7 @@ extension GenericOAuth2Plugin: ASAuthorizationControllerDelegate { "state": appleIDCredential.state as Any, "id_token": String(data: appleIDCredential.identityToken!, encoding: .utf8) as Any, "code": String(data: appleIDCredential.authorizationCode!, encoding: .utf8) as Any - ] as [String : Any] + ] as [String: Any] self.savedPluginCall?.resolve(result as PluginCallResultData) default: self.log("SIWA: Authorization failed!") @@ -669,6 +667,4 @@ extension GenericOAuth2Plugin: ASAuthorizationControllerDelegate { } } - - -} \ No newline at end of file +} diff --git a/ios/Plugin/OAuth2SafariDelegate.swift b/ios/Plugin/OAuth2SafariDelegate.swift index 946c11ca..849e5392 100644 --- a/ios/Plugin/OAuth2SafariDelegate.swift +++ b/ios/Plugin/OAuth2SafariDelegate.swift @@ -3,13 +3,13 @@ import Capacitor import SafariServices class OAuth2SafariDelegate: NSObject, SFSafariViewControllerDelegate { - + var pluginCall: CAPPluginCall - + init(_ call: CAPPluginCall) { self.pluginCall = call } - + func safariViewControllerDidFinish(_ controller: SFSafariViewController) { self.pluginCall.reject(GenericOAuth2Plugin.SharedConstants.ERR_USER_CANCELLED) } diff --git a/src/definitions.ts b/src/definitions.ts index 1d775b3e..00401fc8 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -17,7 +17,10 @@ export interface GenericOAuth2Plugin { * @param {String} id_token Optional idToken, only for Android * @returns {Promise} true if the logout was successful else false. */ - logout(options: OAuth2AuthenticateOptions, id_token?: string): Promise; + logout( + options: OAuth2AuthenticateOptions, + id_token?: string, + ): Promise; } export interface OAuth2RefreshTokenOptions { @@ -108,7 +111,8 @@ export interface OAuth2AuthenticateBaseOptions { additionalResourceHeaders?: { [key: string]: string }; } -export interface OAuth2AuthenticateOptions extends OAuth2AuthenticateBaseOptions { +export interface OAuth2AuthenticateOptions + extends OAuth2AuthenticateBaseOptions { /** * Custom options for the platform "web" */ diff --git a/src/web-utils.test.ts b/src/web-utils.test.ts index 2062ac5d..e9be3767 100644 --- a/src/web-utils.test.ts +++ b/src/web-utils.test.ts @@ -1,4 +1,5 @@ -import { OAuth2AuthenticateOptions } from './definitions'; +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { OAuth2AuthenticateOptions } from './definitions'; import { CryptoUtils, WebUtils } from './web-utils'; const mGetRandomValues = jest.fn().mockReturnValueOnce(new Uint32Array(10)); @@ -29,8 +30,10 @@ const googleOptions: OAuth2AuthenticateOptions = { const oneDriveOptions: OAuth2AuthenticateOptions = { appId: 'appId', - authorizationBaseUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', - accessTokenEndpoint: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + 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', additionalParameters: { @@ -40,10 +43,10 @@ const oneDriveOptions: OAuth2AuthenticateOptions = { redirectUrl: 'https://oauth2.byteowls.com/authorize', pkceEnabled: false, additionalParameters: { - resource: 'resource_id', - emptyParam: null!, + 'resource': 'resource_id', + 'emptyParam': null!, ' ': 'test', - nonce: WebUtils.randomString(10), + 'nonce': WebUtils.randomString(10), }, }, android: { @@ -56,7 +59,8 @@ const oneDriveOptions: OAuth2AuthenticateOptions = { const redirectUrlOptions: OAuth2AuthenticateOptions = { appId: 'appId', - authorizationBaseUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + authorizationBaseUrl: + 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', responseType: 'code', redirectUrl: 'https://mycompany.server.com/oauth', scope: 'files.readwrite offline_access', @@ -84,58 +88,68 @@ describe('base options processing', () => { }); it('should build a overwritable boolean value', () => { - const pkceEnabled = WebUtils.getOverwritableValue(googleOptions, 'pkceEnabled'); + const pkceEnabled = WebUtils.getOverwritableValue( + googleOptions, + 'pkceEnabled', + ); expect(pkceEnabled).toBeTruthy(); }); it('should build a overwritable additional parameters map', () => { - const additionalParameters = WebUtils.getOverwritableValue<{ [key: string]: string }>( - oneDriveOptions, - 'additionalParameters', - ); + const additionalParameters = WebUtils.getOverwritableValue<{ + [key: string]: string; + }>(oneDriveOptions, 'additionalParameters'); expect(additionalParameters).not.toBeUndefined(); expect(additionalParameters['resource']).toEqual('resource_id'); }); it('must not contain overwritten additional parameters', () => { - const additionalParameters = WebUtils.getOverwritableValue<{ [key: string]: string }>( - oneDriveOptions, - 'additionalParameters', - ); + const additionalParameters = WebUtils.getOverwritableValue<{ + [key: string]: string; + }>(oneDriveOptions, 'additionalParameters'); expect(additionalParameters['willbeoverwritten']).toBeUndefined(); }); it('must have a base redirect url', () => { - const redirectUrl = WebUtils.getOverwritableValue(redirectUrlOptions, 'redirectUrl'); + const redirectUrl = WebUtils.getOverwritableValue( + redirectUrlOptions, + 'redirectUrl', + ); expect(redirectUrl).toBeDefined(); }); it('must be overwritten by empty string from web section', () => { - const accessTokenEndpoint = WebUtils.getOverwritableValue(googleOptions, 'accessTokenEndpoint'); + const accessTokenEndpoint = WebUtils.getOverwritableValue( + googleOptions, + 'accessTokenEndpoint', + ); expect(accessTokenEndpoint).toStrictEqual(''); }); it('must not be overwritten if no key exists in web section', () => { - const accessTokenEndpoint = WebUtils.getOverwritableValue(googleOptions, 'scope'); + const accessTokenEndpoint = WebUtils.getOverwritableValue( + googleOptions, + 'scope', + ); expect(accessTokenEndpoint).toStrictEqual('email profile'); }); }); describe('web options', () => { it('should build web options', async () => { - WebUtils.buildWebOptions(oneDriveOptions).then((webOptions) => { + WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { expect(webOptions).not.toBeNull(); }); }); it('should not have a code verifier', async () => { - WebUtils.buildWebOptions(oneDriveOptions).then((webOptions) => { + WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { expect(webOptions.pkceCodeVerifier).toBeUndefined(); }); }); it('must not contain empty additional parameter', async () => { - WebUtils.buildWebOptions(oneDriveOptions).then((webOptions) => { + WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { expect(webOptions.additionalParameters[' ']).toBeUndefined(); expect(webOptions.additionalParameters['emptyParam']).toBeUndefined(); }); @@ -179,25 +193,33 @@ describe('Url param extraction', () => { }); it('should remove invalid combinations multiple param', () => { - const paramObj = WebUtils.getUrlParams('https://app.example.com?=test&key1=param1'); + const paramObj = WebUtils.getUrlParams( + 'https://app.example.com?=test&key1=param1', + ); expect(paramObj).toEqual({ key1: 'param1' }); }); it('should extract work with a single param', () => { - const paramObj = WebUtils.getUrlParams('https://app.example.com?access_token=testtoken'); + const paramObj = WebUtils.getUrlParams( + 'https://app.example.com?access_token=testtoken', + ); expect(paramObj!['access_token']).toStrictEqual('testtoken'); }); it('should extract a uuid state param', () => { const state = WebUtils.randomString(); - const paramObj = WebUtils.getUrlParams(`https://app.example.com?state=${state}&access_token=testtoken`); + const paramObj = WebUtils.getUrlParams( + `https://app.example.com?state=${state}&access_token=testtoken`, + ); expect(paramObj!['state']).toStrictEqual(state); }); it('should use query flag and ignore hash flag', () => { const random = WebUtils.randomString(); const foo = WebUtils.randomString(); - const paramObj = WebUtils.getUrlParams(`https://app.example.com?random=${random}&foo=${foo}#ignored`); + const paramObj = WebUtils.getUrlParams( + `https://app.example.com?random=${random}&foo=${foo}#ignored`, + ); expect(paramObj!['random']).toStrictEqual(random); expect(paramObj!['foo']).toStrictEqual(foo); }); @@ -205,7 +227,9 @@ describe('Url param extraction', () => { it('should use query flag with another question mark in a param', () => { const random = WebUtils.randomString(); const foo = WebUtils.randomString(); - const paramObj = WebUtils.getUrlParams(`https://app.example.com?random=${random}&foo=${foo}?questionmark`); + const paramObj = WebUtils.getUrlParams( + `https://app.example.com?random=${random}&foo=${foo}?questionmark`, + ); expect(paramObj!['random']).toStrictEqual(random); expect(paramObj!['foo']).toStrictEqual(`${foo}?questionmark`); }); @@ -213,7 +237,9 @@ describe('Url param extraction', () => { it('should use hash flag and ignore query flag', () => { const random = WebUtils.randomString(); const foo = WebUtils.randomString(); - const paramObj = WebUtils.getUrlParams(`https://app.example.com#random=${random}&foo=${foo}?ignored`); + const paramObj = WebUtils.getUrlParams( + `https://app.example.com#random=${random}&foo=${foo}?ignored`, + ); expect(paramObj!['random']).toStrictEqual(random); expect(paramObj!['foo']).toStrictEqual(`${foo}?ignored`); }); @@ -221,7 +247,9 @@ describe('Url param extraction', () => { it('should use hash flag with another hash in a param', () => { const random = WebUtils.randomString(); const foo = WebUtils.randomString(); - const paramObj = WebUtils.getUrlParams(`https://app.example.com#random=${random}&foo=${foo}#hash`); + const paramObj = WebUtils.getUrlParams( + `https://app.example.com#random=${random}&foo=${foo}#hash`, + ); expect(paramObj!['random']).toStrictEqual(random); expect(paramObj!['foo']).toStrictEqual(`${foo}#hash`); }); @@ -262,7 +290,7 @@ describe('Random string gen', () => { describe('Authorization url building', () => { it('should contain a nonce param', async () => { - WebUtils.buildWebOptions(oneDriveOptions).then((webOptions) => { + WebUtils.buildWebOptions(oneDriveOptions).then(webOptions => { const authorizationUrl = WebUtils.getAuthorizationUrl(webOptions); expect(authorizationUrl).toContain('nonce'); }); @@ -271,25 +299,25 @@ describe('Authorization url building', () => { describe('Crypto utils', () => { it('base 64 simple', () => { - let arr: Uint8Array = CryptoUtils.toUint8Array('tester'); - let expected = CryptoUtils.toBase64(arr); + const arr: Uint8Array = CryptoUtils.toUint8Array('tester'); + const expected = CryptoUtils.toBase64(arr); expect(expected).toEqual('dGVzdGVy'); }); it('base 64 special char', () => { - let arr: Uint8Array = CryptoUtils.toUint8Array('testerposfieppw2874929'); - let expected = CryptoUtils.toBase64(arr); + const arr: Uint8Array = CryptoUtils.toUint8Array('testerposfieppw2874929'); + const expected = CryptoUtils.toBase64(arr); expect(expected).toEqual('dGVzdGVycG9zZmllcHB3Mjg3NDkyOQ=='); }); it('base 64 with space', () => { - let arr: Uint8Array = CryptoUtils.toUint8Array('base64 encoder'); - let expected = CryptoUtils.toBase64(arr); + const arr: Uint8Array = CryptoUtils.toUint8Array('base64 encoder'); + const expected = CryptoUtils.toBase64(arr); expect(expected).toEqual('YmFzZTY0IGVuY29kZXI='); }); it('base64url safe all base64 special chars included', () => { - let expected = CryptoUtils.toBase64Url('YmFz+TY0IG/uY29kZXI='); + const expected = CryptoUtils.toBase64Url('YmFz+TY0IG/uY29kZXI='); expect(expected).toEqual('YmFz-TY0IG_uY29kZXI'); }); }); @@ -299,8 +327,10 @@ describe('additional resource headers', () => { 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', + 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: { diff --git a/src/web-utils.ts b/src/web-utils.ts index 089ad3c4..cf36ffcc 100644 --- a/src/web-utils.ts +++ b/src/web-utils.ts @@ -1,4 +1,4 @@ -import { OAuth2AuthenticateOptions } from './definitions'; +import type { OAuth2AuthenticateOptions } from './definitions'; // import sha256 from "fast-sha256"; export class WebUtils { @@ -9,7 +9,10 @@ export class WebUtils { return this.getOverwritableValue(options, 'appId'); } - static getOverwritableValue(options: OAuth2AuthenticateOptions | any, key: string): T { + static getOverwritableValue( + options: OAuth2AuthenticateOptions | any, + key: string, + ): T { let base = options[key]; if (options.web && key in options.web) { base = options.web[key]; @@ -47,11 +50,26 @@ export class WebUtils { static getTokenEndpointData(options: WebOptions, code: string): string { let body = ''; - body += encodeURIComponent('grant_type') + '=' + encodeURIComponent('authorization_code') + '&'; - body += encodeURIComponent('client_id') + '=' + encodeURIComponent(options.appId) + '&'; - body += encodeURIComponent('redirect_uri') + '=' + encodeURIComponent(options.redirectUrl) + '&'; + body += + encodeURIComponent('grant_type') + + '=' + + encodeURIComponent('authorization_code') + + '&'; + body += + encodeURIComponent('client_id') + + '=' + + encodeURIComponent(options.appId) + + '&'; + body += + encodeURIComponent('redirect_uri') + + '=' + + encodeURIComponent(options.redirectUrl) + + '&'; body += encodeURIComponent('code') + '=' + encodeURIComponent(code) + '&'; - body += encodeURIComponent('code_verifier') + '=' + encodeURIComponent(options.pkceCodeVerifier); + body += + encodeURIComponent('code_verifier') + + '=' + + encodeURIComponent(options.pkceCodeVerifier); return body; } @@ -78,61 +96,90 @@ export class WebUtils { } const keyValuePairs: string[] = urlParamStr.split(`&`); - // @ts-ignore - return keyValuePairs.reduce((accumulator, currentValue) => { - const [key, val] = currentValue.split(`=`); - if (key && key.length > 0) { - return { - ...accumulator, - [key]: decodeURIComponent(val), - }; - } - }, {}); + return keyValuePairs.reduce<{ [x: string]: string } | undefined>( + (accumulator, currentValue) => { + const [key, val] = currentValue.split(`=`); + if (key && key.length > 0) { + return { + ...accumulator, + [key]: decodeURIComponent(val), + }; + } + }, + {}, + ); } - static randomString(length: number = 10) { - const haystack = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + static randomString(length = 10): string { + const haystack = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; let randomStr; if (window.crypto) { let numberArray: Uint32Array = new Uint32Array(length); window.crypto.getRandomValues(numberArray); - numberArray = numberArray.map((x) => haystack.charCodeAt(x % haystack.length)); + numberArray = numberArray.map(x => + haystack.charCodeAt(x % haystack.length), + ); - let stringArray: string[] = []; - numberArray.forEach((x) => { + const stringArray: string[] = []; + numberArray.forEach(x => { stringArray.push(haystack.charAt(x % haystack.length)); }); randomStr = stringArray.join(''); } else { randomStr = ''; for (let i = 0; i < length; i++) { - randomStr += haystack.charAt(Math.floor(Math.random() * haystack.length)); + randomStr += haystack.charAt( + Math.floor(Math.random() * haystack.length), + ); } } return randomStr; } - static async buildWebOptions(configOptions: OAuth2AuthenticateOptions): Promise { + static async buildWebOptions( + configOptions: OAuth2AuthenticateOptions, + ): Promise { const webOptions = new WebOptions(); webOptions.appId = this.getAppId(configOptions); - webOptions.authorizationBaseUrl = this.getOverwritableValue(configOptions, 'authorizationBaseUrl'); - webOptions.responseType = this.getOverwritableValue(configOptions, 'responseType'); + webOptions.authorizationBaseUrl = this.getOverwritableValue( + configOptions, + 'authorizationBaseUrl', + ); + webOptions.responseType = this.getOverwritableValue( + configOptions, + 'responseType', + ); if (!webOptions.responseType) { webOptions.responseType = 'token'; } - webOptions.redirectUrl = this.getOverwritableValue(configOptions, 'redirectUrl'); + webOptions.redirectUrl = this.getOverwritableValue( + configOptions, + 'redirectUrl', + ); // controlling parameters - webOptions.resourceUrl = this.getOverwritableValue(configOptions, 'resourceUrl'); - webOptions.accessTokenEndpoint = this.getOverwritableValue(configOptions, 'accessTokenEndpoint'); + webOptions.resourceUrl = this.getOverwritableValue( + configOptions, + 'resourceUrl', + ); + webOptions.accessTokenEndpoint = this.getOverwritableValue( + configOptions, + 'accessTokenEndpoint', + ); - webOptions.pkceEnabled = this.getOverwritableValue(configOptions, 'pkceEnabled'); + webOptions.pkceEnabled = this.getOverwritableValue( + configOptions, + 'pkceEnabled', + ); if (webOptions.pkceEnabled) { webOptions.pkceCodeVerifier = this.randomString(64); if (CryptoUtils.HAS_SUBTLE_CRYPTO) { - await CryptoUtils.deriveChallenge(webOptions.pkceCodeVerifier).then((c) => { - webOptions.pkceCodeChallenge = c; - webOptions.pkceCodeChallengeMethod = 'S256'; - }); + await CryptoUtils.deriveChallenge(webOptions.pkceCodeVerifier).then( + c => { + webOptions.pkceCodeChallenge = c; + webOptions.pkceCodeChallengeMethod = 'S256'; + }, + ); } else { webOptions.pkceCodeChallenge = webOptions.pkceCodeVerifier; webOptions.pkceCodeChallengeMethod = 'plain'; @@ -143,42 +190,45 @@ export class WebUtils { if (!webOptions.state || webOptions.state.length === 0) { webOptions.state = this.randomString(20); } - let parametersMapHelper = this.getOverwritableValue<{ [key: string]: string }>( - configOptions, - 'additionalParameters', - ); + const parametersMapHelper = this.getOverwritableValue<{ + [key: string]: string; + }>(configOptions, 'additionalParameters'); if (parametersMapHelper) { webOptions.additionalParameters = {}; for (const key in parametersMapHelper) { if (key && key.trim().length > 0) { - let value = parametersMapHelper[key]; + const value = parametersMapHelper[key]; if (value && value.trim().length > 0) { webOptions.additionalParameters[key] = value; } } } } - let headersMapHelper = this.getOverwritableValue<{ [key: string]: string }>( - configOptions, - 'additionalResourceHeaders', - ); + const 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]; + const value = headersMapHelper[key]; if (value && value.trim().length > 0) { webOptions.additionalResourceHeaders[key] = value; } } } } - webOptions.logsEnabled = this.getOverwritableValue(configOptions, 'logsEnabled'); + webOptions.logsEnabled = this.getOverwritableValue( + configOptions, + 'logsEnabled', + ); return webOptions; } - static buildWindowOptions(configOptions: OAuth2AuthenticateOptions) { + static buildWindowOptions( + configOptions: OAuth2AuthenticateOptions, + ): WebOptions { const windowOptions = new WebOptions(); if (configOptions.web) { if (configOptions.web.windowOptions) { @@ -193,9 +243,12 @@ export class WebUtils { } export class CryptoUtils { - static BASE64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + static BASE64_CHARS = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; static HAS_SUBTLE_CRYPTO: boolean = - typeof window !== 'undefined' && !!(window.crypto as any) && !!(window.crypto.subtle as any); + typeof window !== 'undefined' && + !!(window.crypto as any) && + !!(window.crypto.subtle as any); static toUint8Array(str: string): Uint8Array { const buf = new ArrayBuffer(str.length); @@ -212,12 +265,13 @@ export class CryptoUtils { } static toBase64(bytes: Uint8Array): string { - let len = bytes.length; + const len = bytes.length; let base64 = ''; for (let i = 0; i < len; i += 3) { base64 += this.BASE64_CHARS[bytes[i] >> 2]; base64 += this.BASE64_CHARS[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; - base64 += this.BASE64_CHARS[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += + this.BASE64_CHARS[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; base64 += this.BASE64_CHARS[bytes[i + 2] & 63]; } @@ -239,10 +293,12 @@ export class CryptoUtils { return new Promise((resolve, reject) => { crypto.subtle.digest('SHA-256', this.toUint8Array(codeVerifier)).then( - (arrayBuffer) => { - return resolve(this.toBase64Url(this.toBase64(new Uint8Array(arrayBuffer)))); + arrayBuffer => { + return resolve( + this.toBase64Url(this.toBase64(new Uint8Array(arrayBuffer))), + ); }, - (error) => reject(error), + error => reject(error), ); }); } @@ -259,7 +315,7 @@ export class WebOptions { redirectUrl: string; logsEnabled: boolean; windowOptions: string; - windowTarget: string = '_blank'; + windowTarget = '_blank'; pkceEnabled: boolean; pkceCodeVerifier: string; diff --git a/src/web.ts b/src/web.ts index cd11406c..e67296b3 100644 --- a/src/web.ts +++ b/src/web.ts @@ -1,6 +1,12 @@ import { WebPlugin } from '@capacitor/core'; -import type { OAuth2AuthenticateOptions, GenericOAuth2Plugin, OAuth2RefreshTokenOptions } from './definitions'; -import { WebOptions, WebUtils } from './web-utils'; + +import type { + OAuth2AuthenticateOptions, + GenericOAuth2Plugin, + OAuth2RefreshTokenOptions, +} from './definitions'; +import type { WebOptions } from './web-utils'; +import { WebUtils } from './web-utils'; export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { private webOptions: WebOptions; @@ -13,6 +19,7 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { /** * Get a new access token using an existing refresh token. */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars async refreshToken(_options: OAuth2RefreshTokenOptions): Promise { return new Promise((_resolve, reject) => { reject(new Error('Functionality not implemented for PWAs yet')); @@ -24,18 +31,31 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { // we open the window first to avoid popups being blocked because of // the asynchronous buildWebOptions call - this.windowHandle = window.open('', windowOptions.windowTarget, windowOptions.windowOptions); + this.windowHandle = window.open( + '', + windowOptions.windowTarget, + windowOptions.windowOptions, + ); this.webOptions = await WebUtils.buildWebOptions(options); return new Promise((resolve, reject) => { // validate if (!this.webOptions.appId || this.webOptions.appId.length == 0) { reject(new Error('ERR_PARAM_NO_APP_ID')); - } else if (!this.webOptions.authorizationBaseUrl || this.webOptions.authorizationBaseUrl.length == 0) { + } else if ( + !this.webOptions.authorizationBaseUrl || + this.webOptions.authorizationBaseUrl.length == 0 + ) { reject(new Error('ERR_PARAM_NO_AUTHORIZATION_BASE_URL')); - } else if (!this.webOptions.redirectUrl || this.webOptions.redirectUrl.length == 0) { + } else if ( + !this.webOptions.redirectUrl || + this.webOptions.redirectUrl.length == 0 + ) { reject(new Error('ERR_PARAM_NO_REDIRECT_URL')); - } else if (!this.webOptions.responseType || this.webOptions.responseType.length == 0) { + } else if ( + !this.webOptions.responseType || + this.webOptions.responseType.length == 0 + ) { reject(new Error('ERR_PARAM_NO_RESPONSE_TYPE')); } else { // init internal control params @@ -57,35 +77,51 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { window.clearInterval(this.intervalId); reject(new Error('USER_CANCELLED')); } else { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion let href: string = undefined!; try { - href = this.windowHandle?.location.href!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + href = this.windowHandle!.location.href!; } catch (ignore) { // ignore DOMException: Blocked a frame with origin "http://localhost:4200" from accessing a cross-origin frame. } - if (href != null && href.indexOf(this.webOptions.redirectUrl) >= 0) { + if ( + href != null && + href.indexOf(this.webOptions.redirectUrl) >= 0 + ) { if (this.webOptions.logsEnabled) { this.doLog('Url from Provider: ' + href); } - let authorizationRedirectUrlParamObj = WebUtils.getUrlParams(href); + const authorizationRedirectUrlParamObj = + WebUtils.getUrlParams(href); if (authorizationRedirectUrlParamObj) { if (this.webOptions.logsEnabled) { - this.doLog('Authorization response:', authorizationRedirectUrlParamObj); + this.doLog( + 'Authorization response:', + authorizationRedirectUrlParamObj, + ); } window.clearInterval(this.intervalId); // check state - if (authorizationRedirectUrlParamObj.state === this.webOptions.state) { + if ( + authorizationRedirectUrlParamObj.state === + this.webOptions.state + ) { if (this.webOptions.accessTokenEndpoint) { const self = this; - let authorizationCode = authorizationRedirectUrlParamObj.code; + const authorizationCode = + authorizationRedirectUrlParamObj.code; if (authorizationCode) { const tokenRequest = new XMLHttpRequest(); tokenRequest.onload = function () { if (this.status === 200) { - let accessTokenResponse = JSON.parse(this.response); + const accessTokenResponse = JSON.parse(this.response); if (self.webOptions.logsEnabled) { - self.doLog('Access token response:', accessTokenResponse); + self.doLog( + 'Access token response:', + accessTokenResponse, + ); } self.requestResource( accessTokenResponse.access_token, @@ -98,14 +134,35 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { }; tokenRequest.onerror = function () { // always log error because of CORS hint - self.doLog('ERR_GENERAL: See client logs. It might be CORS. Status text: ' + this.statusText); + 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); - tokenRequest.setRequestHeader('accept', 'application/json'); - tokenRequest.setRequestHeader('cache-control', 'no-cache'); - tokenRequest.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); - tokenRequest.send(WebUtils.getTokenEndpointData(this.webOptions, authorizationCode)); + tokenRequest.open( + 'POST', + this.webOptions.accessTokenEndpoint, + true, + ); + tokenRequest.setRequestHeader( + 'accept', + 'application/json', + ); + tokenRequest.setRequestHeader( + 'cache-control', + 'no-cache', + ); + tokenRequest.setRequestHeader( + 'content-type', + 'application/x-www-form-urlencoded', + ); + tokenRequest.send( + WebUtils.getTokenEndpointData( + this.webOptions, + authorizationCode, + ), + ); } else { reject(new Error('ERR_NO_AUTHORIZATION_CODE')); } @@ -121,8 +178,13 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { } } else { if (this.webOptions.logsEnabled) { - this.doLog('State from web options: ' + this.webOptions.state); - this.doLog('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(); @@ -158,12 +220,17 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { const request = new XMLHttpRequest(); request.onload = function () { if (this.status === 200) { - let resp = JSON.parse(this.response); + const resp = JSON.parse(this.response); if (logsEnabled) { self.doLog('Resource response:', resp); } if (resp) { - self.assignResponses(resp, accessToken, authorizationResponse, accessTokenResponse); + self.assignResponses( + resp, + accessToken, + authorizationResponse, + accessTokenResponse, + ); } if (logsEnabled) { self.doLog(self.MSG_RETURNED_TO_JS, resp); @@ -185,7 +252,10 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { request.setRequestHeader('Authorization', `Bearer ${accessToken}`); if (this.webOptions.additionalResourceHeaders) { for (const key in this.webOptions.additionalResourceHeaders) { - request.setRequestHeader(key, this.webOptions.additionalResourceHeaders[key]); + request.setRequestHeader( + key, + this.webOptions.additionalResourceHeaders[key], + ); } } request.send(); @@ -201,7 +271,12 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { } else { // if no resource url exists just return the accessToken response const resp = {}; - this.assignResponses(resp, accessToken, authorizationResponse, accessTokenResponse); + this.assignResponses( + resp, + accessToken, + authorizationResponse, + accessTokenResponse, + ); if (this.webOptions.logsEnabled) { this.doLog(this.MSG_RETURNED_TO_JS, resp); } @@ -210,7 +285,12 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { } } - assignResponses(resp: any, accessToken: string, authorizationResponse: any, accessTokenResponse: any = null): void { + assignResponses( + resp: any, + accessToken: string, + authorizationResponse: any, + accessTokenResponse: any = null, + ): void { // #154 if (authorizationResponse) { resp['authorization_response'] = authorizationResponse; @@ -222,6 +302,7 @@ export class GenericOAuth2Web extends WebPlugin implements GenericOAuth2Plugin { } async logout(options: OAuth2AuthenticateOptions): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars return new Promise((resolve, _reject) => { localStorage.removeItem(WebUtils.getAppId(options)); resolve(true);