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(), or None 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 directories apiclient/ and oauth2client/ from this same package as well as httplib2 from https://github.com/jcgregorio/httplib2 -- and also customize the client_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 use oauth2, and there is no connection between holding an oauth2-authorized token, and the users 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's get to ensure authorization, and with the decorator's authorized http, it fetches
user = service.people().get(userId='me').execute(http=http)
with service built earlier as discovery.build("plus", "v1", http=http) (with a different non-authorized http).
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() returns princess#bride.com or whatever other fake email you input at the fake login screen -- and this in no way inhibits the completely separate oauth2 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 setting scope 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 from service.people().get(userId='me') a user object with (among many other things) an emails 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)).
Related
I'm making a desktop app in Python that sends mail from Gmail. The problem is that after receiving consent (OAuth 2) through the browser, the user for whom the software receives the consent, continues to be logged in to the browser in Gmail. Is there a way to go through the authorization process without staying logged in to Gmail in your browser?
What you are referring to is Oauth2. Oauth2 gives users the ability to grant applications like yours consent to access their private data. Private data is data that is owned by someone. My gmail data is mine your application can not use it unless I grant you access.
Is there a way to go through the authorization process without staying logged in to Gmail in your browser?
Lets clear up some confusion in this statement you mention authorization which is correct a user is authorizing your application to access their data. Yet you also mention logged in which has nothing to do with authorization. Logging in a user is authentication and is not part of Oauth2. It is part of something else called openid connect.
As for how to request authorization of a user without using the browser. Once the user has consented to your application accessing my data once then your application should have what its called a refresh token, this refresh token can be used at a latter time for your application to request a new access token. Granting you access to may data without using the browser to access my data again. So you could store this refresh token in the backend some where and use that to continue to access the users data without needing to use the browser again.
storing user credentials in an installed application
It is hard to know exactly what you are doing since you did not include any code in your question, and your question is a little unclear.
In the following example please note how the users credentials are stored in gmail.dat using this code in an installed application will cause it to load the refresh token the next time the user runs the app meaning that the consent screen should not be shown, as the credentials are already stored for that user.
def initialize_gmail():
"""Initializes the gmail service object.
Returns:
analytics an authorized gmail service object.
"""
# Parse command-line arguments.
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
parents=[tools.argparser])
flags = parser.parse_args([])
# Set up a Flow object to be used if we need to authenticate.
flow = client.flow_from_clientsecrets(
CLIENT_SECRETS_PATH, scope=SCOPES,
message=tools.message_if_missing(CLIENT_SECRETS_PATH))
# Prepare credentials, and authorize HTTP object with them.
# If the credentials don't exist or are invalid run through the native client
# flow. The Storage object will ensure that if successful the good
# credentials will get written back to a file.
storage = file.Storage('gmail.dat')
credentials = storage.get()
if credentials is None or credentials.invalid:
credentials = tools.run_flow(flow, storage, flags)
http = credentials.authorize(http=httplib2.Http())
# Build the service object.
service = build('gmail ', 'v1', http=http)
return service
I'm working on an app using the Spotify API but I'm a bit new to all of this. I'm trying to get the Authorization Code with Proof Key for Code Exchange (PKCE) (https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce)
My problem is how do I redirect the user to the query where he has to ACCEPT the authorization and make my app to wait until the user clicks on ACCEPT. When he does this, the user will be redirected and that new URL (as the docs said) will contain the authorization code that I need to then exchange it for an authorization token.
This is my function so far to get that authorization code:
def get_auth_code(self):
code_challenge = self.get_code_challenge_PKCE()
scopes_needed = "user-read-email%20user-read-private%20playlist-read-collaborative%20playlist-modify-public%20playlist-read-private%20playlist-modify-private%20user-library-modify%20user-library-read"
endpoint = "https://accounts.spotify.com/authorize"
query = f"{endpoint}?client_id={self.client_ID}&response_type=code&redirect_uri={self.redirect_uri}&scope={scopes_needed}&code_challenge_method=S256&code_challenge={code_challenge}"
webbrowser.open(query)
Set up a web server.
To programmatially extract the access tokens you need a web server to handle the redirection after the user logs in on Spotify (which you redirected them to). Now this server can be the user pasting the URI to an input field on a terminal, but obviously this isn't ideal for user experience. It leaves room for lots of mistakes.
I've authored a Spotify Web API client, whose internals might be useful for you to examine. For example, you can use Flask to construct the server. The main principle is using one endpoint (i.e. /login) to redirect (code 307 worked for me browsers won't remember it) the user to a callback (i.e. /callback) which recieves the code parameter with which you can request an access token.
OAuth2 can be a bit of a pain to implement locally, I know. In my library I also made a similar function that you are constructing using webbrowser, but it does have the manual copy-pasting quirk. To use functions you can define yourself for brevity, the gist of it is:
verifier = secrets.token_urlsafe(32) # for PKCE, not in my library yet
url = user_authorisation_url(scope, state, verifier)
# Communicate with the user
print('Opening browser for Spotify login...')
webbrowser.open(url)
redirected = input('Please paste redirect URL: ').strip()
code = parse_code_from_url(redirected)
state_back = parse_state_from_url(redirected)
assert state == state_back # For that added security juice
token = request_user_token(code, verifier)
The client of my Google Cloud Endpoints API is an JavaScript (AngularJS) web application hosted on the same Google App Engine application as the Endpoints API itself. My users authenticate using webapp2 sessions (datastore). They don't necessarily have a Google account. I want to be able to do a request to the Endpoints API like /api/users/me which would return the user data of the user who is currently logged in.
First I thought I had to implement a OAuth2 provider for my App Engine application, and then let the AngularJS application request a OAuth2 access token from my own App Engine OAuth provider (instead of the OAuth provider of Google, like the built in authentication mechanism does).
However, this comment suggests not implementing my own OAuth2 provider but instead providing arbitrary parameters in my request (in a message field, or in a HTTP header) to the Endpoints API. I guess that parameter should be a user token (some encrypted value unique to the logged in user?). That value should then be passed to the browser. Isn't that insecure? I would like not to serve my AngularJS application on HTTPS if possible (to save costs).
Is this a good use case for OAuth2? Or is OAuth2 only for granting third party applications access to user data?
In case OAuth2 is not the way to go: how to pass a user token securily to the browser and prevent man-in-the-middle attacks? Should the user token expire after a certain amount of time?
I've just finished implementing exactly what you've described. Basically this method does the trick:
def get_current_session(request_state):
cookies = werkzeug.http.parse_cookie(request_state.headers.get('Cookie'))
sess_cookie = cookies.get('mc_session')
parts = sess_cookie.split('|')
if len(parts) != 3:
logging.error('Cookie does not have 3 parts')
return False
signature = hmac.new(COOKIE_SECRET_KEY, digestmod=hashlib.sha1)
signature.update('|'.join(parts))
sig_hex = signature.hexdigest()
if compare_hashes(sig_hex, parts[2]):
logging.error('Cookie signature mismatch!')
return False
cookie_data = webapp2_extras.json.b64decode(parts[0])
return sessions_ndb.Session.get_by_sid(cookie_data['_sid'])
And you'd call that from your API method using:
session = get_current_session(self.request_state)
You can find all the details at: https://blog.artooro.com/2014/08/21/share-sessions-between-google-cloud-endpoints-and-webapp2/
this is my first web-programming experience so I hope my questions doesn't sound very dumb. I have been stucked on this for many days.
I am trying to understand a sample code:
https://github.com/facebook/runwithfriends
However I am not understanding very well how the information flow works and how can I modify that sample (i.e. how the code works).
For example, in the following section of the code:
class RecentRunsHandler(BaseHandler):
"""Show recent runs for the user and friends"""
def get(self):
if self.user:
friends = {}
for friend in select_random(
User.get_by_key_name(self.user.friends), 30):
friends[friend.user_id] = friend
self.render(u'runs',
friends=friends,
user_recent_runs=Run.find_by_user_ids(
[self.user.user_id], limit=5),
friends_runs=Run.find_by_user_ids(friends.keys()),
)
else:
self.render(u'welcome')
As I understand (along with HTML) is useful for showing friends that are using the same app, and if I understand correctly, here is the essential part:
*friends_runs=Run.find_by_user_ids(friends.keys())*
But what if I want to show any given friend. How can I do it?
Summarizing, I would like to know:
1- How the flow of the code works? (I don't fully understand the explanation here)
2- How can I manipulate the code so to get, for example, to show a list of friends of the user (not necessary that use the same app)?
Moreover, Can I show friends filtered by some characteristic (for example, gender)?
Thanks a lot!
The python "SDK" for facebook I use I took from https://gist.github.com/1190267
and combined it with the code from the example app to achieve the functionality I wanted both for a canvas app and for website usage.
It depends whether you're using facebook with websites or a canvas application. For a canvas application you probably could do well with the javascript SDK but for a "login with facebook" I required serverside logic that should work with javascript turned off so I've completed that solution with details you might have help to know. You can try make small changes of that specific app 'runwithfriends' to get an understanding which code does what. The project you're looking at contains some outdated practice though:
getting and setting cookies is likely preferable now doing with webapp2's builtin functions for this instead of the code that comes with the FB example app
logging in and out is now done with OAuth 2.0 so it's likely that the login system you're looking at is outdated and you need to use OAuth 2.0 which is described here. I much rather do login/logout serverside so I did an OAuth 2.0 pure python solution to login / logout following the authentication steps mentioned in the tutorial from FB. I had to clear the cookie to log a user out which was not documented.
To upgrade to python 2.7 I had to also modify so that HTTP header did not cast to unicode. I don't know why but otherwise it complained that headers were "not strings"
To more elaborately answer your specific questions:
1) The requesthandler class you posted is a subclass of a BaseHandler so to fully understand what it does you can look at the BaseHandler class since what you are posting is a BAseHandler. The BaseHandler uses django templates for rendering and if you want to can switch the template engine to jinja2 which is remmended. Further the code accesses the user object inherited from the BaseHandler and does some operations on it and renders it to a template. You can try make a requesthandler of your own, subclass BaseHandler and do what you want.
2) I could manipulate the code and I'm not an expert so you should be able to do it too. I wanted a simple FB app to display random images and I could manipulate it to select random images via blobs and render to to a template while keeping the facebook base functions. A function to use for getting the user using the Graph API I do this:
def parse_signed_request(signed_request, secret):
"""
Parse signed_request given by Facebook (usually via POST),
decrypt with app secret.
Arguments:
signed_request -- Facebook's signed request given through POST
secret -- Application's app_secret required to decrpyt signed_request
"""
if '.' in signed_request:
(esig, payload) = signed_request.split('.')
else:
return {}
sig = urlsafe_b64decode(str(esig))
data = _parse_json(urlsafe_b64decode(str(payload)))
if not isinstance(data, dict):
raise SignedRequestError('Pyload is not a json string!')
return {}
if data['algorithm'].upper() == 'HMAC-SHA256':
if hmac.new(secret, payload, hashlib.sha256).digest() == sig:
return data
else:
raise SignedRequestError('Not HMAC-SHA256 encrypted!')
return {}
def get_user_from_cookie(cookies, app_id, app_secret):
"""Parses the cookie set by the official Facebook JavaScript SDK.
cookies should be a dictionary-like object mapping cookie names to
cookie values.
If the user is logged in via Facebook, we return a dictionary with the
keys "uid" and "access_token". The former is the user's Facebook ID,
and the latter can be used to make authenticated requests to the Graph API.
If the user is not logged in, we return None.
Download the official Facebook JavaScript SDK at
http://github.com/facebook/connect-js/. Read more about Facebook
authentication at http://developers.facebook.com/docs/authentication/.
"""
cookie = cookies.get('fbsr_' + app_id, '')
if not cookie:
return None
response = parse_signed_request(cookie, app_secret)
if not response:
return None
args = dict(code=response['code'], client_id=app_id,
client_secret=app_secret, redirect_uri='')
file = \
urllib.urlopen('https://graph.facebook.com/oauth/access_token?'
+ urllib.urlencode(args))
try:
token_response = file.read()
finally:
file.close()
access_token = cgi.parse_qs(token_response)['access_token'][-1]
logging.debug('returning cookie')
return dict(uid=response['user_id'], access_token=access_token)
See http://developers.facebook.com/docs/api for complete documentation for the API. And you can get the the official Facebook JavaScript SDK at http://github.com/facebook/connect-js/
I'm now writing code to sync a webapp2_extras.auth account with facebook so that custom accounts and facebook accounts can co-exist and we're discussing solutions for this in the webapp2 groups and categories. The current way I do it is adding the recommended current_user to a basehandler and using that as the FB identity while working on "merging" my class FBUser that is a custom class for facebook users that autheorized my website and/or canvas application to sync with webapp2_extras.auth.models.User which is an expando model so it can just add the properties it doesn't have such as facebookid, firstname, lastname, etc.
#property
def current_user(self):
if not hasattr(self, '_current_user'):
self._current_user = None
cookie = get_user_from_cookie(self.request.cookies,
facebookconf.FACEBOOK_APP_ID,
facebookconf.FACEBOOK_APP_SECRET)
if cookie:
# Store a local instance of the user data so we don't need
# a round-trip to Facebook on every request
user = FBUser.get_by_key_name(cookie['uid'])
if not user:
graph = GraphAPI(cookie['access_token'])
profile = graph.get_object('me')
user = FBUser(key_name=str(profile['id']),
id=str(profile['id']),
name=profile['name'],
profile_url=profile['link'],
access_token=cookie['access_token'])
user.put()
elif user.access_token != cookie['access_token']:
user.access_token = cookie['access_token']
user.put()
self._current_user = user
return self._current_user
You can also solve your authentication with session objects and build your authentication system around that. That is what I do when using both custom accounts and facebook accounts and you're welcome to have a lok at my repository for more code examples how to intregrate facebook with google app engine using python 2.7.
im using FriendlyFormPlugin, but would like to retrieve the username that was input as part of the request.params, but its no longer there when i check. this way i can set the default for username if the password is incorrect. thanks
I think what you need to do is to setup a post login handler action when you setup the middleware. In that action you can then check params, set a session var, etc. I had to hook into here in order to create a message to the user that their login had failed. I check for a 'login_failed' param on the login form.
def post_login(self):
""" Handle logic post a user's login
I want to create a login_handler that's redirected to after login. This would
check
- if user was logged in, if not then send back to login
- if user is admin, go to job list
- adjust the max age on the existing cookie to XX remember me timeframe
"""
if auth.check(not_anonymous()):
log.debug('checked auth')
else:
# login failed, redirect back to login
log.debug('failed auth')
redirect_to(controller="root", action="login", login_failed=True)
# expire this cookie into the future
ck = request.cookies['authtkt']
response.set_cookie('authtkt', ck,
max_age=60*60*24*7,
path='/'
)
redirect_to(controller="job", action="list")
In response for more details, too big to add as another comment:
So I've got a few things you can look at. First, this is my docs I'm writing as a repoze 'summary' to help explain to other devs how this stuff works/terminology used:
http://72.14.191.199/docs/morpylons/auth_overview.html
I started out using the repoze sql quickstart plugin:
http://code.gustavonarea.net/repoze.what-quickstart/
I then ripped out their setup_sql_auth and modified it for our own needs since we do both SQL and LDAP auth in our apps. Go make sure to look at the plugin source for setup_sql_auth and go through it until you really understand what it's doing.
and since you asked on middleware config...
app = setup_morpace_auth(app, User, Group, Permission, meta.Session,
post_login_url='/root/post_login',
post_logout_url='/login',
log_level='debug',
log_file='stdout'
)