I am developing Google App Engine application written in Python and using Endpoints API. In conjunction, I am writing a Chrome Extension to interact with the Endpoints API. I've been running into lots of issues with the Endpoints API and authorization. Currently, here is my setup:
Endpoints API (Python)
from google.appengine.ext import endpoints
from protorpc import message_types
from protorpc import remote
ALLOWED_CLIENT_IDS = ['client_id_from_google_api_console',
endpoints.API_EXPLORER_CLIENT_ID]
@endpoints.api(name='my_api',version='v1', description='My API',
allowed_client_ids=ALLOWED_CLIENT_IDS)
class MyApi(remote.Service):
@endpoints.method(message_types.VoidMessage, DeviceListResponse,
name='user.device.list', path='user/device/list',
http_method='GET')
def user_device_list(self, request):
current_user = endpoints.get_current_user()
if current_user is None:
raise endpoints.UnauthorizedException('You must authenticate first.')
if current_user.user_id() is None:
raise endpoints.NotFoundException("Your user id was not found.")
return DeviceListResponse(devices=[]) #Hypothetically return devices
api_service = endpoints.api_server([MyApi], restricted=False)
Google API Console
The JS origins include:
chrome-extensions://chrome_app_id
CHROME EXTENSION (JS)
var apiRoot = "https://my_app_id.appspot.com/_ah/api";
var clientID = "client_id_from_google_api_console";
var oauthScopes = ["https://www.googleapis.com/auth/userinfo.email"];
var responseType = "token id_token";
//Helper method to log to the console
function l(o){console.log(o);}
function oauthSignin(mode) {
gapi.auth.authorize({client_id: clientID, scope: oauthScopes,
immediate: mode, response_type: responseType}, function() {
var request = gapi.client.oauth2.userinfo.get();
request.execute(function(resp) {
authenticated = !resp.code;
if(authenticated) {
var token = gapi.auth.getToken();
token.access_token = token.id_token;
gapi.auth.setToken(token);
l("Successfully authenticated. Loading device list");
gapi.client.my_api.user.device.list({}).execute(function(resp) {
if(resp.code) {
l("Response from device list: " + resp.message);
}
l(resp);
});
}
});
});
}
//This get's called when the page and js library has loaded.
function jsClientLoad() {
l("JS Client Libary loaded. Now loading my_api and oauth2 APIs.");
var apisToLoad;
var callback = function() {
if (--apisToLoad == 0) {
l("APIs have loaded.")
oauthSignin(true);
} else {
l("Waiting for " + apisToLoad + " API" + (apisToLoad>1?"s":"") + " to load.");
}
}
apisToLoad = 2; // must match number of calls to gapi.client.load()
gapi.client.load('my_api', 'v1', callback, apiRoot);
gapi.client.load('oauth2', 'v2', callback);
}
Result
Now that I have shown the main chunk of my code (note, I had to change it a bit to make sense without uploading entire code), if I go to the Google API Explorer and run that method, I get a 200 response. If I run it in the Chrome Extension, I get a 404 code with the message "Your user id was not found.".
It's unclear why/when this ever results in a 200
; it should not. As mentioned in Function User.getUserId() in Cloud endpoint api returns null for a user object that is not null, this is a known issue.
TLDR;
The user_id
will never be populated in the result returned from endpoints.get_current_user()
. A workaround exists: by storing the user object in the datastore and then retrieving it (with a new get, if you are using ndb
), the user_id()
value will be populated.
You should strongly consider using the Google Profile ID associated with the account instead of the App Engine User ID.
History/Explanation:
endpoints
is meant to be used with both Bearer tokens and ID tokens (for Android). ID tokens are a special type of JWT (JSON web token) signed in conjunction with on device crypto. As a result, parsing the user from these tokens can only determine the information encoded in that token (see Cloud endpoints oauth2 error for more info about that).
Since these tokens are minted by a generic Google Auth provider (OAuth 2.0) outside of App Engine, the App Engine User ID is not known/shared by this service. As a result, it is never possible to populate the user_id()
when an ID token is used to sign the request.
When a standard Bearer token is used (which would be fine for your Chrome application), the App Engine OAuth API is used. When the OAuth API calls
oauth.get_current_user(some_scope)
(where oauth
is google.appengine.api.oauth
), the
oauth.oauth_api._maybe_call_get_oauth_user(_scope=None)
method is called. This makes an RPC to a shared App Engine layer which provides a service that is able to get the current user from the token. In this case, the user_id()
of the returned user WILL be set, however, the user value is not kept around for endpoints.get_current_user
, only the email and the auth domain are.
Another Workaround:
The oauth.get_current_user()
call is only expensive IF it makes the RPC. The _maybe_call_get_oauth_user
method stores the value from the last call, so calling oauth.get_current_user()
a second time will incur no network/speed overhead other than the few nanoseconds to lookup a value from a Python dict
.
This is crucial because endpoints.get_current_user()
uses a call to oauth.get_current_user()
to determine the Bearer token user, so if you wanted to call it again, you'd worry about that performance.
If you know you'll never be using ID tokens or can easily determine those situations, you could change your code to just call both:
endpoints_user = endpoints.get_current_user()
if endpoints_user is None:
raise endpoints.UnauthorizedException(...)
oauth_user = oauth.get_current_user(known_scope)
if oauth_user is None or oauth_user.user_id() is None:
# This should never happen
raise endpoints.NotFoundException(...)
NOTE: We still must call endpoints.get_current_user()
because it always makes sure that our token has been minted only for one of the specific scopes we've allowed and for one of the specific client IDs we have whitelisted to talk to our application.
NOTE: The value known_scope
will vary depending on which of your possible scopes matches the token. Your list of scopes will be looped through in one of the endpoints.get_current_user()
helper methods, and if this succeeds, the final matching scope will be stored as os.getenv('OAUTH_LAST_SCOPE')
. I would strongly recommend using this value for known_scope
.
Better Alternative:
As mentioned, the App Engine User ID simply can't be implied from an ID token (at current), however, the Google Profile ID can be used instead of the App Engine User ID. (This ID is often seen as the Google+ ID, though this is consistent across many services.)
To make sure this value is associated with your Bearer OR ID tokens, make sure you also request one of the non-userinfo.email
scopes associated with the userinfo
API:
https://www.googleapis.com/auth/plus.login
https://www.googleapis.com/auth/plus.me
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
(This list of scopes current as of this writing on May 20, 2013.)
Similarly as with the App Engine User ID in the Bearer token case, this Google Profile ID is discarded by endpoints.get_current_user()
, BUT it is available for both kinds of tokens.
The get_google_plus_user_id()
method which is part of the appengine-picturesque-python
sample patches one of the endpoints.get_current_user()
helper methods to keep this data around and allows you to use this value without having to repeat the expensive network calls used to validate the Bearer or ID token from the request.
Just in case anyone is here since 1.8.6 and is still trying to use the auth_util.py
work around to return the Google profile id. endpoints.token_id
now has two methods depending on if the user is on the development server or not.
When on Google's servers the flow returns the oauth_user
and does not hit the tokeninfo endpoint. Therefore no token info is saved in auth_util.py
. However, on the dev server it does hit the tokeninfo endpoint so works as expected.
For me the easiest way to solve this was just to monkey patch endpoints.token_id._is_local_dev
and set that to always be true.
I have just recently run into this headache. However, after some testing it appears that with 1.9.2 user.user_id()
returns None
on the local development server. However, if you deploy, it will return a value. Not sure if this is the App Engine ID or Google+ ID though. Anyway to find out?
However, given the documentation, I assume the ID it does return is safe to use for persistent records.