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

Source Code for Module oauth2client.appengine

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