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