Package oauth2client :: Module appengine
[hide private]
[frames] | no frames]

Source Code for Module oauth2client.appengine

  1  # Copyright 2014 Google Inc. All rights reserved. 
  2  # 
  3  # Licensed under the Apache License, Version 2.0 (the "License"); 
  4  # you may not use this file except in compliance with the License. 
  5  # You may obtain a copy of the License at 
  6  # 
  7  #      http://www.apache.org/licenses/LICENSE-2.0 
  8  # 
  9  # Unless required by applicable law or agreed to in writing, software 
 10  # distributed under the License is distributed on an "AS IS" BASIS, 
 11  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 12  # See the License for the specific language governing permissions and 
 13  # limitations under the License. 
 14   
 15  """Utilities for Google App Engine 
 16   
 17  Utilities for making it easier to use OAuth 2.0 on Google App Engine. 
 18  """ 
 19   
 20  __author__ = 'jcgregorio@google.com (Joe Gregorio)' 
 21   
 22  import cgi 
 23  import json 
 24  import logging 
 25  import os 
 26  import pickle 
 27  import threading 
 28   
 29  import httplib2 
 30   
 31  from google.appengine.api import app_identity 
 32  from google.appengine.api import memcache 
 33  from google.appengine.api import users 
 34  from google.appengine.ext import db 
 35  from google.appengine.ext import webapp 
 36  from google.appengine.ext.webapp.util import login_required 
 37  from google.appengine.ext.webapp.util import run_wsgi_app 
 38  from oauth2client import GOOGLE_AUTH_URI 
 39  from oauth2client import GOOGLE_REVOKE_URI 
 40  from oauth2client import GOOGLE_TOKEN_URI 
 41  from oauth2client import clientsecrets 
 42  from oauth2client import util 
 43  from oauth2client import xsrfutil 
 44  from oauth2client.client import AccessTokenRefreshError 
 45  from oauth2client.client import AssertionCredentials 
 46  from oauth2client.client import Credentials 
 47  from oauth2client.client import Flow 
 48  from oauth2client.client import OAuth2WebServerFlow 
 49  from oauth2client.client import Storage 
 50   
 51  # TODO(dhermes): Resolve import issue. 
 52  # This is a temporary fix for a Google internal issue. 
 53  try: 
 54    from google.appengine.ext import ndb 
 55  except ImportError: 
 56    ndb = None 
 57   
 58   
 59  logger = logging.getLogger(__name__) 
 60   
 61  OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' 
 62   
 63  XSRF_MEMCACHE_ID = 'xsrf_secret_key' 
