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 clientsecrets 
 38  from oauth2client import util 
 39  from oauth2client import xsrfutil 
 40  from oauth2client.anyjson import simplejson 
 41  from oauth2client.client import AccessTokenRefreshError 
 42  from oauth2client.client import AssertionCredentials 
 43  from oauth2client.client import Credentials 
 44  from oauth2client.client import Flow 
 45  from oauth2client.client import OAuth2WebServerFlow 
 46  from oauth2client.client import Storage 
 47   
 48  logger = logging.getLogger(__name__) 
 49   
 50  OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' 
 51   
 52  XSRF_MEMCACHE_ID = 'xsrf_secret_key' 
53 54 55 -def _safe_html(s):
56 """Escape text to make it safe to display. 57 58 Args: 59 s: string, The text to escape. 60 61 Returns: 62 The escaped text as a string. 63 """ 64 return cgi.escape(s, quote=1).replace("'", ''')
65
66 67 -class InvalidClientSecretsError(Exception):
68 """The client_secrets.json file is malformed or missing required fields."""
69
70 71 -class InvalidXsrfTokenError(Exception):
72 """The XSRF token is invalid or expired."""
73
74 75 -class SiteXsrfSecretKey(db.Model):
76 """Storage for the sites XSRF secret key. 77 78 There will only be one instance stored of this model, the one used for the 79 site. """ 80 secret = db.StringProperty()
81
82 83 -def _generate_new_xsrf_secret_key():
84 """Returns a random XSRF secret key. 85 """ 86 return os.urandom(16).encode("hex")
87
88 89 -def xsrf_secret_key():
90 """Return the secret key for use for XSRF protection. 91 92 If the Site entity does not have a secret key, this method will also create 93 one and persist it. 94 95 Returns: 96 The secret key. 97 """ 98 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE) 99 if not secret: 100 # Load the one and only instance of SiteXsrfSecretKey. 101 model = SiteXsrfSecretKey.get_or_insert(key_name='site') 102 if not model.secret: 103 model.secret = _generate_new_xsrf_secret_key() 104 model.put() 105 secret = model.secret 106 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE) 107 108 return str(secret)
109
110 111 -class AppAssertionCredentials(AssertionCredentials):
112 """Credentials object for App Engine Assertion Grants 113 114 This object will allow an App Engine application to identify itself to Google 115 and other OAuth 2.0 servers that can verify assertions. It can be used for the 116 purpose of accessing data stored under an account assigned to the App Engine 117 application itself. 118 119 This credential does not require a flow to instantiate because it represents 120 a two legged flow, and therefore has all of the required information to 121 generate and refresh its own access tokens. 122 """ 123 124 @util.positional(2)
125 - def __init__(self, scope, **kwargs):
126 """Constructor for AppAssertionCredentials 127 128 Args: 129 scope: string or list of strings, scope(s) of the credentials being 130 requested. 131 """ 132 if type(scope) is list: 133 scope = ' '.join(scope) 134 self.scope = scope 135 136 super(AppAssertionCredentials, self).__init__( 137 'ignored' # assertion_type is ignore in this subclass. 138 )
139 140 @classmethod
141 - def from_json(cls, json):
142 data = simplejson.loads(json) 143 return AppAssertionCredentials(data['scope'])
144
145 - def _refresh(self, http_request):
146 """Refreshes the access_token. 147 148 Since the underlying App Engine app_identity implementation does its own 149 caching we can skip all the storage hoops and just to a refresh using the 150 API. 151 152 Args: 153 http_request: callable, a callable that matches the method signature of 154 httplib2.Http.request, used to make the refresh request. 155 156 Raises: 157 AccessTokenRefreshError: When the refresh fails. 158 """ 159 try: 160 (token, _) = app_identity.get_access_token(self.scope) 161 except app_identity.Error, e: 162 raise AccessTokenRefreshError(str(e)) 163 self.access_token = token
164
165 166 -class FlowProperty(db.Property):
167 """App Engine datastore Property for Flow. 168 169 Utility property that allows easy storage and retreival of an 170 oauth2client.Flow""" 171 172 # Tell what the user type is. 173 data_type = Flow 174 175 # For writing to datastore.
176 - def get_value_for_datastore(self, model_instance):
177 flow = super(FlowProperty, 178 self).get_value_for_datastore(model_instance) 179 return db.Blob(pickle.dumps(flow))
180 181 # For reading from datastore.
182 - def make_value_from_datastore(self, value):
183 if value is None: 184 return None 185 return pickle.loads(value)
186
187 - def validate(self, value):
188 if value is not None and not isinstance(value, Flow): 189 raise db.BadValueError('Property %s must be convertible ' 190 'to a FlowThreeLegged instance (%s)' % 191 (self.name, value)) 192 return super(FlowProperty, self).validate(value)
193
194 - def empty(self, value):
195 return not value
196
197 198 -class CredentialsProperty(db.Property):
199 """App Engine datastore Property for Credentials. 200 201 Utility property that allows easy storage and retrieval of 202 oath2client.Credentials 203 """ 204 205 # Tell what the user type is. 206 data_type = Credentials 207 208 # For writing to datastore.
209 - def get_value_for_datastore(self, model_instance):
210 logger.info("get: Got type " + str(type(model_instance))) 211 cred = super(CredentialsProperty, 212 self).get_value_for_datastore(model_instance) 213 if cred is None: 214 cred = '' 215 else: 216 cred = cred.to_json() 217 return db.Blob(cred)
218 219 # For reading from datastore.
220 - def make_value_from_datastore(self, value):
221 logger.info("make: Got type " + str(type(value))) 222 if value is None: 223 return None 224 if len(value) == 0: 225 return None 226 try: 227 credentials = Credentials.new_from_json(value) 228 except ValueError: 229 credentials = None 230 return credentials
231
232 - def validate(self, value):
233 value = super(CredentialsProperty, self).validate(value) 234 logger.info("validate: Got type " + str(type(value))) 235 if value is not None and not isinstance(value, Credentials): 236 raise db.BadValueError('Property %s must be convertible ' 237 'to a Credentials instance (%s)' % 238 (self.name, value)) 239 #if value is not None and not isinstance(value, Credentials): 240 # return None 241 return value
242
243 244 -class StorageByKeyName(Storage):
245 """Store and retrieve a single credential to and from 246 the App Engine datastore. 247 248 This Storage helper presumes the Credentials 249 have been stored as a CredenialsProperty 250 on a datastore model class, and that entities 251 are stored by key_name. 252 """ 253 254 @util.positional(4)
255 - def __init__(self, model, key_name, property_name, cache=None):
256 """Constructor for Storage. 257 258 Args: 259 model: db.Model, model class 260 key_name: string, key name for the entity that has the credentials 261 property_name: string, name of the property that is a CredentialsProperty 262 cache: memcache, a write-through cache to put in front of the datastore 263 """ 264 self._model = model 265 self._key_name = key_name 266 self._property_name = property_name 267 self._cache = cache
268
269 - def locked_get(self):
270 """Retrieve Credential from datastore. 271 272 Returns: 273 oauth2client.Credentials 274 """ 275 if self._cache: 276 json = self._cache.get(self._key_name) 277 if json: 278 return Credentials.new_from_json(json) 279 280 credential = None 281 entity = self._model.get_by_key_name(self._key_name) 282 if entity is not None: 283 credential = getattr(entity, self._property_name) 284 if credential and hasattr(credential, 'set_store'): 285 credential.set_store(self) 286 if self._cache: 287 self._cache.set(self._key_name, credential.to_json()) 288 289 return credential
290
291 - def locked_put(self, credentials):
292 """Write a Credentials to the datastore. 293 294 Args: 295 credentials: Credentials, the credentials to store. 296 """ 297 entity = self._model.get_or_insert(self._key_name) 298 setattr(entity, self._property_name, credentials) 299 entity.put() 300 if self._cache: 301 self._cache.set(self._key_name, credentials.to_json())
302
303 - def locked_delete(self):
304 """Delete Credential from datastore.""" 305 306 if self._cache: 307 self._cache.delete(self._key_name) 308 309 entity = self._model.get_by_key_name(self._key_name) 310 if entity is not None: 311 entity.delete()
312
313 314 -class CredentialsModel(db.Model):
315 """Storage for OAuth 2.0 Credentials 316 317 Storage of the model is keyed by the user.user_id(). 318 """ 319 credentials = CredentialsProperty()
320
321 322 -def _build_state_value(request_handler, user):
323 """Composes the value for the 'state' parameter. 324 325 Packs the current request URI and an XSRF token into an opaque string that 326 can be passed to the authentication server via the 'state' parameter. 327 328 Args: 329 request_handler: webapp.RequestHandler, The request. 330 user: google.appengine.api.users.User, The current user. 331 332 Returns: 333 The state value as a string. 334 """ 335 uri = request_handler.request.url 336 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(), 337 action_id=str(uri)) 338 return uri + ':' + token
339
340 341 -def _parse_state_value(state, user):
342 """Parse the value of the 'state' parameter. 343 344 Parses the value and validates the XSRF token in the state parameter. 345 346 Args: 347 state: string, The value of the state parameter. 348 user: google.appengine.api.users.User, The current user. 349 350 Raises: 351 InvalidXsrfTokenError: if the XSRF token is invalid. 352 353 Returns: 354 The redirect URI. 355 """ 356 uri, token = state.rsplit(':', 1) 357 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(), 358 action_id=uri): 359 raise InvalidXsrfTokenError() 360 361 return uri
362
363 364 -class OAuth2Decorator(object):
365 """Utility for making OAuth 2.0 easier. 366 367 Instantiate and then use with oauth_required or oauth_aware 368 as decorators on webapp.RequestHandler methods. 369 370 Example: 371 372 decorator = OAuth2Decorator( 373 client_id='837...ent.com', 374 client_secret='Qh...wwI', 375 scope='https://www.googleapis.com/auth/plus') 376 377 378 class MainHandler(webapp.RequestHandler): 379 380 @decorator.oauth_required 381 def get(self): 382 http = decorator.http() 383 # http is authorized with the user's Credentials and can be used 384 # in API calls 385 386 """ 387 388 @util.positional(4)
389 - def __init__(self, client_id, client_secret, scope, 390 auth_uri='https://accounts.google.com/o/oauth2/auth', 391 token_uri='https://accounts.google.com/o/oauth2/token', 392 user_agent=None, 393 message=None, 394 callback_path='/oauth2callback', 395 **kwargs):
396 397 """Constructor for OAuth2Decorator 398 399 Args: 400 client_id: string, client identifier. 401 client_secret: string client secret. 402 scope: string or list of strings, scope(s) of the credentials being 403 requested. 404 auth_uri: string, URI for authorization endpoint. For convenience 405 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 406 token_uri: string, URI for token endpoint. For convenience 407 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 408 user_agent: string, User agent of your application, default to None. 409 message: Message to display if there are problems with the OAuth 2.0 410 configuration. The message may contain HTML and will be presented on the 411 web interface for any method that uses the decorator. 412 callback_path: string, The absolute path to use as the callback URI. Note 413 that this must match up with the URI given when registering the 414 application in the APIs Console. 415 **kwargs: dict, Keyword arguments are be passed along as kwargs to the 416 OAuth2WebServerFlow constructor. 417 """ 418 self.flow = None 419 self.credentials = None 420 self._client_id = client_id 421 self._client_secret = client_secret 422 self._scope = scope 423 self._auth_uri = auth_uri 424 self._token_uri = token_uri 425 self._user_agent = user_agent 426 self._kwargs = kwargs 427 self._message = message 428 self._in_error = False 429 self._callback_path = callback_path
430
431 - def _display_error_message(self, request_handler):
432 request_handler.response.out.write('<html><body>') 433 request_handler.response.out.write(_safe_html(self._message)) 434 request_handler.response.out.write('</body></html>')
435
436 - def oauth_required(self, method):
437 """Decorator that starts the OAuth 2.0 dance. 438 439 Starts the OAuth dance for the logged in user if they haven't already 440 granted access for this application. 441 442 Args: 443 method: callable, to be decorated method of a webapp.RequestHandler 444 instance. 445 """ 446 447 def check_oauth(request_handler, *args, **kwargs): 448 if self._in_error: 449 self._display_error_message(request_handler) 450 return 451 452 user = users.get_current_user() 453 # Don't use @login_decorator as this could be used in a POST request. 454 if not user: 455 request_handler.redirect(users.create_login_url( 456 request_handler.request.uri)) 457 return 458 459 self._create_flow(request_handler) 460 461 # Store the request URI in 'state' so we can use it later 462 self.flow.params['state'] = _build_state_value(request_handler, user) 463 self.credentials = StorageByKeyName( 464 CredentialsModel, user.user_id(), 'credentials').get() 465 466 if not self.has_credentials(): 467 return request_handler.redirect(self.authorize_url()) 468 try: 469 return method(request_handler, *args, **kwargs) 470 except AccessTokenRefreshError: 471 return request_handler.redirect(self.authorize_url())
472 473 return check_oauth
474
475 - def _create_flow(self, request_handler):
476 """Create the Flow object. 477 478 The Flow is calculated lazily since we don't know where this app is 479 running until it receives a request, at which point redirect_uri can be 480 calculated and then the Flow object can be constructed. 481 482 Args: 483 request_handler: webapp.RequestHandler, the request handler. 484 """ 485 if self.flow is None: 486 redirect_uri = request_handler.request.relative_url( 487 self._callback_path) # Usually /oauth2callback 488 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret, 489 self._scope, redirect_uri=redirect_uri, 490 user_agent=self._user_agent, 491 auth_uri=self._auth_uri, 492 token_uri=self._token_uri, **self._kwargs)
493 494
495 - def oauth_aware(self, method):
496 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. 497 498 Does all the setup for the OAuth dance, but doesn't initiate it. 499 This decorator is useful if you want to create a page that knows 500 whether or not the user has granted access to this application. 501 From within a method decorated with @oauth_aware the has_credentials() 502 and authorize_url() methods can be called. 503 504 Args: 505 method: callable, to be decorated method of a webapp.RequestHandler 506 instance. 507 """ 508 509 def setup_oauth(request_handler, *args, **kwargs): 510 if self._in_error: 511 self._display_error_message(request_handler) 512 return 513 514 user = users.get_current_user() 515 # Don't use @login_decorator as this could be used in a POST request. 516 if not user: 517 request_handler.redirect(users.create_login_url( 518 request_handler.request.uri)) 519 return 520 521 self._create_flow(request_handler) 522 523 self.flow.params['state'] = _build_state_value(request_handler, user) 524 self.credentials = StorageByKeyName( 525 CredentialsModel, user.user_id(), 'credentials').get() 526 return method(request_handler, *args, **kwargs)
527 return setup_oauth 528
529 - def has_credentials(self):
530 """True if for the logged in user there are valid access Credentials. 531 532 Must only be called from with a webapp.RequestHandler subclassed method 533 that had been decorated with either @oauth_required or @oauth_aware. 534 """ 535 return self.credentials is not None and not self.credentials.invalid
536
537 - def authorize_url(self):
538 """Returns the URL to start the OAuth dance. 539 540 Must only be called from with a webapp.RequestHandler subclassed method 541 that had been decorated with either @oauth_required or @oauth_aware. 542 """ 543 url = self.flow.step1_get_authorize_url() 544 return str(url)
545
546 - def http(self):
547 """Returns an authorized http instance. 548 549 Must only be called from within an @oauth_required decorated method, or 550 from within an @oauth_aware decorated method where has_credentials() 551 returns True. 552 """ 553 return self.credentials.authorize(httplib2.Http())
554 555 @property
556 - def callback_path(self):
557 """The absolute path where the callback will occur. 558 559 Note this is the absolute path, not the absolute URI, that will be 560 calculated by the decorator at runtime. See callback_handler() for how this 561 should be used. 562 563 Returns: 564 The callback path as a string. 565 """ 566 return self._callback_path
567 568
569 - def callback_handler(self):
570 """RequestHandler for the OAuth 2.0 redirect callback. 571 572 Usage: 573 app = webapp.WSGIApplication([ 574 ('/index', MyIndexHandler), 575 ..., 576 (decorator.callback_path, decorator.callback_handler()) 577 ]) 578 579 Returns: 580 A webapp.RequestHandler that handles the redirect back from the 581 server during the OAuth 2.0 dance. 582 """ 583 decorator = self 584 585 class OAuth2Handler(webapp.RequestHandler): 586 """Handler for the redirect_uri of the OAuth 2.0 dance.""" 587 588 @login_required 589 def get(self): 590 error = self.request.get('error') 591 if error: 592 errormsg = self.request.get('error_description', error) 593 self.response.out.write( 594 'The authorization request failed: %s' % _safe_html(errormsg)) 595 else: 596 user = users.get_current_user() 597 decorator._create_flow(self) 598 credentials = decorator.flow.step2_exchange(self.request.params) 599 StorageByKeyName( 600 CredentialsModel, user.user_id(), 'credentials').put(credentials) 601 redirect_uri = _parse_state_value(str(self.request.get('state')), 602 user) 603 self.redirect(redirect_uri)
604 605 return OAuth2Handler 606
607 - def callback_application(self):
608 """WSGI application for handling the OAuth 2.0 redirect callback. 609 610 If you need finer grained control use `callback_handler` which returns just 611 the webapp.RequestHandler. 612 613 Returns: 614 A webapp.WSGIApplication that handles the redirect back from the 615 server during the OAuth 2.0 dance. 616 """ 617 return webapp.WSGIApplication([ 618 (self.callback_path, self.callback_handler()) 619 ])
620
621 622 -class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
623 """An OAuth2Decorator that builds from a clientsecrets file. 624 625 Uses a clientsecrets file as the source for all the information when 626 constructing an OAuth2Decorator. 627 628 Example: 629 630 decorator = OAuth2DecoratorFromClientSecrets( 631 os.path.join(os.path.dirname(__file__), 'client_secrets.json') 632 scope='https://www.googleapis.com/auth/plus') 633 634 635 class MainHandler(webapp.RequestHandler): 636 637 @decorator.oauth_required 638 def get(self): 639 http = decorator.http() 640 # http is authorized with the user's Credentials and can be used 641 # in API calls 642 """ 643 644 @util.positional(3)
645 - def __init__(self, filename, scope, message=None, cache=None):
646 """Constructor 647 648 Args: 649 filename: string, File name of client secrets. 650 scope: string or list of strings, scope(s) of the credentials being 651 requested. 652 message: string, A friendly string to display to the user if the 653 clientsecrets file is missing or invalid. The message may contain HTML 654 and will be presented on the web interface for any method that uses the 655 decorator. 656 cache: An optional cache service client that implements get() and set() 657 methods. See clientsecrets.loadfile() for details. 658 """ 659 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 660 if client_type not in [ 661 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 662 raise InvalidClientSecretsError( 663 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.') 664 super(OAuth2DecoratorFromClientSecrets, self).__init__( 665 client_info['client_id'], 666 client_info['client_secret'], 667 scope, 668 auth_uri=client_info['auth_uri'], 669 token_uri=client_info['token_uri'], 670 message=message) 671 if message is not None: 672 self._message = message 673 else: 674 self._message = "Please configure your application for OAuth 2.0"
675
676 677 @util.positional(2) 678 -def oauth2decorator_from_clientsecrets(filename, scope, 679 message=None, cache=None):
680 """Creates an OAuth2Decorator populated from a clientsecrets file. 681 682 Args: 683 filename: string, File name of client secrets. 684 scope: string or list of strings, scope(s) of the credentials being 685 requested. 686 message: string, A friendly string to display to the user if the 687 clientsecrets file is missing or invalid. The message may contain HTML and 688 will be presented on the web interface for any method that uses the 689 decorator. 690 cache: An optional cache service client that implements get() and set() 691 methods. See clientsecrets.loadfile() for details. 692 693 Returns: An OAuth2Decorator 694 695 """ 696 return OAuth2DecoratorFromClientSecrets(filename, scope, 697 message=message, cache=cache)
698