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