Skip to content

Commit

Permalink
Merge pull request EddyVerbruggen#378 from remster/master
Browse files Browse the repository at this point in the history
add access_token retrieval
  • Loading branch information
EddyVerbruggen authored May 4, 2017
2 parents de47036 + 7cd169a commit ee15212
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 28 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down
134 changes: 108 additions & 26 deletions src/android/GooglePlus.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -16,9 +24,17 @@
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.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;

Expand All @@ -35,6 +51,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 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";
public static final String ARGUMENT_SCOPES = "scopes";
Expand All @@ -43,6 +64,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;
Expand Down Expand Up @@ -295,7 +317,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;
Expand All @@ -314,31 +336,33 @@ 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<Void, Void, Void>() {
@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(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());
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();
}
}

Expand Down Expand Up @@ -373,4 +397,62 @@ private void getSigningCertificateFingerprint() {
savedCallbackContext.error(e.getMessage());
}
}

private JSONObject getAuthToken(Activity activity, Account account, boolean retry) throws Exception {
AccountManager manager = AccountManager.get(activity);
AccountManagerFuture<Bundle> 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 {
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(FIELD_TOKEN_EXPIRES_IN);
if (expires_in < KAssumeStaleTokenSec) {
throw new IOException("Auth token soon expiring.");
}
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();
}
}

0 comments on commit ee15212

Please sign in to comment.