Skip to content

Commit 07ea7c5

Browse files
author
Jon Wayne Parrott
committed
Fixing incremental auth in flask_util.
* Flow is now stored in the session, ensuring that the scopes survive the round trip to the auth server. * The base credentials are now checked in `required`, solving an issue where it's possible to pass the check with the incremental scopes but without the base scopes. Fixes googleapis#320
1 parent 55c2bcc commit 07ea7c5

File tree

2 files changed

+191
-114
lines changed

2 files changed

+191
-114
lines changed

oauth2client/flask_util.py

Lines changed: 61 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
Configuration
2424
=============
2525
26-
To configure, you'll need a set of OAuth2 client ID from the
26+
To configure, you'll need a set of OAuth2 web application credentials from the
2727
`Google Developer's Console <https://console.developers.google.com/project/_/\
2828
apiui/credential>`__.
2929
@@ -164,6 +164,7 @@ def requires_calendar():
164164
import hashlib
165165
import json
166166
import os
167+
import pickle
167168
from functools import wraps
168169

169170
import six.moves.http_client as httplib
@@ -185,12 +186,26 @@ def requires_calendar():
185186
from oauth2client.client import OAuth2WebServerFlow
186187
from oauth2client.client import Storage
187188
from oauth2client import clientsecrets
188-
from oauth2client import util
189189

190190

191191
__author__ = '[email protected] (Jon Wayne Parrott)'
192192

193-
DEFAULT_SCOPES = ('email',)
193+
_DEFAULT_SCOPES = ('email',)
194+
_CREDENTIALS_KEY = 'google_oauth2_credentials'
195+
_FLOW_KEY = 'google_oauth2_flow_{0}'
196+
_CSRF_KEY = 'google_oauth2_csrf_token'
197+
198+
199+
def _get_flow_for_token(csrf_token):
200+
"""Retrieves the flow instance associated with a given CSRF token from
201+
the Flask session."""
202+
flow_pickle = session.get(
203+
_FLOW_KEY.format(csrf_token), None)
204+
205+
if flow_pickle is None:
206+
return None
207+
else:
208+
return pickle.loads(flow_pickle)
194209

195210

196211
class UserOAuth2(object):
@@ -250,7 +265,7 @@ def init_app(self, app, scopes=None, client_secrets_file=None,
250265
self.storage = storage
251266

252267
if scopes is None:
253-
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', DEFAULT_SCOPES)
268+
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
254269
self.scopes = scopes
255270

256271
self._load_config(client_secrets_file, client_id, client_secret)
@@ -300,7 +315,8 @@ def _load_client_secrets(self, filename):
300315
client_type, client_info = clientsecrets.loadfile(filename)
301316
if client_type != clientsecrets.TYPE_WEB:
302317
raise ValueError(
303-
'The flow specified in %s is not supported.' % client_type)
318+
'The flow specified in {0} is not supported.'.format(
319+
client_type))
304320

305321
self.client_id = client_info['client_id']
306322
self.client_secret = client_info['client_secret']
@@ -310,7 +326,7 @@ def _make_flow(self, return_url=None, **kwargs):
310326
# Generate a CSRF token to prevent malicious requests.
311327
csrf_token = hashlib.sha256(os.urandom(1024)).hexdigest()
312328

313-
session['google_oauth2_csrf_token'] = csrf_token
329+
session[_CSRF_KEY] = csrf_token
314330

