From 5484796ce62d809ee549a399cc76a76092d96dcc Mon Sep 17 00:00:00 2001 From: Remek Zajac# Date: Thu, 27 Apr 2017 08:14:53 +0100 Subject: [PATCH 1/2] add access_token retrieval --- plugin.xml | 1 + src/android/GooglePlus.java | 120 ++++++++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 26 deletions(-) diff --git a/plugin.xml b/plugin.xml index 85a3268c..f93a5546 100755 --- a/plugin.xml +++ b/plugin.xml @@ -31,6 +31,7 @@ + diff --git a/src/android/GooglePlus.java b/src/android/GooglePlus.java index 4dc8c5f8..fd3ee316 100644 --- a/src/android/GooglePlus.java +++ b/src/android/GooglePlus.java @@ -1,8 +1,16 @@ package nl.xservices.plugins; +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AccountManagerFuture; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.app.Activity; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.os.AsyncTask; +import android.os.Bundle; import android.util.Log; import com.google.android.gms.auth.api.Auth; @@ -16,12 +24,18 @@ import com.google.android.gms.common.api.Scope; import org.apache.cordova.*; +import org.apache.cordova.engine.SystemWebChromeClient; import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; import java.security.MessageDigest; import android.content.pm.Signature; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + /** * Originally written by Eddy Verbruggen (http://github.com/EddyVerbruggen/cordova-plugin-googleplus) * Forked/Duplicated and Modified by PointSource, LLC, 2016. @@ -35,6 +49,11 @@ public class GooglePlus extends CordovaPlugin implements GoogleApiClient.OnConne public static final String ACTION_DISCONNECT = "disconnect"; public static final String ACTION_GET_SIGNING_CERTIFICATE_FINGERPRINT = "getSigningCertificateFingerprint"; + private final static String KAccess_Token = "access_token"; + private final static String KTokenExpires = "expires"; + + private final static String KVerifyTokenUrl = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token="; + //String options/config object names passed in to login and trySilentLogin public static final String ARGUMENT_WEB_CLIENT_ID = "webClientId"; public static final String ARGUMENT_SCOPES = "scopes"; @@ -43,6 +62,7 @@ public class GooglePlus extends CordovaPlugin implements GoogleApiClient.OnConne public static final String TAG = "GooglePlugin"; public static final int RC_GOOGLEPLUS = 77552; // Request Code to identify our plugin's activities + public static final int KAssumeStaleTokenSec = 60; // Wraps our service connection to Google Play services and provides access to the users sign in state and Google APIs private GoogleApiClient mGoogleApiClient; @@ -295,7 +315,7 @@ public void onActivityResult(int requestCode, final int resultCode, final Intent * * @param signInResult - the GoogleSignInResult object retrieved in the onActivityResult method. */ - private void handleSignInResult(GoogleSignInResult signInResult) { + private void handleSignInResult(final GoogleSignInResult signInResult) { if (this.mGoogleApiClient == null) { savedCallbackContext.error("GoogleApiClient was never initialized"); return; @@ -314,31 +334,32 @@ private void handleSignInResult(GoogleSignInResult signInResult) { //Return the status code to be handled client side savedCallbackContext.error(signInResult.getStatus().getStatusCode()); } else { - GoogleSignInAccount acct = signInResult.getSignInAccount(); - - JSONObject result = new JSONObject(); - - try { - Log.i(TAG, "trying to get account information"); - - result.put("email", acct.getEmail()); - - //only gets included if requested (See Line 164). - result.put("idToken", acct.getIdToken()); - - //only gets included if requested (See Line 166). - result.put("serverAuthCode", acct.getServerAuthCode()); - - result.put("userId", acct.getId()); - result.put("displayName", acct.getDisplayName()); - result.put("familyName", acct.getFamilyName()); - result.put("givenName", acct.getGivenName()); - result.put("imageUrl", acct.getPhotoUrl()); - - this.savedCallbackContext.success(result); - } catch (JSONException e) { - savedCallbackContext.error("Trouble parsing result, error: " + e.getMessage()); - } + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + GoogleSignInAccount acct = signInResult.getSignInAccount(); + JSONObject result = new JSONObject(); + try { + JSONObject accessTokenBundle = getAuthToken( + cordova.getActivity(), acct.getAccount(), true + ); + result.put(KAccess_Token, accessTokenBundle.get(KAccess_Token)); + result.put(KTokenExpires, accessTokenBundle.get(KTokenExpires)); + result.put("email", acct.getEmail()); + result.put("idToken", acct.getIdToken()); + result.put("serverAuthCode", acct.getServerAuthCode()); + result.put("userId", acct.getId()); + result.put("displayName", acct.getDisplayName()); + result.put("familyName", acct.getFamilyName()); + result.put("givenName", acct.getGivenName()); + result.put("imageUrl", acct.getPhotoUrl()); + savedCallbackContext.success(result); + } catch (Exception e) { + savedCallbackContext.error("Trouble obtaining result, error: " + e.getMessage()); + } + return null; + } + }.execute(); } } @@ -373,4 +394,51 @@ private void getSigningCertificateFingerprint() { savedCallbackContext.error(e.getMessage()); } } + + private JSONObject getAuthToken(Activity activity, Account account, boolean retry) throws Exception { + AccountManager manager = AccountManager.get(activity); + AccountManagerFuture future = manager.getAuthToken(account, "oauth2:profile email", null, activity, null, null); + Bundle bundle = future.getResult(); + String authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN); + try { + return verifyToken(authToken); + } catch (IOException e) { + if (retry) { + manager.invalidateAuthToken("com.google", authToken); + return getAuthToken(activity, account, false); + } else { + throw e; + } + } + } + + private JSONObject verifyToken(String authToken) throws IOException, JSONException { + OkHttpClient client = client = new OkHttpClient.Builder().build(); + Request request = new Request.Builder() + .url(KVerifyTokenUrl+authToken) + .get().build(); + Response response = client.newCall(request).execute(); + /* expecting: + { + "issued_to": "608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com", + "audience": "608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com", + "user_id": "107046534809469736555", + "scope": "https://www.googleapis.com/auth/userinfo.profile", + "expires_in": 3595, + "access_type": "offline" + }*/ + + String stringResponse = response.body().string(); + Log.d("AuthenticatedBackend", "token: " + authToken + ", verification: " + stringResponse); + JSONObject jsonResponse = new JSONObject( + stringResponse + ); + int expires_in = jsonResponse.getInt("expires_in"); + if (expires_in < KAssumeStaleTokenSec) { + throw new IOException("Auth token soon expiring."); + } + jsonResponse.put(KAccess_Token, authToken); + jsonResponse.put(KTokenExpires, expires_in + (System.currentTimeMillis()/1000)); + return jsonResponse; + } } From 7cd169a40d01069f978cbbb97a2582c89df41050 Mon Sep 17 00:00:00 2001 From: Remek Zajac# Date: Sat, 29 Apr 2017 09:58:52 +0100 Subject: [PATCH 2/2] review comments --- README.md | 5 +-- plugin.xml | 1 - src/android/GooglePlus.java | 72 ++++++++++++++++++++++--------------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index d2908c28..9706ce8d 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Login on iOS takes the user to a [SafariViewController](https://developer.apple. ### Android To configure Android, [generate a configuration file here](https://developers.google.com/mobile/add?platform=android&cntapi=signin). Once Google Sign-In is enabled Google will automatically create necessary credentials in Developer Console. There is no need to add the generated google-services.json file into your cordova project. -Make sure you execute the `keytool` steps as explained [here](https://developers.google.com/android/guides/client-auth) or authentication will fail. +Make sure you execute the `keytool` steps as explained [here](https://developers.google.com/drive/android/auth) or authentication will fail (do this for both release and debug keystores). IMPORTANT: * The step above, about `keytool`, show 2 types of certificate fingerprints, the **Release** and the **Debug**, when generating the configuration file, it's better to use the **Debug** certificate fingerprint, after that, you have to go on [Google Credentials Manager](https://console.developers.google.com/apis/credentials), and manually create a credential for **OAuth2 client** with your **Release** certificate fingerprint. This is necessary to your application work on both Development and Production releases. @@ -148,7 +148,7 @@ function deviceReady() { The login function walks the user through the Google Auth process. All parameters are optional, however there are a few caveats. -To get an `idToken` on Android, you ***must*** pass in your `webClientId`. On iOS, the `idToken` is included in the sign in result by default. +To get an `idToken` on Android, you ***must*** pass in your `webClientId` (a frequent mistake is to supply Android Client ID). On iOS, the `idToken` is included in the sign in result by default. To get a `serverAuthCode`, you must pass in your `webClientId` _and_ set `offline` to true. If offline is true, but no webClientId is provided, the `serverAuthCode` will _**NOT**_ be requested. @@ -183,6 +183,7 @@ The success callback (second argument) gets a JSON object with the following con obj.imageUrl // 'http://link-to-my-profilepic.google.com' obj.idToken // idToken that can be exchanged to verify user identity. obj.serverAuthCode // Auth code that can be exchanged for an access token and refresh token for offline access + obj.accessToken // OAuth2 access token ``` Additional user information is available by use case. Add the scopes needed to the scopes option then return the info to the result object being created in the `handleSignInResult` and `didSignInForUser` functions on Android and iOS, respectively. diff --git a/plugin.xml b/plugin.xml index f93a5546..85a3268c 100755 --- a/plugin.xml +++ b/plugin.xml @@ -31,7 +31,6 @@ - diff --git a/src/android/GooglePlus.java b/src/android/GooglePlus.java index fd3ee316..4e643707 100644 --- a/src/android/GooglePlus.java +++ b/src/android/GooglePlus.java @@ -28,14 +28,16 @@ import org.json.JSONException; import org.json.JSONObject; +import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; import java.security.MessageDigest; import android.content.pm.Signature; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; - /** * Originally written by Eddy Verbruggen (http://github.com/EddyVerbruggen/cordova-plugin-googleplus) * Forked/Duplicated and Modified by PointSource, LLC, 2016. @@ -49,10 +51,10 @@ public class GooglePlus extends CordovaPlugin implements GoogleApiClient.OnConne public static final String ACTION_DISCONNECT = "disconnect"; public static final String ACTION_GET_SIGNING_CERTIFICATE_FINGERPRINT = "getSigningCertificateFingerprint"; - private final static String KAccess_Token = "access_token"; - private final static String KTokenExpires = "expires"; - - private final static String KVerifyTokenUrl = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token="; + private final static String FIELD_ACCESS_TOKEN = "accessToken"; + private final static String FIELD_TOKEN_EXPIRES = "expires"; + private final static String FIELD_TOKEN_EXPIRES_IN = "expires_in"; + private final static String VERIFY_TOKEN_URL = "https://www.googleapis.com/oauth2/v1/tokeninfo?access_token="; //String options/config object names passed in to login and trySilentLogin public static final String ARGUMENT_WEB_CLIENT_ID = "webClientId"; @@ -343,8 +345,9 @@ protected Void doInBackground(Void... params) { JSONObject accessTokenBundle = getAuthToken( cordova.getActivity(), acct.getAccount(), true ); - result.put(KAccess_Token, accessTokenBundle.get(KAccess_Token)); - result.put(KTokenExpires, accessTokenBundle.get(KTokenExpires)); + result.put(FIELD_ACCESS_TOKEN, accessTokenBundle.get(FIELD_ACCESS_TOKEN)); + result.put(FIELD_TOKEN_EXPIRES, accessTokenBundle.get(FIELD_TOKEN_EXPIRES)); + result.put(FIELD_TOKEN_EXPIRES_IN, accessTokenBundle.get(FIELD_TOKEN_EXPIRES_IN)); result.put("email", acct.getEmail()); result.put("idToken", acct.getIdToken()); result.put("serverAuthCode", acct.getServerAuthCode()); @@ -413,32 +416,43 @@ private JSONObject getAuthToken(Activity activity, Account account, boolean retr } private JSONObject verifyToken(String authToken) throws IOException, JSONException { - OkHttpClient client = client = new OkHttpClient.Builder().build(); - Request request = new Request.Builder() - .url(KVerifyTokenUrl+authToken) - .get().build(); - Response response = client.newCall(request).execute(); - /* expecting: - { - "issued_to": "608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com", - "audience": "608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com", - "user_id": "107046534809469736555", - "scope": "https://www.googleapis.com/auth/userinfo.profile", - "expires_in": 3595, - "access_type": "offline" - }*/ - - String stringResponse = response.body().string(); + URL url = new URL(VERIFY_TOKEN_URL+authToken); + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setInstanceFollowRedirects(true); + String stringResponse = fromStream( + new BufferedInputStream(urlConnection.getInputStream()) + ); + /* expecting: + { + "issued_to": "608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com", + "audience": "608941808256-43vtfndets79kf5hac8ieujto8837660.apps.googleusercontent.com", + "user_id": "107046534809469736555", + "scope": "https://www.googleapis.com/auth/userinfo.profile", + "expires_in": 3595, + "access_type": "offline" + }*/ + Log.d("AuthenticatedBackend", "token: " + authToken + ", verification: " + stringResponse); JSONObject jsonResponse = new JSONObject( stringResponse ); - int expires_in = jsonResponse.getInt("expires_in"); + int expires_in = jsonResponse.getInt(FIELD_TOKEN_EXPIRES_IN); if (expires_in < KAssumeStaleTokenSec) { throw new IOException("Auth token soon expiring."); } - jsonResponse.put(KAccess_Token, authToken); - jsonResponse.put(KTokenExpires, expires_in + (System.currentTimeMillis()/1000)); + jsonResponse.put(FIELD_ACCESS_TOKEN, authToken); + jsonResponse.put(FIELD_TOKEN_EXPIRES, expires_in + (System.currentTimeMillis()/1000)); return jsonResponse; } + + public static String fromStream(InputStream is) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + StringBuilder sb = new StringBuilder(); + String line = null; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + reader.close(); + return sb.toString(); + } }