Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use the token returned by GoogleAuthUtil.getToken with my App Engine backend

What I want to do

I have a simple Google App Engine backend and a simple Android Application and I want to do authenticated requests from the Android App to the server. I read about Google Cloud Endpoints and even if it is a really good API, I feel that it's a bit overkill for what I want to do. I just want to do an authenticated HTTP request and get the response text.

GET myappid.appspot.com/api/user

Should answer:

Hello john.doe

If the user [email protected] does the request.

Backend side:

I created a new App Engine project:

WEB_CLIENT_ID=123456789012.apps.googleusercontent.com

and registered an Android App ("Accessing APIs directly from Android"):

package name : com.myappid
debug SHA1 fingerprint: 3a:e1:05:17:15:54:c6:c7:9b:ef:19:74:ae:5b:f7:0f:c3:d5:45:9d

And this created

ANDROID_CLIENT_ID=123456789012-9f4sd525df3254s3d5s40s441df705sd.apps.googleusercontent.com

app.yaml

application: myappid
version: 1
runtime: python27
api_version: 1
threadsafe: true    

handlers:
- url: /api/.*
    secure: always
    script: api.APP

libraries:
- name: webapp2
    version: latest
- name: pycrypto
    version: latest

api.py

import webapp2
from google.appengine.api import users
from google.appengine.api import oauth

class GetUser(webapp2.RequestHandler):

    def get(self):
        user = users.get_current_user()
        self.response.headers['Content-Type'] = 'text/plain'
        self.response.out.write('Hello, {}\n'.format('None' if user is None else user.nickname()))
        try:
            user = oauth.get_current_user()
            self.response.out.write('Hello OAuth, {}\n'.format('None' if user is None else user.nickname()))
        except Exception as e:
            self.response.out.write(str(e)+'\n')

class SignIn(webapp2.RequestHandler):

    def get(self):
        if users.get_current_user() is None:
            self.redirect(users.create_login_url(self.request.uri))

APP = webapp2.WSGIApplication([
    ('/api/user', GetUser),
    ('/api/signin', SignIn),
], debug = True)

Android side

public class MainActivity extends Activity
{
    private static final String CLIENT_ID = "123456789012.apps.googleusercontent.com";
    private static final String SCOPE = "audience:server:client_id:" + CLIENT_ID;
    private static final int AUTH_REQUEST_CODE = 1;
    private Account mAccount;

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mAccount = AccountManager.get(mActivity).getAccountsByType(GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE)[0];
        new GetAuthToken().execute(mAccount.name);
    }

    protected void log(String msg) {
        TextView tv = (TextView) mActivity.findViewById(R.id.textView);
        tv.setText(tv.getText() + "\n" + msg);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == AUTH_REQUEST_CODE) {
            if (resultCode == RESULT_OK) {
                new GetAuthToken().execute(mAccount.name);
            }
        }
    }

    private class GetAuthToken extends AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... params) {
            try {
                // Retrieve a token for the given account and scope. It will always return either
                // a non-empty String or throw an exception.
                String email = params[0];
                String token = GoogleAuthUtil.getToken(mActivity, email, SCOPE);
                return token;
            } catch (GooglePlayServicesAvailabilityException playEx) {
                Dialog alert = GooglePlayServicesUtil.getErrorDialog(playEx.getConnectionStatusCode(), mActivity, AUTH_REQUEST_CODE);
                return "error - Play Services needed " + playEx;
            } catch (UserRecoverableAuthException userAuthEx) {
                // Start the user recoverable action using the intent returned by
                // getIntent()
                mActivity.startActivityForResult(userAuthEx.getIntent(), AUTH_REQUEST_CODE);
                return "error - Autorization needed " + userAuthEx;
            } catch (IOException transientEx) {
                // 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.
                return "error - Network error " + transientEx;
            } catch (GoogleAuthException authEx) {
                // Failure. The call is not expected to ever succeed so it should not be
                // retried.
                return "error - Other auth error " + authEx;
            }
        }

        @Override
        protected void onPostExecute(String result) {
            if (result.startsWith("error -")) {
                log(result);
            } else {
                log("Obtained token : " + result);
                new GetAuthedUserName().execute(result);
            }
        }
    }

    private class GetAuthedUserName extends AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... params) {
            try {
                String token = params[0];
                URL url = new URL("https://myappid.appspot.com/api/user");
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                //conn.setRequestProperty("Authorization", "Bearer " + token);
                conn.addRequestProperty("Authorization",  "OAuth " + token);
                InputStream istream = conn.getInputStream();
                try {
                    BufferedReader reader = new BufferedReader(new InputStreamReader(istream));
                    String line;
                    StringBuilder sb = new StringBuilder();
                    while ((line = reader.readLine()) != null) {
                        sb.append(line);
                    }
                    return sb.toString();
                } catch (IOException e) {
                    return "error - Unable to read from the connection";
                }
            } catch (MalformedURLException e) {
                return "error - Malformed URL " + e;
            } catch (IOException e) {
                return "error - IO error " + e;
            }
        }

        @Override
        protected void onPostExecute(String result) {
            if (result.startsWith("error -")) {
                log(result);
            } else {
                log("Request result : " + result);
            }
        }
    }
}

