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