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 17Utilities for making it easier to use OAuth 2.0 on Google App Engine. 18""" 19 20__author__ = 'jcgregorio@google.com (Joe Gregorio)' 21 22import cgi 23import json 24import logging 25import os 26import pickle 27import threading 28 29import httplib2 30 31from google.appengine.api import app_identity 32from google.appengine.api import memcache 33from google.appengine.api import users 34from google.appengine.ext import db 35from google.appengine.ext import webapp 36from google.appengine.ext.webapp.util import login_required 37from google.appengine.ext.webapp.util import run_wsgi_app 38from oauth2client import GOOGLE_AUTH_URI 39from oauth2client import GOOGLE_REVOKE_URI 40from oauth2client import GOOGLE_TOKEN_URI 41from oauth2client import clientsecrets 42from oauth2client import util 43from oauth2client import xsrfutil 44from oauth2client.client import AccessTokenRefreshError 45from oauth2client.client import AssertionCredentials 46from oauth2client.client import Credentials 47from oauth2client.client import Flow 48from oauth2client.client import OAuth2WebServerFlow 49from oauth2client.client import Storage 50 51# TODO(dhermes): Resolve import issue. 52# This is a temporary fix for a Google internal issue. 53try: 54 from google.appengine.ext import ndb 55except ImportError: 56 ndb = None 57 58 59logger = logging.getLogger(__name__) 60 61OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' 62 63XSRF_MEMCACHE_ID = 'xsrf_secret_key' 64 65 66def _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 78class InvalidClientSecretsError(Exception): 79 """The client_secrets.json file is malformed or missing required fields.""" 80 81 82class InvalidXsrfTokenError(Exception): 83 """The XSRF token is invalid or expired.""" 84 85 86class 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 94if 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 113def _generate_new_xsrf_secret_key(): 114 """Returns a random XSRF secret key. 115 """ 116 return os.urandom(16).encode("hex") 117 118 119def 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 141class 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 210class 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 242if 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 269class 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 315if 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 377class 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 498class 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 506if 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 526def _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 545def _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 568class 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 :: 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 class MainHandler(webapp.RequestHandler): 582 @decorator.oauth_required 583 def get(self): 584 http = decorator.http() 585 # http is authorized with the user's Credentials and can be used 586 # in API calls 587 588 """ 589 590 def set_credentials(self, credentials): 591 self._tls.credentials = credentials 592 593 def get_credentials(self): 594 """A thread local Credentials object. 595 596 Returns: 597 A client.Credentials object, or None if credentials hasn't been set in 598 this thread yet, which may happen when calling has_credentials inside 599 oauth_aware. 600 """ 601 return getattr(self._tls, 'credentials', None) 602 603 credentials = property(get_credentials, set_credentials) 604 605 def set_flow(self, flow): 606 self._tls.flow = flow 607 608 def get_flow(self): 609 """A thread local Flow object. 610 611 Returns: 612 A credentials.Flow object, or None if the flow hasn't been set in this 613 thread yet, which happens in _create_flow() since Flows are created 614 lazily. 615 """ 616 return getattr(self._tls, 'flow', None) 617 618 flow = property(get_flow, set_flow) 619 620 621 @util.positional(4) 622 def __init__(self, client_id, client_secret, scope, 623 auth_uri=GOOGLE_AUTH_URI, 624 token_uri=GOOGLE_TOKEN_URI, 625 revoke_uri=GOOGLE_REVOKE_URI, 626 user_agent=None, 627 message=None, 628 callback_path='/oauth2callback', 629 token_response_param=None, 630 _storage_class=StorageByKeyName, 631 _credentials_class=CredentialsModel, 632 _credentials_property_name='credentials', 633 **kwargs): 634 635 """Constructor for OAuth2Decorator 636 637 Args: 638 client_id: string, client identifier. 639 client_secret: string client secret. 640 scope: string or iterable of strings, scope(s) of the credentials being 641 requested. 642 auth_uri: string, URI for authorization endpoint. For convenience 643 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 644 token_uri: string, URI for token endpoint. For convenience 645 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 646 revoke_uri: string, URI for revoke endpoint. For convenience 647 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 648 user_agent: string, User agent of your application, default to None. 649 message: Message to display if there are problems with the OAuth 2.0 650 configuration. The message may contain HTML and will be presented on the 651 web interface for any method that uses the decorator. 652 callback_path: string, The absolute path to use as the callback URI. Note 653 that this must match up with the URI given when registering the 654 application in the APIs Console. 655 token_response_param: string. If provided, the full JSON response 656 to the access token request will be encoded and included in this query 657 parameter in the callback URI. This is useful with providers (e.g. 658 wordpress.com) that include extra fields that the client may want. 659 _storage_class: "Protected" keyword argument not typically provided to 660 this constructor. A storage class to aid in storing a Credentials object 661 for a user in the datastore. Defaults to StorageByKeyName. 662 _credentials_class: "Protected" keyword argument not typically provided to 663 this constructor. A db or ndb Model class to hold credentials. Defaults 664 to CredentialsModel. 665 _credentials_property_name: "Protected" keyword argument not typically 666 provided to this constructor. A string indicating the name of the field 667 on the _credentials_class where a Credentials object will be stored. 668 Defaults to 'credentials'. 669 **kwargs: dict, Keyword arguments are passed along as kwargs to 670 the OAuth2WebServerFlow constructor. 671 672 """ 673 self._tls = threading.local() 674 self.flow = None 675 self.credentials = None 676 self._client_id = client_id 677 self._client_secret = client_secret 678 self._scope = util.scopes_to_string(scope) 679 self._auth_uri = auth_uri 680 self._token_uri = token_uri 681 self._revoke_uri = revoke_uri 682 self._user_agent = user_agent 683 self._kwargs = kwargs 684 self._message = message 685 self._in_error = False 686 self._callback_path = callback_path 687 self._token_response_param = token_response_param 688 self._storage_class = _storage_class 689 self._credentials_class = _credentials_class 690 self._credentials_property_name = _credentials_property_name 691 692 def _display_error_message(self, request_handler): 693 request_handler.response.out.write('<html><body>') 694 request_handler.response.out.write(_safe_html(self._message)) 695 request_handler.response.out.write('</body></html>') 696 697 def oauth_required(self, method): 698 """Decorator that starts the OAuth 2.0 dance. 699 700 Starts the OAuth dance for the logged in user if they haven't already 701 granted access for this application. 702 703 Args: 704 method: callable, to be decorated method of a webapp.RequestHandler 705 instance. 706 """ 707 708 def check_oauth(request_handler, *args, **kwargs): 709 if self._in_error: 710 self._display_error_message(request_handler) 711 return 712 713 user = users.get_current_user() 714 # Don't use @login_decorator as this could be used in a POST request. 715 if not user: 716 request_handler.redirect(users.create_login_url( 717 request_handler.request.uri)) 718 return 719 720 self._create_flow(request_handler) 721 722 # Store the request URI in 'state' so we can use it later 723 self.flow.params['state'] = _build_state_value(request_handler, user) 724 self.credentials = self._storage_class( 725 self._credentials_class, None, 726 self._credentials_property_name, user=user).get() 727 728 if not self.has_credentials(): 729 return request_handler.redirect(self.authorize_url()) 730 try: 731 resp = method(request_handler, *args, **kwargs) 732 except AccessTokenRefreshError: 733 return request_handler.redirect(self.authorize_url()) 734 finally: 735 self.credentials = None 736 return resp 737 738 return check_oauth 739 740 def _create_flow(self, request_handler): 741 """Create the Flow object. 742 743 The Flow is calculated lazily since we don't know where this app is 744 running until it receives a request, at which point redirect_uri can be 745 calculated and then the Flow object can be constructed. 746 747 Args: 748 request_handler: webapp.RequestHandler, the request handler. 749 """ 750 if self.flow is None: 751 redirect_uri = request_handler.request.relative_url( 752 self._callback_path) # Usually /oauth2callback 753 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret, 754 self._scope, redirect_uri=redirect_uri, 755 user_agent=self._user_agent, 756 auth_uri=self._auth_uri, 757 token_uri=self._token_uri, 758 revoke_uri=self._revoke_uri, 759 **self._kwargs) 760 761 def oauth_aware(self, method): 762 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. 763 764 Does all the setup for the OAuth dance, but doesn't initiate it. 765 This decorator is useful if you want to create a page that knows 766 whether or not the user has granted access to this application. 767 From within a method decorated with @oauth_aware the has_credentials() 768 and authorize_url() methods can be called. 769 770 Args: 771 method: callable, to be decorated method of a webapp.RequestHandler 772 instance. 773 """ 774 775 def setup_oauth(request_handler, *args, **kwargs): 776 if self._in_error: 777 self._display_error_message(request_handler) 778 return 779 780 user = users.get_current_user() 781 # Don't use @login_decorator as this could be used in a POST request. 782 if not user: 783 request_handler.redirect(users.create_login_url( 784 request_handler.request.uri)) 785 return 786 787 self._create_flow(request_handler) 788 789 self.flow.params['state'] = _build_state_value(request_handler, user) 790 self.credentials = self._storage_class( 791 self._credentials_class, None, 792 self._credentials_property_name, user=user).get() 793 try: 794 resp = method(request_handler, *args, **kwargs) 795 finally: 796 self.credentials = None 797 return resp 798 return setup_oauth 799 800 801 def has_credentials(self): 802 """True if for the logged in user there are valid access Credentials. 803 804 Must only be called from with a webapp.RequestHandler subclassed method 805 that had been decorated with either @oauth_required or @oauth_aware. 806 """ 807 return self.credentials is not None and not self.credentials.invalid 808 809 def authorize_url(self): 810 """Returns the URL to start the OAuth dance. 811 812 Must only be called from with a webapp.RequestHandler subclassed method 813 that had been decorated with either @oauth_required or @oauth_aware. 814 """ 815 url = self.flow.step1_get_authorize_url() 816 return str(url) 817 818 def http(self, *args, **kwargs): 819 """Returns an authorized http instance. 820 821 Must only be called from within an @oauth_required decorated method, or 822 from within an @oauth_aware decorated method where has_credentials() 823 returns True. 824 825 Args: 826 *args: Positional arguments passed to httplib2.Http constructor. 827 **kwargs: Positional arguments passed to httplib2.Http constructor. 828 """ 829 return self.credentials.authorize(httplib2.Http(*args, **kwargs)) 830 831 @property 832 def callback_path(self): 833 """The absolute path where the callback will occur. 834 835 Note this is the absolute path, not the absolute URI, that will be 836 calculated by the decorator at runtime. See callback_handler() for how this 837 should be used. 838 839 Returns: 840 The callback path as a string. 841 """ 842 return self._callback_path 843 844 845 def callback_handler(self): 846 """RequestHandler for the OAuth 2.0 redirect callback. 847 848 Usage:: 849 850 app = webapp.WSGIApplication([ 851 ('/index', MyIndexHandler), 852 ..., 853 (decorator.callback_path, decorator.callback_handler()) 854 ]) 855 856 Returns: 857 A webapp.RequestHandler that handles the redirect back from the 858 server during the OAuth 2.0 dance. 859 """ 860 decorator = self 861 862 class OAuth2Handler(webapp.RequestHandler): 863 """Handler for the redirect_uri of the OAuth 2.0 dance.""" 864 865 @login_required 866 def get(self): 867 error = self.request.get('error') 868 if error: 869 errormsg = self.request.get('error_description', error) 870 self.response.out.write( 871 'The authorization request failed: %s' % _safe_html(errormsg)) 872 else: 873 user = users.get_current_user() 874 decorator._create_flow(self) 875 credentials = decorator.flow.step2_exchange(self.request.params) 876 decorator._storage_class( 877 decorator._credentials_class, None, 878 decorator._credentials_property_name, user=user).put(credentials) 879 redirect_uri = _parse_state_value(str(self.request.get('state')), 880 user) 881 882 if decorator._token_response_param and credentials.token_response: 883 resp_json = json.dumps(credentials.token_response) 884 redirect_uri = util._add_query_parameter( 885 redirect_uri, decorator._token_response_param, resp_json) 886 887 self.redirect(redirect_uri) 888 889 return OAuth2Handler 890 891 def callback_application(self): 892 """WSGI application for handling the OAuth 2.0 redirect callback. 893 894 If you need finer grained control use `callback_handler` which returns just 895 the webapp.RequestHandler. 896 897 Returns: 898 A webapp.WSGIApplication that handles the redirect back from the 899 server during the OAuth 2.0 dance. 900 """ 901 return webapp.WSGIApplication([ 902 (self.callback_path, self.callback_handler()) 903 ]) 904 905 906class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): 907 """An OAuth2Decorator that builds from a clientsecrets file. 908 909 Uses a clientsecrets file as the source for all the information when 910 constructing an OAuth2Decorator. 911 912 :: 913 914 decorator = OAuth2DecoratorFromClientSecrets( 915 os.path.join(os.path.dirname(__file__), 'client_secrets.json') 916 scope='https://www.googleapis.com/auth/plus') 917 918 class MainHandler(webapp.RequestHandler): 919 @decorator.oauth_required 920 def get(self): 921 http = decorator.http() 922 # http is authorized with the user's Credentials and can be used 923 # in API calls 924 925 """ 926 927 @util.positional(3) 928 def __init__(self, filename, scope, message=None, cache=None, **kwargs): 929 """Constructor 930 931 Args: 932 filename: string, File name of client secrets. 933 scope: string or iterable of strings, scope(s) of the credentials being 934 requested. 935 message: string, A friendly string to display to the user if the 936 clientsecrets file is missing or invalid. The message may contain HTML 937 and will be presented on the web interface for any method that uses the 938 decorator. 939 cache: An optional cache service client that implements get() and set() 940 methods. See clientsecrets.loadfile() for details. 941 **kwargs: dict, Keyword arguments are passed along as kwargs to 942 the OAuth2WebServerFlow constructor. 943 """ 944 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 945 if client_type not in [ 946 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 947 raise InvalidClientSecretsError( 948 "OAuth2Decorator doesn't support this OAuth 2.0 flow.") 949 constructor_kwargs = dict(kwargs) 950 constructor_kwargs.update({ 951 'auth_uri': client_info['auth_uri'], 952 'token_uri': client_info['token_uri'], 953 'message': message, 954 }) 955 revoke_uri = client_info.get('revoke_uri') 956 if revoke_uri is not None: 957 constructor_kwargs['revoke_uri'] = revoke_uri 958 super(OAuth2DecoratorFromClientSecrets, self).__init__( 959 client_info['client_id'], client_info['client_secret'], 960 scope, **constructor_kwargs) 961 if message is not None: 962 self._message = message 963 else: 964 self._message = 'Please configure your application for OAuth 2.0.' 965 966 967@util.positional(2) 968def oauth2decorator_from_clientsecrets(filename, scope, 969 message=None, cache=None): 970 """Creates an OAuth2Decorator populated from a clientsecrets file. 971 972 Args: 973 filename: string, File name of client secrets. 974 scope: string or list of strings, scope(s) of the credentials being 975 requested. 976 message: string, A friendly string to display to the user if the 977 clientsecrets file is missing or invalid. The message may contain HTML and 978 will be presented on the web interface for any method that uses the 979 decorator. 980 cache: An optional cache service client that implements get() and set() 981 methods. See clientsecrets.loadfile() for details. 982 983 Returns: An OAuth2Decorator 984 985 """ 986 return OAuth2DecoratorFromClientSecrets(filename, scope, 987 message=message, cache=cache) 988