What works

I can use my browser, to to

https://myappid.appspot.com/api/signin

login as John Doe and then

https://myappid.appspot.com/api/user

And I get

Hello, john.doe

Fantastic it's exactly what I expect.

What doesn't work

With Android, I all my tries resulted in

Hello, None

As you can see in the Android code, I use GoogleAuthUtil to retrieve a token but I don't really understand what I'm supposed to do with it.

String token = GoogleAuthUtil.getToken(mActivity, email, SCOPE);

Then I build the request:

URL url = new URL("https://myappid.appspot.com/api/user");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();

And add the "Authorization" header:

conn.setRequestProperty("Authorization", "Bearer " + token);

I also tried:

conn.addRequestProperty("Authorization",  "OAuth " + token);

There is probably something missing on Android or on the App Engine backend but I really don't get what. Is there a piece of API that simplifies this ?

It seems so simple with a browser...

TIA

like image 404
alberthier Avatar asked Dec 18 '13 14:12

alberthier


1 Answers

It is possible to send the access token to your Google App Engine app (or any other web application) (as a bearer token, it's all that is needed to forward credentials) however Google App Engine won't automatically recognize the "Authorization" header and set the user object for you (that's something Endpoints can help you with).

You could choose to find the access token yourself via the request headers object:

access_token = self.request.headers['Authorization']

Then send that to a Google API to verify it's valid and get information about that user (I think this includes email so long as email is a scope that you originally asked for an access token for).

See Get user info via Google API for details on how to do this.

You should also check that the access token has been issued to your application (https://www.googleapis.com/oauth2/v1/tokeninfo?access_token={access_token} - verify client ID in the response) - if you don't that makes it very easy for another application that has permission from the user to get an access token to make calls against your private API.

That all said, another mechanism is to get an IDToken from Android, and send that to your web application- more details can be found here: http://googledevelopers.blogspot.com/2013/05/cross-platform-sso-technology.html and https://developers.google.com/accounts/docs/CrossClientAuth

Sample showing using Google API Python Client to get information about an issued token:

from apiclient.discovery import build
print build('oauth2', 'v1').tokeninfo(access_token=access_token).execute()

# Result
{
  'issued_to': 'xxxxxx.apps.googleusercontent.com',
  'user_id': 'yyyyyy',
  'expires_in': 3457,
  'access_type': 'online',
  'audience': 'xxxxxx.apps.googleusercontent.com',
  'scope': 'https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile',
  'email': '[email protected]',
  'verified_email': True
}
like image 86
aeijdenberg Avatar answered Oct 10 '22 04:10

aeijdenberg