I am working on a webapp based on google app engine. The application uses the google authentication apis. Basically every handler extends from this BaseHandler and as first operation of any get/post the checkAuth is executed.
class BaseHandler(webapp2.RequestHandler):
googleUser = None
userId = None
def checkAuth(self):
user = users.get_current_user()
self.googleUser = user;
if user:
self.userId = user.user_id()
userKey=ndb.Key(PROJECTNAME, 'rootParent', 'Utente', self.userId)
dbuser = MyUser.query(MyUser.key==userKey).get(keys_only=True)
if dbuser:
pass
else:
self.redirect('/')
else:
self.redirect('/')
The idea is that it redirects to / if no user is logged in via Google OR if there is not a User in my db of users having that google id.
The problem is that I can succesfully log in my web app and make operations. Then, from gmail, o Logout from any google account BUT if i try to keep using the web app it works. This means the users.get_current_user() still returns a valid user (valid but actually OLD). Is that possible?
IMPORTANT UPDATE I Do Understand what explained in the Alex Martelli's Comment: There is a cookie which keeps the former GAE authentication valid. The problem is that the same web app also exploits the Google Api Client Library for Python https://developers.google.com/api-client-library/python/ to perform operations on Drive and Calendar. In GAE apps such library can be easily used through decorators implementing the whole OAuth2 Flow (https://developers.google.com/api-client-library/python/guide/google_app_engine).
I therefore have my Handlers get/post methods decorated with oauth_required like this
class SomeHandler(BaseHandler):
@DECORATOR.oauth_required
def get(self):
super(SomeHandler,self).checkAuth()
uid = self.googleUser.user_id()
http = DECORATOR.http()
service = build('calendar', 'v3')
calendar_list = service.calendarList().list(pageToken=page_token).execute(http=http)
Where decorator is
from oauth2client.appengine import OAuth2Decorator
DECORATOR = OAuth2Decorator(
client_id='XXXXXX.apps.googleusercontent.com',
client_secret='YYYYYYY',
scope='https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.appdata https://www.googleapis.com/auth/drive.file'
)
It usually works fine. However (!!) when the app is idle for a long time it happens that the oauth2 decorator redirects me to the Google authentication page where, if I change account (I have 2 different accounts) Something WEIRD happens: The app is still logged as the former account (retrieved through users.get_current_user()) while the api client library, and thus the oauth2 decorator, returns data (drive, calendar, etc.) belonging to the second account.
Which is REALLY not appropriate.
Following the example above (SomeHandler class) suppose I am logged as Account A. The users.get_current_user() always returns A as expected. Now suppose I stopped using the app, after a long while the oauth_required redirects me to the Google Account page. I therefore decide (or make a mistake) to log is as Account B. When accessing the Get method of the SomeHandler class the userId (retrived through users.get_current_user() is A while the list of calendars returned through the service object (Google Api client Library) is the list of calendars belonging to B (the actual currently logged user).
Am I doing something wrong? is Something expected?
Another Update
this is after the Martelli's Answer. I have updated the handlers like this:
class SomeHandler(BaseHandler):
@DECORATOR.oauth_aware
def get(self):
if DECORATOR.has_credentials():
super(SomeHandler,self).checkAuth()
uid = self.googleUser.user_id()
try:
http = DECORATOR.http()
service = build('calendar', 'v3')
calendar_list = service.calendarList().list(pageToken=page_token).execute(http=http)
except (AccessTokenRefreshError, appengine.InvalidXsrfTokenError):
self.redirect(users.create_logout_url(
DECORATOR.authorize_url()))
else:
self.redirect(users.create_logout_url(
DECORATOR.authorize_url()))
so basically I now use oauth_aware and, in case of none credentials I logout the user and redirect it to the DECORATOR.authorize_url()
I have noticed that after a period of inactivity, the handler raises AccessTokenRefreshError and appengine.InvalidXsrfTokenError exceptions (but the has_credentials() method returns True). I catch them and (again) redirect the flow to the logout and authorize_url()
It seems to work and seems to be robust to accounts switch. Is it a reasonable solution or am I not considering some aspects of the issue?
I understand the confusion, but the system is "working as designed".
At any point in time a GAE handler can have zero or one "logged-in user" (the object returned by
users.get_current_user()
, orNone
if no logged-in user) and zero or more "oauth2 authorization tokens" (for whatever users and scopes have been granted and not revoked).There is no constraint that forces the oauth2 thingies to match, in any sense, the "logged-in user, if any".
I would recommend checking out the very simple sample at https://code.google.com/p/google-api-python-client/source/browse/samples/appengine/main.py (to run it, you'll have to clone the whole "google-api-python-client" package, then copy into the
google-api-python-client/source/browse/samples/appengine
directory directoriesapiclient/
andoauth2client/
from this same package as well ashttplib2
from https://github.com/jcgregorio/httplib2 -- and also customize theclient_secrets.json
-- however, you don't need to run it, just to read and follow the code).This sample doesn't even use
users.get_current_user()
-- it doesn't need it nor care about it: it only shows how to useoauth2
, and there is no connection between holding anoauth2
-authorized token, and theusers
service. (This allows you for example to have cron execute on behalf of one or more users certain tasks later -- cron doesn't log in, but it doesn't matter -- if the oauth2 tokens are properly stored and retrieved then it can use them).So the code makes a decorator from the client secrets, with
scope='https://www.googleapis.com/auth/plus.me'
, then uses@decorator.oauth_required
on a handler'sget
to ensure authorization, and with the decorator's authorizedhttp
, it fetcheswith
service
built earlier asdiscovery.build("plus", "v1", http=http)
(with a different non-authorizedhttp
).Should you run this locally, it's easy to add a fake login (remember, user login is faked with dev_appserver) so that
users.get_current_user()
returnsprincess@bride.com
or whatever other fake email you input at the fake login screen -- and this in no way inhibits the completely separateoauth2
flow from still performing as intended (i.e, exactly the same way as it does without any such fake login).If you deploy the modified app (with an extra user login) to production, the login will have to be a real one -- but it's just as indifferent to, and separate from, the
oauth2
part of the app.If your application's logic does require constraining the
oauth2
token to the specific user who's also logged into your app, you'll have to implement this yourself -- e.g by settingscope
to'https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/plus.profile.emails.read'
(plus whatever else you need), you'll get fromservice.people().get(userId='me')
auser
object with (among many other things) anemails
attribute in which you can check that the authorization token is for the user with the email you intended to authorize (and take remedial action otherwise, e.g via a logout URL &c). ((This can be done more simply and in any case I doubt you really need such functionality, but, just wanted to mention it)).