315331
state = json.dumps({
316332
'csrf_token': csrf_token,
@@ -320,17 +336,22 @@ def _make_flow(self, return_url=None, **kwargs):
320336
kw = self.flow_kwargs.copy()
321337
kw.update(kwargs)
322338

323-
extra_scopes = util.scopes_to_string(kw.pop('scopes', ''))
324-
scopes = ' '.join([util.scopes_to_string(self.scopes), extra_scopes])
339+
extra_scopes = kw.pop('scopes', [])
340+
scopes = set(self.scopes).union(set(extra_scopes))
325341

326-
return OAuth2WebServerFlow(
342+
flow = OAuth2WebServerFlow(
327343
client_id=self.client_id,
328344
client_secret=self.client_secret,
329345
scope=scopes,
330346
state=state,
331347
redirect_uri=url_for('oauth2.callback', _external=True),
332348
**kw)
333349

350+
flow_key = _FLOW_KEY.format(csrf_token)
351+
session[flow_key] = pickle.dumps(flow)
352+
353+
return flow
354+
334355
def _create_blueprint(self):
335356
bp = Blueprint('oauth2', __name__)
336357
bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
@@ -367,11 +388,12 @@ def callback_view(self):
367388
if 'error' in request.args:
368389
reason = request.args.get(
369390
'error_description', request.args.get('error', ''))
370-
return 'Authorization failed: %s' % reason, httplib.BAD_REQUEST
391+
return ('Authorization failed: {0}'.format(reason),
392+
httplib.BAD_REQUEST)
371393

372394
try:
373395
encoded_state = request.args['state']
374-
server_csrf = session['google_oauth2_csrf_token']
396+
server_csrf = session[_CSRF_KEY]
375397
code = request.args['code']
376398
except KeyError:
377399
return 'Invalid request', httplib.BAD_REQUEST
@@ -386,14 +408,17 @@ def callback_view(self):
386408
if client_csrf != server_csrf:
387409
return 'Invalid request state', httplib.BAD_REQUEST
388410

389-
flow = self._make_flow()
411+
flow = _get_flow_for_token(server_csrf)
412+
413+
if flow is None:
414+
return 'Invalid request state', httplib.BAD_REQUEST
390415

391416
# Exchange the auth code for credentials.
392417
try:
393418
credentials = flow.step2_exchange(code)
394419
except FlowExchangeError as exchange_error:
395420
current_app.logger.exception(exchange_error)
396-
content = 'An error occurred: %s' % (exchange_error,)
421+
content = 'An error occurred: {0}'.format(exchange_error)
397422
return content, httplib.BAD_REQUEST
398423

399424
# Save the credentials to the storage.
@@ -409,7 +434,7 @@ def credentials(self):
409434
"""The credentials for the current user or None if unavailable."""
410435
ctx = _app_ctx_stack.top
411436

412-
if not hasattr(ctx, 'google_oauth2_credentials'):
437+
if not hasattr(ctx, _CREDENTIALS_KEY):
413438
ctx.google_oauth2_credentials = self.storage.get()
414439

415440
return ctx.google_oauth2_credentials
@@ -432,7 +457,7 @@ def email(self):
432457
return self.credentials.id_token['email']
433458
except KeyError:
434459
current_app.logger.error(
435-
'Invalid id_token %s', self.credentials.id_token)
460+
'Invalid id_token {0}'.format(self.credentials.id_token))
436461

437462
@property
438463
def user_id(self):
@@ -448,7 +473,7 @@ def user_id(self):
448473
return self.credentials.id_token['sub']
449474
except KeyError:
450475
current_app.logger.error(
451-
'Invalid id_token %s', self.credentials.id_token)
476+
'Invalid id_token {0}'.format(self.credentials.id_token))
452477

453478
def authorize_url(self, return_url, **kwargs):
454479
"""Creates a URL that can be used to start the authorization flow.
@@ -473,28 +498,30 @@ def required(self, decorated_function=None, scopes=None,
473498
def curry_wrapper(wrapped_function):
474499
@wraps(wrapped_function)
475500
def required_wrapper(*args, **kwargs):
476-
477501
return_url = decorator_kwargs.pop('return_url', request.url)
478502

479-
# No credentials, redirect for new authorization.
480-
if not self.has_credentials():
503+
requested_scopes = set(self.scopes)
504+
if scopes is not None:
505+
requested_scopes |= set(scopes)
506+
if self.has_credentials():
507+
requested_scopes |= self.credentials.scopes
508+
509+
requested_scopes = list(requested_scopes)
510+
511+
# Does the user have credentials and does the credentials have
512+
# all of the needed scopes?
513+
if (self.has_credentials() and
514+
self.credentials.has_scopes(requested_scopes)):
515+
return wrapped_function(*args, **kwargs)
516+
# Otherwise, redirect to authorization
517+
else:
481518
auth_url = self.authorize_url(
482519
return_url,
483-
scopes=scopes,
520+
scopes=requested_scopes,
484521
**decorator_kwargs)
485-
return redirect(auth_url)
486522

487-
# Existing credentials but mismatching scopes, redirect for
488-
# incremental authorization.
489-
if scopes and not self.credentials.has_scopes(scopes):
490-
auth_url = self.authorize_url(
491-
return_url,
492-
scopes=list(self.credentials.scopes) + scopes,
493-
**decorator_kwargs)
494523
return redirect(auth_url)
495524

496-
return wrapped_function(*args, **kwargs)
497-
498525
return required_wrapper
499526

500527
if decorated_function:
@@ -530,7 +557,7 @@ class FlaskSessionStorage(Storage):
530557
"""
531558

532559
def locked_get(self):
533-
serialized = session.get('google_oauth2_credentials')
560+
serialized = session.get(_CREDENTIALS_KEY)
534561

535562
if serialized is None:
536563
return None
@@ -541,8 +568,8 @@ def locked_get(self):
541568
return credentials
542569

543570
def locked_put(self, credentials):
544-
session['google_oauth2_credentials'] = credentials.to_json()
571+
session[_CREDENTIALS_KEY] = credentials.to_json()
545572

546573
def locked_delete(self):
547-
if 'google_oauth2_credentials' in session:
548-
del session['google_oauth2_credentials']
574+
if _CREDENTIALS_KEY in session:
575+
del session[_CREDENTIALS_KEY]

0 commit comments

Comments
 (0)