64 65 66 -def _safe_html(s):
67 """Escape text to make it safe to display. 68 69 Args: 70 s: string, The text to escape. 71 72 Returns: 73 The escaped text as a string. 74 """ 75 return cgi.escape(s, quote=1).replace("'", ''')
76
77 78 -class InvalidClientSecretsError(Exception):
79 """The client_secrets.json file is malformed or missing required fields."""
80
81 82 -class InvalidXsrfTokenError(Exception):
83 """The XSRF token is invalid or expired."""
84
85 86 -class SiteXsrfSecretKey(db.Model):
87 """Storage for the sites XSRF secret key. 88 89 There will only be one instance stored of this model, the one used for the 90 site. 91 """ 92 secret = db.StringProperty()
93 94 if ndb is not None:
95 - class SiteXsrfSecretKeyNDB(ndb.Model):
96 """NDB Model for storage for the sites XSRF secret key. 97 98 Since this model uses the same kind as SiteXsrfSecretKey, it can be used 99 interchangeably. This simply provides an NDB model for interacting with the 100 same data the DB model interacts with. 101 102 There should only be one instance stored of this model, the one used for the 103 site. 104 """ 105 secret = ndb.StringProperty() 106 107 @classmethod
108 - def _get_kind(cls):
109 """Return the kind name for this class.""" 110 return 'SiteXsrfSecretKey'
111
112 113 -def _generate_new_xsrf_secret_key():
114 """Returns a random XSRF secret key. 115 """ 116 return os.urandom(16).encode("hex")
117
118 119 -def xsrf_secret_key():
120 """Return the secret key for use for XSRF protection. 121 122 If the Site entity does not have a secret key, this method will also create 123 one and persist it. 124 125 Returns: 126 The secret key. 127 """ 128 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE) 129 if not secret: 130 # Load the one and only instance of SiteXsrfSecretKey. 131 model = SiteXsrfSecretKey.get_or_insert(key_name='site') 132 if not model.secret: 133 model.secret = _generate_new_xsrf_secret_key() 134 model.put() 135 secret = model.secret 136 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE) 137 138 return str(secret)
139
140 141 -class AppAssertionCredentials(AssertionCredentials):
142 """Credentials object for App Engine Assertion Grants 143 144 This object will allow an App Engine application to identify itself to Google 145 and other OAuth 2.0 servers that can verify assertions. It can be used for the 146 purpose of accessing data stored under an account assigned to the App Engine 147 application itself. 148 149 This credential does not require a flow to instantiate because it represents 150 a two legged flow, and therefore has all of the required information to 151 generate and refresh its own access tokens. 152 """ 153 154 @util.positional(2)
155 - def __init__(self, scope, **kwargs):
156 """Constructor for AppAssertionCredentials 157 158 Args: 159 scope: string or iterable of strings, scope(s) of the credentials being 160 requested. 161 **kwargs: optional keyword args, including: 162 service_account_id: service account id of the application. If None or 163 unspecified, the default service account for the app is used. 164 """ 165 self.scope = util.scopes_to_string(scope) 166 self._kwargs = kwargs 167 self.service_account_id = kwargs.get('service_account_id', None) 168 169 # Assertion type is no longer used, but still in the parent class signature. 170 super(AppAssertionCredentials, self).__init__(None)
171 172 @classmethod
173 - def from_json(cls, json_data):
174 data = json.loads(json_data) 175 return AppAssertionCredentials(data['scope'])
176
177 - def _refresh(self, http_request):
178 """Refreshes the access_token. 179 180 Since the underlying App Engine app_identity implementation does its own 181 caching we can skip all the storage hoops and just to a refresh using the 182 API. 183 184 Args: 185 http_request: callable, a callable that matches the method signature of 186 httplib2.Http.request, used to make the refresh request. 187 188 Raises: 189 AccessTokenRefreshError: When the refresh fails. 190 """ 191 try: 192 scopes = self.scope.split() 193 (token, _) = app_identity.get_access_token( 194 scopes, service_account_id=self.service_account_id) 195 except app_identity.Error as e: 196 raise AccessTokenRefreshError(str(e)) 197 self.access_token = token
198 199 @property
200 - def serialization_data(self):
201 raise NotImplementedError('Cannot serialize credentials for AppEngine.')
202
203 - def create_scoped_required(self):
204 return not self.scope
205
206 - def create_scoped(self, scopes):
207 return AppAssertionCredentials(scopes, **self._kwargs)
208
209 210 -class FlowProperty(db.Property):
211 """App Engine datastore Property for Flow. 212 213 Utility property that allows easy storage and retrieval of an 214 oauth2client.Flow""" 215 216 # Tell what the user type is. 217 data_type = Flow 218 219 # For writing to datastore.
220 - def get_value_for_datastore(self, model_instance):
221 flow = super(FlowProperty, 222 self).get_value_for_datastore(model_instance) 223 return db.Blob(pickle.dumps(flow))
224 225 # For reading from datastore.
226 - def make_value_from_datastore(self, value):
227 if value is None: 228 return None 229 return pickle.loads(value)
230
231 - def validate(self, value):
232 if value is not None and not isinstance(value, Flow): 233 raise db.BadValueError('Property %s must be convertible ' 234 'to a FlowThreeLegged instance (%s)' % 235 (self.name, value)) 236 return super(FlowProperty, self).validate(value)
237
238 - def empty(self, value):
239 return not value
240 241 242 if ndb is not None:
243 - class FlowNDBProperty(ndb.PickleProperty):
244 """App Engine NDB datastore Property for Flow. 245 246 Serves the same purpose as the DB FlowProperty, but for NDB models. Since 247 PickleProperty inherits from BlobProperty, the underlying representation of 248 the data in the datastore will be the same as in the DB case. 249 250 Utility property that allows easy storage and retrieval of an 251 oauth2client.Flow 252 """ 253
254 - def _validate(self, value):
255 """Validates a value as a proper Flow object. 256 257 Args: 258 value: A value to be set on the property. 259 260 Raises: 261 TypeError if the value is not an instance of Flow. 262 """ 263 logger.info('validate: Got type %s', type(value)) 264 if value is not None and not isinstance(value, Flow): 265 raise TypeError('Property %s must be convertible to a flow ' 266 'instance; received: %s.' % (self._name, value))
267
268 269 -class CredentialsProperty(db.Property):
270 """App Engine datastore Property for Credentials. 271 272 Utility property that allows easy storage and retrieval of 273 oath2client.Credentials 274 """ 275 276 # Tell what the user type is. 277 data_type = Credentials 278 279 # For writing to datastore.
280 - def get_value_for_datastore(self, model_instance):
281 logger.info("get: Got type " + str(type(model_instance))) 282 cred = super(CredentialsProperty, 283 self).get_value_for_datastore(model_instance) 284 if cred is None: 285 cred = '' 286 else: 287 cred = cred.to_json() 288 return db.Blob(cred)
289 290 # For reading from datastore.
291 - def make_value_from_datastore(self, value):
292 logger.info("make: Got type " + str(type(value))) 293 if value is None: 294 return None 295 if len(value) == 0: 296 return None 297 try: 298 credentials = Credentials.new_from_json(value) 299 except ValueError: 300 credentials = None 301 return credentials
302
303 - def validate(self, value):
304 value = super(CredentialsProperty, self).validate(value) 305 logger.info("validate: Got type " + str(type(value))) 306 if value is not None and not isinstance(value, Credentials): 307 raise db.BadValueError('Property %s must be convertible ' 308 'to a Credentials instance (%s)' % 309 (self.name, value)) 310 #if value is not None and not isinstance(value, Credentials): 311 # return None 312 return value
313 314 315 if ndb is not None:
316 # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials 317 # and subclass mechanics to use new_from_dict, to_dict, 318 # from_dict, etc. 319 - class CredentialsNDBProperty(ndb.BlobProperty):
320 """App Engine NDB datastore Property for Credentials. 321 322 Serves the same purpose as the DB CredentialsProperty, but for NDB models. 323 Since CredentialsProperty stores data as a blob and this inherits from 324 BlobProperty, the data in the datastore will be the same as in the DB case. 325 326 Utility property that allows easy storage and retrieval of Credentials and 327 subclasses. 328 """
329 - def _validate(self, value):
330 """Validates a value as a proper credentials object. 331 332 Args: 333 value: A value to be set on the property. 334 335 Raises: 336 TypeError if the value is not an instance of Credentials. 337 """ 338 logger.info('validate: Got type %s', type(value)) 339 if value is not None and not isinstance(value, Credentials): 340 raise TypeError('Property %s must be convertible to a credentials ' 341 'instance; received: %s.' % (self._name, value))
342
343 - def _to_base_type(self, value):
344 """Converts our validated value to a JSON serialized string. 345 346 Args: 347 value: A value to be set in the datastore. 348 349 Returns: 350 A JSON serialized version of the credential, else '' if value is None. 351 """ 352 if value is None: 353 return '' 354 else: 355 return value.to_json()
356
357 - def _from_base_type(self, value):
358 """Converts our stored JSON string back to the desired type. 359 360 Args: 361 value: A value from the datastore to be converted to the desired type. 362 363 Returns: 364 A deserialized Credentials (or subclass) object, else None if the 365 value can't be parsed. 366 """ 367 if not value: 368 return None 369 try: 370 # Uses the from_json method of the implied class of value 371 credentials = Credentials.new_from_json(value) 372 except ValueError: 373 credentials = None 374 return credentials
375
376 377 -class StorageByKeyName(Storage):
378 """Store and retrieve a credential to and from the App Engine datastore. 379 380 This Storage helper presumes the Credentials have been stored as a 381 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and 382 that entities are stored by key_name. 383 """ 384 385 @util.positional(4)
386 - def __init__(self, model, key_name, property_name, cache=None, user=None):
387 """Constructor for Storage. 388 389 Args: 390 model: db.Model or ndb.Model, model class 391 key_name: string, key name for the entity that has the credentials 392 property_name: string, name of the property that is a CredentialsProperty 393 or CredentialsNDBProperty. 394 cache: memcache, a write-through cache to put in front of the datastore. 395 If the model you are using is an NDB model, using a cache will be 396 redundant since the model uses an instance cache and memcache for you. 397 user: users.User object, optional. Can be used to grab user ID as a 398 key_name if no key name is specified. 399 """ 400 if key_name is None: 401 if user is None: 402 raise ValueError('StorageByKeyName called with no key name or user.') 403 key_name = user.user_id() 404 405 self._model = model 406 self._key_name = key_name 407 self._property_name = property_name 408 self._cache = cache
409
410 - def _is_ndb(self):
411 """Determine whether the model of the instance is an NDB model. 412 413 Returns: 414 Boolean indicating whether or not the model is an NDB or DB model. 415 """ 416 # issubclass will fail if one of the arguments is not a class, only need 417 # worry about new-style classes since ndb and db models are new-style 418 if isinstance(self._model, type): 419 if ndb is not None and issubclass(self._model, ndb.Model): 420 return True 421 elif issubclass(self._model, db.Model): 422 return False 423 424 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
425
426 - def _get_entity(self):
427 """Retrieve entity from datastore. 428 429 Uses a different model method for db or ndb models. 430 431 Returns: 432 Instance of the model corresponding to the current storage object 433 and stored using the key name of the storage object. 434 """ 435 if self._is_ndb(): 436 return self._model.get_by_id(self._key_name) 437 else: 438 return self._model.get_by_key_name(self._key_name)
439
440 - def _delete_entity(self):
441 """Delete entity from datastore. 442 443 Attempts to delete using the key_name stored on the object, whether or not 444 the given key is in the datastore. 445 """ 446 if self._is_ndb(): 447 ndb.Key(self._model, self._key_name).delete() 448 else: 449 entity_key = db.Key.from_path(self._model.kind(), self._key_name) 450 db.delete(entity_key)
451 452 @db.non_transactional(allow_existing=True)
453 - def locked_get(self):
454 """Retrieve Credential from datastore. 455 456 Returns: 457 oauth2client.Credentials 458 """ 459 credentials = None 460 if self._cache: 461 json = self._cache.get(self._key_name) 462 if json: 463 credentials = Credentials.new_from_json(json) 464 if credentials is None: 465 entity = self._get_entity() 466 if entity is not None: 467 credentials = getattr(entity, self._property_name) 468 if self._cache: 469 self._cache.set(self._key_name, credentials.to_json()) 470 471 if credentials and hasattr(credentials, 'set_store'): 472 credentials.set_store(self) 473 return credentials
474 475 @db.non_transactional(allow_existing=True)
476 - def locked_put(self, credentials):
477 """Write a Credentials to the datastore. 478 479 Args: 480 credentials: Credentials, the credentials to store. 481 """ 482 entity = self._model.get_or_insert(self._key_name) 483 setattr(entity, self._property_name, credentials) 484 entity.put() 485 if self._cache: 486 self._cache.set(self._key_name, credentials.to_json())
487 488 @db.non_transactional(allow_existing=True)
489 - def locked_delete(self):
490 """Delete Credential from datastore.""" 491 492 if self._cache: 493 self._cache.delete(self._key_name) 494 495 self._delete_entity()
496
497 498 -class CredentialsModel(db.Model):
499 """Storage for OAuth 2.0 Credentials 500 501 Storage of the model is keyed by the user.user_id(). 502 """ 503 credentials = CredentialsProperty()
504 505 506 if ndb is not None:
507 - class CredentialsNDBModel(ndb.Model):
508 """NDB Model for storage of OAuth 2.0 Credentials 509 510 Since this model uses the same kind as CredentialsModel and has a property 511 which can serialize and deserialize Credentials correctly, it can be used 512 interchangeably with a CredentialsModel to access, insert and delete the 513 same entities. This simply provides an NDB model for interacting with the 514 same data the DB model interacts with. 515 516 Storage of the model is keyed by the user.user_id(). 517 """ 518 credentials = CredentialsNDBProperty() 519 520 @classmethod
521 - def _get_kind(cls):
522 """Return the kind name for this class.""" 523 return 'CredentialsModel'
524
525 526 -def _build_state_value(request_handler, user):
527 """Composes the value for the 'state' parameter. 528 529 Packs the current request URI and an XSRF token into an opaque string that 530 can be passed to the authentication server via the 'state' parameter. 531 532 Args: 533 request_handler: webapp.RequestHandler, The request. 534 user: google.appengine.api.users.User, The current user. 535 536 Returns: 537 The state value as a string. 538 """ 539 uri = request_handler.request.url 540 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(), 541 action_id=str(uri)) 542 return uri + ':' + token
543
544 545 -def _parse_state_value(state, user):
546 """Parse the value of the 'state' parameter. 547 548 Parses the value and validates the XSRF token in the state parameter. 549 550 Args: 551 state: string, The value of the state parameter. 552 user: google.appengine.api.users.User, The current user. 553 554 Raises: 555 InvalidXsrfTokenError: if the XSRF token is invalid. 556 557 Returns: 558 The redirect URI. 559 """ 560 uri, token = state.rsplit(':', 1) 561 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), 562 action_id=uri): 563 raise InvalidXsrfTokenError() 564 565 return uri
566
567 568 -class OAuth2Decorator(object):
569 """Utility for making OAuth 2.0 easier. 570 571 Instantiate and then use with oauth_required or oauth_aware 572 as decorators on webapp.RequestHandler methods. 573 574 Example: 575 576 decorator = OAuth2Decorator( 577 client_id='837...ent.com', 578 client_secret='Qh...wwI', 579 scope='https://www.googleapis.com/auth/plus') 580 581 582 class MainHandler(webapp.RequestHandler): 583 584 @decorator.oauth_required 585 def get(self): 586 http = decorator.http() 587 # http is authorized with the user's Credentials and can be used 588 # in API calls 589 590 """ 591
592 - def set_credentials(self, credentials):
593 self._tls.credentials = credentials
594
595 - def get_credentials(self):
596 """A thread local Credentials object. 597 598 Returns: 599 A client.Credentials object, or None if credentials hasn't been set in 600 this thread yet, which may happen when calling has_credentials inside 601 oauth_aware. 602 """ 603 return getattr(self._tls, 'credentials', None)
604 605 credentials = property(get_credentials, set_credentials) 606
607 - def set_flow(self, flow):
608 self._tls.flow = flow
609
610 - def get_flow(self):
611 """A thread local Flow object. 612 613 Returns: 614 A credentials.Flow object, or None if the flow hasn't been set in this 615 thread yet, which happens in _create_flow() since Flows are created 616 lazily. 617 """ 618 return getattr(self._tls, 'flow', None)
619 620 flow = property(get_flow, set_flow) 621 622 623 @util.positional(4)
624 - def __init__(self, client_id, client_secret, scope, 625 auth_uri=GOOGLE_AUTH_URI, 626 token_uri=GOOGLE_TOKEN_URI, 627 revoke_uri=GOOGLE_REVOKE_URI, 628 user_agent=None, 629 message=None, 630 callback_path='/oauth2callback', 631 token_response_param=None, 632 _storage_class=StorageByKeyName, 633 _credentials_class=CredentialsModel, 634 _credentials_property_name='credentials', 635 **kwargs):
636 637 """Constructor for OAuth2Decorator 638 639 Args: 640 client_id: string, client identifier. 641 client_secret: string client secret. 642 scope: string or iterable of strings, scope(s) of the credentials being 643 requested. 644 auth_uri: string, URI for authorization endpoint. For convenience 645 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 646 token_uri: string, URI for token endpoint. For convenience 647 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 648 revoke_uri: string, URI for revoke endpoint. For convenience 649 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 650 user_agent: string, User agent of your application, default to None. 651 message: Message to display if there are problems with the OAuth 2.0 652 configuration. The message may contain HTML and will be presented on the 653 web interface for any method that uses the decorator. 654 callback_path: string, The absolute path to use as the callback URI. Note 655 that this must match up with the URI given when registering the 656 application in the APIs Console. 657 token_response_param: string. If provided, the full JSON response 658 to the access token request will be encoded and included in this query 659 parameter in the callback URI. This is useful with providers (e.g. 660 wordpress.com) that include extra fields that the client may want. 661 _storage_class: "Protected" keyword argument not typically provided to 662 this constructor. A storage class to aid in storing a Credentials object 663 for a user in the datastore. Defaults to StorageByKeyName. 664 _credentials_class: "Protected" keyword argument not typically provided to 665 this constructor. A db or ndb Model class to hold credentials. Defaults 666 to CredentialsModel. 667 _credentials_property_name: "Protected" keyword argument not typically 668 provided to this constructor. A string indicating the name of the field 669 on the _credentials_class where a Credentials object will be stored. 670 Defaults to 'credentials'. 671 **kwargs: dict, Keyword arguments are passed along as kwargs to 672 the OAuth2WebServerFlow constructor. 673 674 """ 675 self._tls = threading.local() 676 self.flow = None 677 self.credentials = None 678 self._client_id = client_id 679 self._client_secret = client_secret 680 self._scope = util.scopes_to_string(scope) 681 self._auth_uri = auth_uri 682 self._token_uri = token_uri 683 self._revoke_uri = revoke_uri 684 self._user_agent = user_agent 685 self._kwargs = kwargs 686 self._message = message 687 self._in_error = False 688 self._callback_path = callback_path 689 self._token_response_param = token_response_param 690 self._storage_class = _storage_class 691 self._credentials_class = _credentials_class 692 self._credentials_property_name = _credentials_property_name
693
694 - def _display_error_message(self, request_handler):
695 request_handler.response.out.write('<html><body>') 696 request_handler.response.out.write(_safe_html(self._message)) 697 request_handler.response.out.write('</body></html>')
698
699 - def oauth_required(self, method):
700 """Decorator that starts the OAuth 2.0 dance. 701 702 Starts the OAuth dance for the logged in user if they haven't already 703 granted access for this application. 704 705 Args: 706 method: callable, to be decorated method of a webapp.RequestHandler 707 instance. 708 """ 709 710 def check_oauth(request_handler, *args, **kwargs): 711 if self._in_error: 712 self._display_error_message(request_handler) 713 return 714 715 user = users.get_current_user() 716 # Don't use @login_decorator as this could be used in a POST request. 717 if not user: 718 request_handler.redirect(users.create_login_url( 719 request_handler.request.uri)) 720 return 721 722 self._create_flow(request_handler) 723 724 # Store the request URI in 'state' so we can use it later 725 self.flow.params['state'] = _build_state_value(request_handler, user) 726 self.credentials = self._storage_class( 727 self._credentials_class, None, 728 self._credentials_property_name, user=user).get() 729 730 if not self.has_credentials(): 731 return request_handler.redirect(self.authorize_url()) 732 try: 733 resp = method(request_handler, *args, **kwargs) 734 except AccessTokenRefreshError: 735 return request_handler.redirect(self.authorize_url()) 736 finally: 737 self.credentials = None 738 return resp
739 740 return check_oauth
741
742 - def _create_flow(self, request_handler):
743 """Create the Flow object. 744 745 The Flow is calculated lazily since we don't know where this app is 746 running until it receives a request, at which point redirect_uri can be 747 calculated and then the Flow object can be constructed. 748 749 Args: 750 request_handler: webapp.RequestHandler, the request handler. 751 """ 752 if self.flow is None: 753 redirect_uri = request_handler.request.relative_url( 754 self._callback_path) # Usually /oauth2callback 755 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret, 756 self._scope, redirect_uri=redirect_uri, 757 user_agent=self._user_agent, 758 auth_uri=self._auth_uri, 759 token_uri=self._token_uri, 760 revoke_uri=self._revoke_uri, 761 **self._kwargs)
762
763 - def oauth_aware(self, method):
764 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. 765 766 Does all the setup for the OAuth dance, but doesn't initiate it. 767 This decorator is useful if you want to create a page that knows 768 whether or not the user has granted access to this application. 769 From within a method decorated with @oauth_aware the has_credentials() 770 and authorize_url() methods can be called. 771 772 Args: 773 method: callable, to be decorated method of a webapp.RequestHandler 774 instance. 775 """ 776 777 def setup_oauth(request_handler, *args, **kwargs): 778 if self._in_error: 779 self._display_error_message(request_handler) 780 return 781 782 user = users.get_current_user() 783 # Don't use @login_decorator as this could be used in a POST request. 784 if not user: 785 request_handler.redirect(users.create_login_url( 786 request_handler.request.uri)) 787 return 788 789 self._create_flow(request_handler) 790 791 self.flow.params['state'] = _build_state_value(request_handler, user) 792 self.credentials = self._storage_class( 793 self._credentials_class, None, 794 self._credentials_property_name, user=user).get() 795 try: 796 resp = method(request_handler, *args, **kwargs) 797 finally: 798 self.credentials = None 799 return resp
800 return setup_oauth 801 802
803 - def has_credentials(self):
804 """True if for the logged in user there are valid access Credentials. 805 806 Must only be called from with a webapp.RequestHandler subclassed method 807 that had been decorated with either @oauth_required or @oauth_aware. 808 """ 809 return self.credentials is not None and not self.credentials.invalid
810
811 - def authorize_url(self):
812 """Returns the URL to start the OAuth dance. 813 814 Must only be called from with a webapp.RequestHandler subclassed method 815 that had been decorated with either @oauth_required or @oauth_aware. 816 """ 817 url = self.flow.step1_get_authorize_url() 818 return str(url)
819
820 - def http(self, *args, **kwargs):
821 """Returns an authorized http instance. 822 823 Must only be called from within an @oauth_required decorated method, or 824 from within an @oauth_aware decorated method where has_credentials() 825 returns True. 826 827 Args: 828 *args: Positional arguments passed to httplib2.Http constructor. 829 **kwargs: Positional arguments passed to httplib2.Http constructor. 830 """ 831 return self.credentials.authorize(httplib2.Http(*args, **kwargs))
832 833 @property
834 - def callback_path(self):
835 """The absolute path where the callback will occur. 836 837 Note this is the absolute path, not the absolute URI, that will be 838 calculated by the decorator at runtime. See callback_handler() for how this 839 should be used. 840 841 Returns: 842 The callback path as a string. 843 """ 844 return self._callback_path
845 846
847 - def callback_handler(self):
848 """RequestHandler for the OAuth 2.0 redirect callback. 849 850 Usage: 851 app = webapp.WSGIApplication([ 852 ('/index', MyIndexHandler), 853 ..., 854 (decorator.callback_path, decorator.callback_handler()) 855 ]) 856 857 Returns: 858 A webapp.RequestHandler that handles the redirect back from the 859 server during the OAuth 2.0 dance. 860 """ 861 decorator = self 862 863 class OAuth2Handler(webapp.RequestHandler): 864 """Handler for the redirect_uri of the OAuth 2.0 dance.""" 865 866 @login_required 867 def get(self): 868 error = self.request.get('error') 869 if error: 870 errormsg = self.request.get('error_description', error) 871 self.response.out.write( 872 'The authorization request failed: %s' % _safe_html(errormsg)) 873 else: 874 user = users.get_current_user() 875 decorator._create_flow(self) 876 credentials = decorator.flow.step2_exchange(self.request.params) 877 decorator._storage_class( 878 decorator._credentials_class, None, 879 decorator._credentials_property_name, user=user).put(credentials) 880 redirect_uri = _parse_state_value(str(self.request.get('state')), 881 user) 882 883 if decorator._token_response_param and credentials.token_response: 884 resp_json = json.dumps(credentials.token_response) 885 redirect_uri = util._add_query_parameter( 886 redirect_uri, decorator._token_response_param, resp_json) 887 888 self.redirect(redirect_uri)
889 890 return OAuth2Handler 891
892 - def callback_application(self):
893 """WSGI application for handling the OAuth 2.0 redirect callback. 894 895 If you need finer grained control use `callback_handler` which returns just 896 the webapp.RequestHandler. 897 898 Returns: 899 A webapp.WSGIApplication that handles the redirect back from the 900 server during the OAuth 2.0 dance. 901 """ 902 return webapp.WSGIApplication([ 903 (self.callback_path, self.callback_handler()) 904 ])
905
906 907 -class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
908 """An OAuth2Decorator that builds from a clientsecrets file. 909 910 Uses a clientsecrets file as the source for all the information when 911 constructing an OAuth2Decorator. 912 913 Example: 914 915 decorator = OAuth2DecoratorFromClientSecrets( 916 os.path.join(os.path.dirname(__file__), 'client_secrets.json') 917 scope='https://www.googleapis.com/auth/plus') 918 919 920 class MainHandler(webapp.RequestHandler): 921 922 @decorator.oauth_required 923 def get(self): 924 http = decorator.http() 925 # http is authorized with the user's Credentials and can be used 926 # in API calls 927 """ 928 929 @util.positional(3)
930 - def __init__(self, filename, scope, message=None, cache=None, **kwargs):
931 """Constructor 932 933 Args: 934 filename: string, File name of client secrets. 935 scope: string or iterable of strings, scope(s) of the credentials being 936 requested. 937 message: string, A friendly string to display to the user if the 938 clientsecrets file is missing or invalid. The message may contain HTML 939 and will be presented on the web interface for any method that uses the 940 decorator. 941 cache: An optional cache service client that implements get() and set() 942 methods. See clientsecrets.loadfile() for details. 943 **kwargs: dict, Keyword arguments are passed along as kwargs to 944 the OAuth2WebServerFlow constructor. 945 """ 946 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 947 if client_type not in [ 948 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 949 raise InvalidClientSecretsError( 950 "OAuth2Decorator doesn't support this OAuth 2.0 flow.") 951 constructor_kwargs = dict(kwargs) 952 constructor_kwargs.update({ 953 'auth_uri': client_info['auth_uri'], 954 'token_uri': client_info['token_uri'], 955 'message': message, 956 }) 957 revoke_uri = client_info.get('revoke_uri') 958 if revoke_uri is not None: 959 constructor_kwargs['revoke_uri'] = revoke_uri 960 super(OAuth2DecoratorFromClientSecrets, self).__init__( 961 client_info['client_id'], client_info['client_secret'], 962 scope, **constructor_kwargs) 963 if message is not None: 964 self._message = message 965 else: 966 self._message = 'Please configure your application for OAuth 2.0.'
967
968 969 @util.positional(2) 970 -def oauth2decorator_from_clientsecrets(filename, scope, 971 message=None, cache=None):
972 """Creates an OAuth2Decorator populated from a clientsecrets file. 973 974 Args: 975 filename: string, File name of client secrets. 976 scope: string or list of strings, scope(s) of the credentials being 977 requested. 978 message: string, A friendly string to display to the user if the 979 clientsecrets file is missing or invalid. The message may contain HTML and 980 will be presented on the web interface for any method that uses the 981 decorator. 982 cache: An optional cache service client that implements get() and set() 983 methods. See clientsecrets.loadfile() for details. 984 985 Returns: An OAuth2Decorator 986 987 """ 988 return OAuth2DecoratorFromClientSecrets(filename, scope, 989 message=message, cache=cache)
990