package io.fullstack.oauth; import android.app.Activity; import android.app.FragmentManager; import android.content.Context; import android.support.annotation.Nullable; import android.util.Log; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableMap; import com.github.scribejava.core.model.OAuth1AccessToken; import com.github.scribejava.core.model.OAuth2AccessToken; import com.github.scribejava.core.model.OAuthRequest; import com.github.scribejava.core.model.Response; import com.github.scribejava.core.model.Verb; import com.github.scribejava.core.oauth.OAuth10aService; import com.github.scribejava.core.oauth.OAuth20Service; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; class ProviderNotConfiguredException extends Exception { public ProviderNotConfiguredException(String message) { super(message); } } @SuppressWarnings("WeakerAccess") class OAuthManagerModule extends ReactContextBaseJavaModule { private static final String TAG = "OAuthManager"; private Context context; private ReactContext mReactContext; private HashMap _configuration = new HashMap<String, HashMap<String,Object>>(); private ArrayList _callbackUrls = new ArrayList<String>(); private OAuthManagerStore _credentialsStore; public OAuthManagerModule(ReactApplicationContext reactContext) { super(reactContext); mReactContext = reactContext; _credentialsStore = OAuthManagerStore.getOAuthManagerStore(mReactContext, TAG, Context.MODE_PRIVATE); Log.d(TAG, "New instance"); } @Override public String getName() { return TAG; } @ReactMethod public void configureProvider( final String providerName, final ReadableMap params, @Nullable final Callback onComplete ) { Log.i(TAG, "configureProvider for " + providerName); // Save callback url for later String callbackUrlStr = params.getString("callback_url"); _callbackUrls.add(callbackUrlStr); Log.d(TAG, "Added callback url " + callbackUrlStr + " for providler " + providerName); // Keep configuration map HashMap<String, Object> cfg = new HashMap<String,Object>(); ReadableMapKeySetIterator iterator = params.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); ReadableType readableType = params.getType(key); switch(readableType) { case String: String val = params.getString(key); // String escapedVal = Uri.encode(val); cfg.put(key, val); break; default: throw new IllegalArgumentException("Could not read object with key: " + key); } } _configuration.put(providerName, cfg); onComplete.invoke(null, true); } @ReactMethod public void authorize( final String providerName, @Nullable final ReadableMap params, final Callback callback) { try { final OAuthManagerModule self = this; final HashMap<String,Object> cfg = this.getConfiguration(providerName); final String authVersion = (String) cfg.get("auth_version"); Activity activity = this.getCurrentActivity(); FragmentManager fragmentManager = activity.getFragmentManager(); String callbackUrl = "http://localhost/" + providerName; OAuthManagerOnAccessTokenListener listener = new OAuthManagerOnAccessTokenListener() { public void onRequestTokenError(final Exception ex) { Log.e(TAG, "Exception with request token: " + ex.getMessage()); _credentialsStore.delete(providerName); _credentialsStore.commit(); } public void onOAuth1AccessToken(final OAuth1AccessToken accessToken) { _credentialsStore.store(providerName, accessToken); _credentialsStore.commit(); WritableMap resp = self.accessTokenResponse(providerName, cfg, accessToken, authVersion); callback.invoke(null, resp); } public void onOAuth2AccessToken(final OAuth2AccessToken accessToken) { _credentialsStore.store(providerName, accessToken); _credentialsStore.commit(); WritableMap resp = self.accessTokenResponse(providerName, cfg, accessToken, authVersion); callback.invoke(null, resp); } }; if (authVersion.equals("1.0")) { final OAuth10aService service = OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, params, callbackUrl); OAuthManagerFragmentController ctrl = new OAuthManagerFragmentController(mReactContext, fragmentManager, providerName, service, callbackUrl); ctrl.requestAuth(cfg, listener); } else if (authVersion.equals("2.0")) { final OAuth20Service service = OAuthManagerProviders.getApiFor20Provider(providerName, cfg, params, callbackUrl); OAuthManagerFragmentController ctrl = new OAuthManagerFragmentController(mReactContext, fragmentManager, providerName, service, callbackUrl); ctrl.requestAuth(cfg, listener); } else { Log.d(TAG, "Auth version unknown: " + (String) cfg.get("auth_version")); } } catch (Exception ex) { Log.d(TAG, "Exception in callback " + ex.getMessage()); exceptionCallback(ex, callback); } } @ReactMethod public void makeRequest( final String providerName, final String urlString, final ReadableMap params, final Callback onComplete) { Log.i(TAG, "makeRequest called for " + providerName + " to " + urlString); try { HashMap<String,Object> cfg = this.getConfiguration(providerName); final String authVersion = (String) cfg.get("auth_version"); URL url; try { if (urlString.contains("http")) { url = new URL(urlString); } else { String apiHost = (String) cfg.get("api_url"); url = new URL(apiHost + urlString); } } catch (MalformedURLException ex) { Log.e(TAG, "Bad url. Check request and try again: " + ex.getMessage()); exceptionCallback(ex, onComplete); return; } String httpMethod; if (params.hasKey("method")) { httpMethod = params.getString("method"); } else { httpMethod = "GET"; } Verb httpVerb; if (httpMethod.equalsIgnoreCase("GET")) { httpVerb = Verb.GET; } else if (httpMethod.equalsIgnoreCase("POST")) { httpVerb = Verb.POST; } else if (httpMethod.equalsIgnoreCase("PUT")) { httpVerb = Verb.PUT; } else if (httpMethod.equalsIgnoreCase("DELETE")) { httpVerb = Verb.DELETE; } else if (httpMethod.equalsIgnoreCase("OPTIONS")) { httpVerb = Verb.OPTIONS; } else if (httpMethod.equalsIgnoreCase("HEAD")) { httpVerb = Verb.HEAD; } else if (httpMethod.equalsIgnoreCase("PATCH")) { httpVerb = Verb.PATCH; } else if (httpMethod.equalsIgnoreCase("TRACE")) { httpVerb = Verb.TRACE; } else { httpVerb = Verb.GET; } ReadableMap requestParams = null; if (params != null && params.hasKey("params")) { requestParams = params.getMap("params"); } OAuthRequest request = oauthRequestWithParams(providerName, cfg, authVersion, httpVerb, url, requestParams); if (authVersion.equals("1.0")) { final OAuth10aService service = OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, requestParams, null); OAuth1AccessToken token = _credentialsStore.get(providerName, OAuth1AccessToken.class); service.signRequest(token, request); } else if (authVersion.equals("2.0")) { final OAuth20Service service = OAuthManagerProviders.getApiFor20Provider(providerName, cfg, requestParams, null); OAuth2AccessToken token = _credentialsStore.get(providerName, OAuth2AccessToken.class); service.signRequest(token, request); } else { // Some kind of error here Log.e(TAG, "An error occurred"); WritableMap err = Arguments.createMap(); err.putString("status", "error"); err.putString("msg", "A weird error occurred"); onComplete.invoke(err); return; } final Response response = request.send(); final String rawBody = response.getBody(); Log.d(TAG, "rawBody: " + rawBody); // final Object response = new Gson().fromJson(rawBody, Object.class); WritableMap resp = Arguments.createMap(); resp.putInt("status", response.getCode()); resp.putString("data", rawBody); onComplete.invoke(null, resp); } catch (IOException ex) { Log.e(TAG, "IOException when making request: " + ex.getMessage()); ex.printStackTrace(); exceptionCallback(ex, onComplete); } catch (Exception ex) { Log.e(TAG, "Exception when making request: " + ex.getMessage()); exceptionCallback(ex, onComplete); } } private OAuthRequest oauthRequestWithParams( final String providerName, final HashMap<String,Object> cfg, final String authVersion, final Verb httpVerb, final URL url, @Nullable final ReadableMap params ) throws Exception { OAuthRequest request; // OAuthConfig config; if (authVersion.equals("1.0")) { // final OAuth10aService service = // OAuthManagerProviders.getApiFor10aProvider(providerName, cfg, null, null); OAuth1AccessToken oa1token = _credentialsStore.get(providerName, OAuth1AccessToken.class); request = OAuthManagerProviders.getRequestForProvider( providerName, httpVerb, oa1token, url, cfg, params); // config = service.getConfig(); // request = new OAuthRequest(httpVerb, url.toString(), config); } else if (authVersion.equals("2.0")) { // final OAuth20Service service = // OAuthManagerProviders.getApiFor20Provider(providerName, cfg, null, null); // oa2token = _credentialsStore.get(providerName, OAuth2AccessToken.class); OAuth2AccessToken oa2token = _credentialsStore.get(providerName, OAuth2AccessToken.class); request = OAuthManagerProviders.getRequestForProvider( providerName, httpVerb, oa2token, url, cfg, params); // config = service.getConfig(); // request = new OAuthRequest(httpVerb, url.toString(), config); } else { Log.e(TAG, "Error in making request method"); throw new Exception("Provider not handled yet"); } return request; } @ReactMethod public void getSavedAccounts(final ReadableMap options, final Callback onComplete) { // Log.d(TAG, "getSavedAccounts"); } @ReactMethod public void getSavedAccount( final String providerName, final ReadableMap options, final Callback onComplete) { try { HashMap<String,Object> cfg = this.getConfiguration(providerName); final String authVersion = (String) cfg.get("auth_version"); Log.i(TAG, "getSavedAccount for " + providerName); if (authVersion.equals("1.0")) { OAuth1AccessToken token = _credentialsStore.get(providerName, OAuth1AccessToken.class); Log.d(TAG, "Found token: " + token); if (token == null || token.equals("")) { throw new Exception("No token found"); } WritableMap resp = this.accessTokenResponse(providerName, cfg, token, authVersion); onComplete.invoke(null, resp); } else if (authVersion.equals("2.0")) { OAuth2AccessToken token = _credentialsStore.get(providerName, OAuth2AccessToken.class); if (token == null || token.equals("")) { throw new Exception("No token found"); } WritableMap resp = this.accessTokenResponse(providerName, cfg, token, authVersion); onComplete.invoke(null, resp); } else { } } catch (ProviderNotConfiguredException ex) { Log.e(TAG, "Provider not yet configured: " + providerName); exceptionCallback(ex, onComplete); } catch (Exception ex) { Log.e(TAG, "An exception occurred getSavedAccount: " + ex.getMessage()); ex.printStackTrace(); exceptionCallback(ex, onComplete); } } @ReactMethod public void deauthorize(final String providerName, final Callback onComplete) { try { Log.i(TAG, "deauthorizing " + providerName); HashMap<String,Object> cfg = this.getConfiguration(providerName); final String authVersion = (String) cfg.get("auth_version"); _credentialsStore.delete(providerName); WritableMap resp = Arguments.createMap(); resp.putString("status", "ok"); onComplete.invoke(null, resp); } catch (Exception ex) { exceptionCallback(ex, onComplete); } } private HashMap<String,Object> getConfiguration( final String providerName ) throws Exception { if (!_configuration.containsKey(providerName)) { throw new ProviderNotConfiguredException("Provider not configured: " + providerName); } HashMap<String,Object> cfg = (HashMap) _configuration.get(providerName); return cfg; } private WritableMap accessTokenResponse( final String providerName, final HashMap<String,Object> cfg, final OAuth1AccessToken accessToken, final String oauthVersion ) { WritableMap resp = Arguments.createMap(); WritableMap response = Arguments.createMap(); Log.d(TAG, "Credential raw response: " + accessToken.getRawResponse()); /* Some things return as JSON, some as x-www-form-urlencoded (querystring) */ Map accessTokenMap = null; try { accessTokenMap = new Gson().fromJson(accessToken.getRawResponse(), Map.class); } catch (JsonSyntaxException e) { /* failed to parse as JSON, so turn it into a HashMap which looks like the one we'd get back from the JSON parser, so the rest of the code continues unchanged. */ Log.d(TAG, "Credential looks like a querystring; parsing as such"); accessTokenMap = new HashMap(); accessTokenMap.put("user_id", accessToken.getParameter("user_id")); accessTokenMap.put("oauth_token_secret", accessToken.getParameter("oauth_token_secret")); accessTokenMap.put("token_type", accessToken.getParameter("token_type")); } resp.putString("status", "ok"); resp.putBoolean("authorized", true); resp.putString("provider", providerName); String uuid = accessToken.getParameter("user_id"); response.putString("uuid", uuid); String oauthTokenSecret = (String) accessToken.getParameter("oauth_token_secret"); String tokenType = (String) accessToken.getParameter("token_type"); if (tokenType == null) { tokenType = "Bearer"; } String consumerKey = (String) cfg.get("consumer_key"); WritableMap credentials = Arguments.createMap(); credentials.putString("access_token", accessToken.getToken()); credentials.putString("access_token_secret", oauthTokenSecret); credentials.putString("type", tokenType); credentials.putString("consumerKey", consumerKey); response.putMap("credentials", credentials); resp.putMap("response", response); return resp; } private WritableMap accessTokenResponse( final String providerName, final HashMap<String,Object> cfg, final OAuth2AccessToken accessToken, final String oauthVersion ) { WritableMap resp = Arguments.createMap(); WritableMap response = Arguments.createMap(); resp.putString("status", "ok"); resp.putBoolean("authorized", true); resp.putString("provider", providerName); String uuid = accessToken.getParameter("user_id"); response.putString("uuid", uuid); WritableMap credentials = Arguments.createMap(); Log.d(TAG, "Credential raw response: " + accessToken.getRawResponse()); credentials.putString("accessToken", accessToken.getAccessToken()); String authHeader; String tokenType = accessToken.getTokenType(); if (tokenType == null) { tokenType = "Bearer"; } String scope = accessToken.getScope(); if (scope == null) { scope = (String) cfg.get("scopes"); } String clientID = (String) cfg.get("client_id"); String idToken = accessToken.getParameter("id_token"); authHeader = tokenType + " " + accessToken.getAccessToken(); credentials.putString("authorizationHeader", authHeader); credentials.putString("type", tokenType); credentials.putString("scopes", scope); credentials.putString("clientID", clientID); credentials.putString("idToken", idToken); response.putMap("credentials", credentials); resp.putMap("response", response); return resp; } private void exceptionCallback(Exception ex, final Callback onFail) { WritableMap error = Arguments.createMap(); error.putInt("errorCode", ex.hashCode()); error.putString("errorMessage", ex.getMessage()); error.putString("allErrorMessage", ex.toString()); onFail.invoke(error); } public static Map<String, Object> recursivelyDeconstructReadableMap(ReadableMap readableMap) { Map<String, Object> deconstructedMap = new HashMap<>(); if (readableMap == null) { return deconstructedMap; } ReadableMapKeySetIterator iterator = readableMap.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); ReadableType type = readableMap.getType(key); switch (type) { case Null: deconstructedMap.put(key, null); break; case Boolean: deconstructedMap.put(key, readableMap.getBoolean(key)); break; case Number: deconstructedMap.put(key, readableMap.getDouble(key)); break; case String: deconstructedMap.put(key, readableMap.getString(key)); break; case Map: deconstructedMap.put(key, OAuthManagerModule.recursivelyDeconstructReadableMap(readableMap.getMap(key))); break; case Array: deconstructedMap.put(key, OAuthManagerModule.recursivelyDeconstructReadableArray(readableMap.getArray(key))); break; default: throw new IllegalArgumentException("Could not convert object with key: " + key + "."); } } return deconstructedMap; } public static List<Object> recursivelyDeconstructReadableArray(ReadableArray readableArray) { List<Object> deconstructedList = new ArrayList<>(readableArray.size()); for (int i = 0; i < readableArray.size(); i++) { ReadableType indexType = readableArray.getType(i); switch (indexType) { case Null: deconstructedList.add(i, null); break; case Boolean: deconstructedList.add(i, readableArray.getBoolean(i)); break; case Number: deconstructedList.add(i, readableArray.getDouble(i)); break; case String: deconstructedList.add(i, readableArray.getString(i)); break; case Map: deconstructedList.add(i, OAuthManagerModule.recursivelyDeconstructReadableMap(readableArray.getMap(i))); break; case Array: deconstructedList.add(i, OAuthManagerModule.recursivelyDeconstructReadableArray(readableArray.getArray(i))); break; default: throw new IllegalArgumentException("Could not convert object at index " + i + "."); } } return deconstructedList; } }