Search code examples
androidgoogle-api-php-clientgoogle-api-client

Google+ Sign In cross client(android/web) authentication


i'm trying to integrate 'Log in with Google' in app that have an android and web component. Everything in the web component is working fine with the following steps: 1. Rendering the view with an anti-forgery token, client id and app name.

$state = md5(rand());
Session::set('state', $state);
$this->view->render('login', array(
    'CLIENT_ID' => 'my_web_client_id',
    'STATE' => $state,
    'APPLICATION_NAME' => 'my_app_name'));

2. When user clicks on the Google's SignIn button, obtain the one-time code from Google's servers and send it to my server. 3. After my server receives the one-time code using https://github.com/google/google-api-php-client to authenticate the user with that code.

if ($_SESSION['state'] != $_POST['state']) { // Where state is the anti-forgery token
  return 'some error';
}

$code = $_POST['code'];
$client = new Google_Client();
$client->setApplicationName("my_app_name");
$client->setClientId('my_web_client_id');
$client->setClientSecret('client_secret');
$client->setRedirectUri('postmessage');
$client->addScope("https://www.googleapis.com/auth/urlshortener");
$client->authenticate($code);

$token = json_decode($client->getAccessToken());
// Verify the token
$reqUrl = 'https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' . $token->access_token;
$req = new Google_Http_Request($reqUrl);          
$tokenInfo = json_decode($client->getAuth()->authenticatedRequest($req)->getResponseBody());

// If there was an error in the token info, abort.
if ($tokenInfo->error) {
  return 'some error';
}

// Make sure the token we got is for our app.
if ($tokenInfo->audience != "my_web_client_id") {
  return 'some error';
}

// Saving user in db
...
// Load the app view

Now, for android client should be something similar, right? Following these tutorials:https://developers.google.com/+/mobile/android/sign-in and http://www.androidhive.info/2014/02/android-login-with-google-plus-account-1/

Executing async task in onConnected method

class CreateToken extends AsyncTask<Void, Void, String> {

    @Override
    protected String doInBackground(Void... voids) {
        oneTimeCode = getOneTimeCode();
        String email = getUserGPlusEmail();
        try {
            // Opens connection and sends the one-time code and email to the server with 'POST' request
            googleLogin(oneTimeCode, email);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return oneTimeCode;
    }
}

private String getOneTimeCode() {

    String scopes = "oauth2:server:client_id:" + SERVER_CLIENT_ID + ":api_scope:" + SCOPE_EMAIL;
    String code = null;
    try {
        code = GoogleAuthUtil.getToken(
                LoginActivity.this,                                // Context context
                Plus.AccountApi.getAccountName(mGoogleApiClient),  // String accountName
                scopes                                             // String scope
        );

    } catch (IOException transientEx) {
        Log.e(Constants.TAG, "IOException");
        transientEx.printStackTrace();
        // network or server error, the call is expected to succeed if you try again later.
        // Don't attempt to call again immediately - the request is likely to
        // fail, you'll hit quotas or back-off.
    } catch (UserRecoverableAuthException e) {
        Log.e(Constants.TAG, "UserRecoverableAuthException");
        e.printStackTrace();
        // Requesting an authorization code will always throw
        // UserRecoverableAuthException on the first call to GoogleAuthUtil.getToken
        // because the user must consent to offline access to their data.  After
        // consent is granted control is returned to your activity in onActivityResult
        // and the second call to GoogleAuthUtil.getToken will succeed.
        startActivityForResult(e.getIntent(), AUTH_CODE_REQUEST_CODE);
    } catch (GoogleAuthException authEx) {
        // Failure. The call is not expected to ever succeed so it should not be
        // retried.
        Log.e(Constants.TAG, "GoogleAuthException");
        authEx.printStackTrace();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

    Log.e(Constants.TAG, "ONE TIME CODE: " + code);
    return code;
}

After obtaining the code successfully, send it to my server for authentication. And here's the code on the server:

$code = $_POST['code'];
$client = new Google_Client();
$client->setApplicationName("my_app_name");
$client->setClientId('my_web_client_id');  // Web component's client id
$client->setClientSecret('client_secret'); // Web component's secret
$client->addScope("email");
$client->setAccessType("offline");
$client->authenticate($code);
...

And the problem is that authentication works only once every 10-15 minutes. When trying to obtain the one-time code more than once in 10-15 minutes, i get the same code as the last one(Clearly there is something wrong. This happens only with the android client and i'm getting this error: Error fetching OAuth2 access token, message: 'invalid_grant: i'). Couldn't find anyone with the same problem here in SO. Probably i'm doing something wrong, but can't figure out what is it...Any help would be appreciated.


Solution

  • You shouldn't be sending the code each time. On the web this is kind of OK as when you first consent you'll get a code that gives you offline access (you'll see a refresh token in the response when you exchange it) but in future cases you wont. On Android, you get a code that gives you a refresh token every time, which means you'll need to show the consent every time, and you're likely to run into per-user limits or cache issues (as you seem to be).

    The magic extra component you need is a thing called an ID token. This you can get easily on both platforms and tells you who the person is. Take a look at this blog post for more: http://www.riskcompletefailure.com/2013/11/client-server-authentication-with-id.html

    The limitation with an ID token is that you can't use it to call Google APIs. All it does is give you the Google user ID, the client ID of the app being used and (if email scope is used) the email address. The nice thing is that you can get one really easily on all platforms with less user interaction, and they're cryptographically signed so most of the time you can use them without making any further network calls on the server. If you don't need to make Google API calls (because you're just using it for auth) this is the best thing to use by far - given that you're just getting the email, I would be inclined to stop here.

    If you need to make Google API calls from your server though, then you should use the code - but just once. When you exchange it, you store the refresh token in a database keyed against the user ID. Then, when the user comes back you look up the refresh token and use it to generate a new access token. So the flow would be:

    First time:

    1. Android -> Server: id token
    2. Server -> I have no refresh token!
    3. Android -> Server: code

    Other times:

    1. Android -> Server: id token
    2. Server - I have a code, and can make calls.

    For the web, you can use the same flow or carry on sending the code each time, but you should still keep the refresh token in the database if the response contains one.