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