Skip to content

Commit 311a53f

Browse files
Merge pull request googleapis#322 from jonparrott/master
Fixed incremental auth in flask_util.
2 parents b8a6a0f + 07ea7c5 commit 311a53f

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
@@ -165,6 +165,7 @@ def requires_calendar():
165165
import hashlib
166166
import json
167167
import os
168+
import pickle
168169
from functools import wraps
169170

170171
import six.moves.http_client as httplib
@@ -186,12 +187,26 @@ def requires_calendar():
186187
from oauth2client.client import OAuth2WebServerFlow
187188
from oauth2client.client import Storage
188189
from oauth2client import clientsecrets
189-
from oauth2client import util
190190

191191

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

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

196211

197212
class UserOAuth2(object):
@@ -251,7 +266,7 @@ def init_app(self, app, scopes=None, client_secrets_file=None,
251266
self.storage = storage
252267

253268
if scopes is None:
254-
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', DEFAULT_SCOPES)
269+
scopes = app.config.get('GOOGLE_OAUTH2_SCOPES', _DEFAULT_SCOPES)
255270
self.scopes = scopes
256271

257272
self._load_config(client_secrets_file, client_id, client_secret)
@@ -301,7 +316,8 @@ def _load_client_secrets(self, filename):
301316
client_type, client_info = clientsecrets.loadfile(filename)
302317
if client_type != clientsecrets.TYPE_WEB:
303318
raise ValueError(
304-
'The flow specified in %s is not supported.' % client_type)
319+
'The flow specified in {0} is not supported.'.format(
320+
client_type))
305321

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

314-
session['google_oauth2_csrf_token'] = csrf_token
330+
session[_CSRF_KEY] = csrf_token
315331

316332
state = json.dumps({
317333
'csrf_token': csrf_token,
@@ -321,17 +337,22 @@ def _make_flow(self, return_url=None, **kwargs):
321337
kw = self.flow_kwargs.copy()
322338
kw.update(kwargs)
323339

324-
extra_scopes = util.scopes_to_string(kw.pop('scopes', ''))
325-
scopes = ' '.join([util.scopes_to_string(self.scopes), extra_scopes])
340+
extra_scopes = kw.pop('scopes', [])
341+
scopes = set(self.scopes).union(set(extra_scopes))
326342

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

351+
flow_key = _FLOW_KEY.format(csrf_token)
352+
session[flow_key] = pickle.dumps(flow)
353+
354+
return flow
355+
335356
def _create_blueprint(self):
336357
bp = Blueprint('oauth2', __name__)
337358
bp.add_url_rule('/oauth2authorize', 'authorize', self.authorize_view)
@@ -368,11 +389,12 @@ def callback_view(self):
368389
if 'error' in request.args:
369390
reason = request.args.get(
370391
'error_description', request.args.get('error', ''))
371-
return 'Authorization failed: %s' % reason, httplib.BAD_REQUEST
392+
return ('Authorization failed: {0}'.format(reason),
393+
httplib.BAD_REQUEST)
372394

373395
try:
374396
encoded_state = request.args['state']
375-
server_csrf = session['google_oauth2_csrf_token']
397+
server_csrf = session[_CSRF_KEY]
376398
code = request.args['code']
377399
except KeyError:
378400
return 'Invalid request', httplib.BAD_REQUEST
@@ -387,14 +409,17 @@ def callback_view(self):
387409
if client_csrf != server_csrf:
388410
return 'Invalid request state', httplib.BAD_REQUEST
389411

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

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

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

413-
if not hasattr(ctx, 'google_oauth2_credentials'):
438+
if not hasattr(ctx, _CREDENTIALS_KEY):
414439
ctx.google_oauth2_credentials = self.storage.get()
415440

416441
return ctx.google_oauth2_credentials
@@ -433,7 +458,7 @@ def email(self):
433458
return self.credentials.id_token['email']
434459
except KeyError:
435460
current_app.logger.error(
436-
'Invalid id_token %s', self.credentials.id_token)
461+
'Invalid id_token {0}'.format(self.credentials.id_token))
437462

438463
@property
439464
def user_id(self):
@@ -449,7 +474,7 @@ def user_id(self):
449474
return self.credentials.id_token['sub']
450475
except KeyError:
451476
current_app.logger.error(
452-
'Invalid id_token %s', self.credentials.id_token)
477+
'Invalid id_token {0}'.format(self.credentials.id_token))
453478

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

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

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

497-
return wrapped_function(*args, **kwargs)
498-
499526
return required_wrapper
500527

501528
if decorated_function:
@@ -531,7 +558,7 @@ class FlaskSessionStorage(Storage):
531558
"""
532559

533560
def locked_get(self):
534-
serialized = session.get('google_oauth2_credentials')
561+
serialized = session.get(_CREDENTIALS_KEY)
535562

536563
if serialized is None:
537564
return None
@@ -542,8 +569,8 @@ def locked_get(self):
542569
return credentials
543570

544571
def locked_put(self, credentials):
545-
session['google_oauth2_credentials'] = credentials.to_json()
572+
session[_CREDENTIALS_KEY] = credentials.to_json()
546573

547574
def locked_delete(self):
548-
if 'google_oauth2_credentials' in session:
549-
del session['google_oauth2_credentials']
575+
if _CREDENTIALS_KEY in session:
576+
del session[_CREDENTIALS_KEY]

0 commit comments

Comments
 (0)