23
23
Configuration
24
24
=============
25
25
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
27
27
`Google Developer's Console <https://console.developers.google.com/project/_/\
28
28
apiui/credential>`__.
29
29
@@ -165,6 +165,7 @@ def requires_calendar():
165
165
import hashlib
166
166
import json
167
167
import os
168
+ import pickle
168
169
from functools import wraps
169
170
170
171
import six .moves .http_client as httplib
@@ -186,12 +187,26 @@ def requires_calendar():
186
187
from oauth2client .client import OAuth2WebServerFlow
187
188
from oauth2client .client import Storage
188
189
from oauth2client import clientsecrets
189
- from oauth2client import util
190
190
191
191
192
192
__author__ = '[email protected] (Jon Wayne Parrott)'
193
193
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 )
195
210
196
211
197
212
class UserOAuth2 (object ):
@@ -251,7 +266,7 @@ def init_app(self, app, scopes=None, client_secrets_file=None,
251
266
self .storage = storage
252
267
253
268
if scopes is None :
254
- scopes = app .config .get ('GOOGLE_OAUTH2_SCOPES' , DEFAULT_SCOPES )
269
+ scopes = app .config .get ('GOOGLE_OAUTH2_SCOPES' , _DEFAULT_SCOPES )
255
270
self .scopes = scopes
256
271
257
272
self ._load_config (client_secrets_file , client_id , client_secret )
@@ -301,7 +316,8 @@ def _load_client_secrets(self, filename):
301
316
client_type , client_info = clientsecrets .loadfile (filename )
302
317
if client_type != clientsecrets .TYPE_WEB :
303
318
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 ))
305
321
306
322
self .client_id = client_info ['client_id' ]
307
323
self .client_secret = client_info ['client_secret' ]
@@ -311,7 +327,7 @@ def _make_flow(self, return_url=None, **kwargs):
311
327
# Generate a CSRF token to prevent malicious requests.
312
328
csrf_token = hashlib .sha256 (os .urandom (1024 )).hexdigest ()
313
329
314
- session ['google_oauth2_csrf_token' ] = csrf_token
330
+ session [_CSRF_KEY ] = csrf_token
315
331
316
332
state = json .dumps ({
317
333
'csrf_token' : csrf_token ,
@@ -321,17 +337,22 @@ def _make_flow(self, return_url=None, **kwargs):
321
337
kw = self .flow_kwargs .copy ()
322
338
kw .update (kwargs )
323
339
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 ) )
326
342
327
- return OAuth2WebServerFlow (
343
+ flow = OAuth2WebServerFlow (
328
344
client_id = self .client_id ,
329
345
client_secret = self .client_secret ,
330
346
scope = scopes ,
331
347
state = state ,
332
348
redirect_uri = url_for ('oauth2.callback' , _external = True ),
333
349
** kw )
334
350
351
+ flow_key = _FLOW_KEY .format (csrf_token )
352
+ session [flow_key ] = pickle .dumps (flow )
353
+
354
+ return flow
355
+
335
356
def _create_blueprint (self ):
336
357
bp = Blueprint ('oauth2' , __name__ )
337
358
bp .add_url_rule ('/oauth2authorize' , 'authorize' , self .authorize_view )
@@ -368,11 +389,12 @@ def callback_view(self):
368
389
if 'error' in request .args :
369
390
reason = request .args .get (
370
391
'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 )
372
394
373
395
try :
374
396
encoded_state = request .args ['state' ]
375
- server_csrf = session ['google_oauth2_csrf_token' ]
397
+ server_csrf = session [_CSRF_KEY ]
376
398
code = request .args ['code' ]
377
399
except KeyError :
378
400
return 'Invalid request' , httplib .BAD_REQUEST
@@ -387,14 +409,17 @@ def callback_view(self):
387
409
if client_csrf != server_csrf :
388
410
return 'Invalid request state' , httplib .BAD_REQUEST
389
411
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
391
416
392
417
# Exchange the auth code for credentials.
393
418
try :
394
419
credentials = flow .step2_exchange (code )
395
420
except FlowExchangeError as exchange_error :
396
421
current_app .logger .exception (exchange_error )
397
- content = 'An error occurred: %s' % (exchange_error , )
422
+ content = 'An error occurred: {0}' . format (exchange_error )
398
423
return content , httplib .BAD_REQUEST
399
424
400
425
# Save the credentials to the storage.
@@ -410,7 +435,7 @@ def credentials(self):
410
435
"""The credentials for the current user or None if unavailable."""
411
436
ctx = _app_ctx_stack .top
412
437
413
- if not hasattr (ctx , 'google_oauth2_credentials' ):
438
+ if not hasattr (ctx , _CREDENTIALS_KEY ):
414
439
ctx .google_oauth2_credentials = self .storage .get ()
415
440
416
441
return ctx .google_oauth2_credentials
@@ -433,7 +458,7 @@ def email(self):
433
458
return self .credentials .id_token ['email' ]
434
459
except KeyError :
435
460
current_app .logger .error (
436
- 'Invalid id_token %s' , self .credentials .id_token )
461
+ 'Invalid id_token {0}' . format ( self .credentials .id_token ) )
437
462
438
463
@property
439
464
def user_id (self ):
@@ -449,7 +474,7 @@ def user_id(self):
449
474
return self .credentials .id_token ['sub' ]
450
475
except KeyError :
451
476
current_app .logger .error (
452
- 'Invalid id_token %s' , self .credentials .id_token )
477
+ 'Invalid id_token {0}' . format ( self .credentials .id_token ) )
453
478
454
479
def authorize_url (self , return_url , ** kwargs ):
455
480
"""Creates a URL that can be used to start the authorization flow.
@@ -474,28 +499,30 @@ def required(self, decorated_function=None, scopes=None,
474
499
def curry_wrapper (wrapped_function ):
475
500
@wraps (wrapped_function )
476
501
def required_wrapper (* args , ** kwargs ):
477
-
478
502
return_url = decorator_kwargs .pop ('return_url' , request .url )
479
503
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 :
482
519
auth_url = self .authorize_url (
483
520
return_url ,
484
- scopes = scopes ,
521
+ scopes = requested_scopes ,
485
522
** decorator_kwargs )
486
- return redirect (auth_url )
487
523
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 )
495
524
return redirect (auth_url )
496
525
497
- return wrapped_function (* args , ** kwargs )
498
-
499
526
return required_wrapper
500
527
501
528
if decorated_function :
@@ -531,7 +558,7 @@ class FlaskSessionStorage(Storage):
531
558
"""
532
559
533
560
def locked_get (self ):
534
- serialized = session .get ('google_oauth2_credentials' )
561
+ serialized = session .get (_CREDENTIALS_KEY )
535
562
536
563
if serialized is None :
537
564
return None
@@ -542,8 +569,8 @@ def locked_get(self):
542
569
return credentials
543
570
544
571
def locked_put (self , credentials ):
545
- session ['google_oauth2_credentials' ] = credentials .to_json ()
572
+ session [_CREDENTIALS_KEY ] = credentials .to_json ()
546
573
547
574
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