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 john.doe@gmail.com 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