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 pickle 
 26  import time 
 27   
 28  import clientsecrets 
 29   
 30  from anyjson import simplejson 
 31  from client import AccessTokenRefreshError 
 32  from client import AssertionCredentials 
 33  from client import Credentials 
 34  from client import Flow 
 35  from client import OAuth2WebServerFlow 
 36  from client import Storage 
 37  from google.appengine.api import memcache 
 38  from google.appengine.api import users 
 39  from google.appengine.api import app_identity 
 40  from google.appengine.ext import db 
 41  from google.appengine.ext import webapp 
 42  from google.appengine.ext.webapp.util import login_required 
 43  from google.appengine.ext.webapp.util import run_wsgi_app 
 44   
 45   
 46  logger = logging.getLogger(__name__) 
 47   
 48  OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' 
49 50 51 -class InvalidClientSecretsError(Exception):
52 """The client_secrets.json file is malformed or missing required fields.""" 53 pass
54
55 56 -class AppAssertionCredentials(AssertionCredentials):
57 """Credentials object for App Engine Assertion Grants 58 59 This object will allow an App Engine application to identify itself to Google 60 and other OAuth 2.0 servers that can verify assertions. It can be used for 61 the purpose of accessing data stored under an account assigned to the App 62 Engine application itself. 63 64 This credential does not require a flow to instantiate because it represents 65 a two legged flow, and therefore has all of the required information to 66 generate and refresh its own access tokens. 67 """ 68
69 - def __init__(self, scope, **kwargs):
70 """Constructor for AppAssertionCredentials 71 72 Args: 73 scope: string or list of strings, scope(s) of the credentials being requested. 74 """ 75 if type(scope) is list: 76 scope = ' '.join(scope) 77 self.scope = scope 78 79 super(AppAssertionCredentials, self).__init__( 80 None, 81 None, 82 None)
83 84 @classmethod
85 - def from_json(cls, json):
86 data = simplejson.loads(json) 87 return AppAssertionCredentials(data['scope'])
88
89 - def _refresh(self, http_request):
90 """Refreshes the access_token. 91 92 Since the underlying App Engine app_identity implementation does its own 93 caching we can skip all the storage hoops and just to a refresh using the 94 API. 95 96 Args: 97 http_request: callable, a callable that matches the method signature of 98 httplib2.Http.request, used to make the refresh request. 99 100 Raises: 101 AccessTokenRefreshError: When the refresh fails. 102 """ 103 try: 104 (token, _) = app_identity.get_access_token(self.scope) 105 except app_identity.Error, e: 106 raise AccessTokenRefreshError(str(e)) 107 self.access_token = token
108
109 110 -class FlowProperty(db.Property):
111 """App Engine datastore Property for Flow. 112 113 Utility property that allows easy storage and retreival of an 114 oauth2client.Flow""" 115 116 # Tell what the user type is. 117 data_type = Flow 118 119 # For writing to datastore.
120 - def get_value_for_datastore(self, model_instance):
121 flow = super(FlowProperty, 122 self).get_value_for_datastore(model_instance) 123 return db.Blob(pickle.dumps(flow))
124 125 # For reading from datastore.
126 - def make_value_from_datastore(self, value):
127 if value is None: 128 return None 129 return pickle.loads(value)
130
131 - def validate(self, value):
132 if value is not None and not isinstance(value, Flow): 133 raise db.BadValueError('Property %s must be convertible ' 134 'to a FlowThreeLegged instance (%s)' % 135 (self.name, value)) 136 return super(FlowProperty, self).validate(value)
137
138 - def empty(self, value):
139 return not value
140
141 142 -class CredentialsProperty(db.Property):
143 """App Engine datastore Property for Credentials. 144 145 Utility property that allows easy storage and retrieval of 146 oath2client.Credentials 147 """ 148 149 # Tell what the user type is. 150 data_type = Credentials 151 152 # For writing to datastore.
153 - def get_value_for_datastore(self, model_instance):
154 logger.info("get: Got type " + str(type(model_instance))) 155 cred = super(CredentialsProperty, 156 self).get_value_for_datastore(model_instance) 157 if cred is None: 158 cred = '' 159 else: 160 cred = cred.to_json() 161 return db.Blob(cred)
162 163 # For reading from datastore.
164 - def make_value_from_datastore(self, value):
165 logger.info("make: Got type " + str(type(value))) 166 if value is None: 167 return None 168 if len(value) == 0: 169 return None 170 try: 171 credentials = Credentials.new_from_json(value) 172 except ValueError: 173 credentials = None 174 return credentials
175
176 - def validate(self, value):
177 value = super(CredentialsProperty, self).validate(value) 178 logger.info("validate: Got type " + str(type(value))) 179 if value is not None and not isinstance(value, Credentials): 180 raise db.BadValueError('Property %s must be convertible ' 181 'to a Credentials instance (%s)' % 182 (self.name, value)) 183 #if value is not None and not isinstance(value, Credentials): 184 # return None 185 return value
186
187 188 -class StorageByKeyName(Storage):
189 """Store and retrieve a single credential to and from 190 the App Engine datastore. 191 192 This Storage helper presumes the Credentials 193 have been stored as a CredenialsProperty 194 on a datastore model class, and that entities 195 are stored by key_name. 196 """ 197
198 - def __init__(self, model, key_name, property_name, cache=None):
199 """Constructor for Storage. 200 201 Args: 202 model: db.Model, model class 203 key_name: string, key name for the entity that has the credentials 204 property_name: string, name of the property that is a CredentialsProperty 205 cache: memcache, a write-through cache to put in front of the datastore 206 """ 207 self._model = model 208 self._key_name = key_name 209 self._property_name = property_name 210 self._cache = cache
211
212 - def locked_get(self):
213 """Retrieve Credential from datastore. 214 215 Returns: 216 oauth2client.Credentials 217 """ 218 if self._cache: 219 json = self._cache.get(self._key_name) 220 if json: 221 return Credentials.new_from_json(json) 222 223 credential = None 224 entity = self._model.get_by_key_name(self._key_name) 225 if entity is not None: 226 credential = getattr(entity, self._property_name) 227 if credential and hasattr(credential, 'set_store'): 228 credential.set_store(self) 229 if self._cache: 230 self._cache.set(self._key_name, credential.to_json()) 231 232 return credential
233
234 - def locked_put(self, credentials):
235 """Write a Credentials to the datastore. 236 237 Args: 238 credentials: Credentials, the credentials to store. 239 """ 240 entity = self._model.get_or_insert(self._key_name) 241 setattr(entity, self._property_name, credentials) 242 entity.put() 243 if self._cache: 244 self._cache.set(self._key_name, credentials.to_json())
245
246 - def locked_delete(self):
247 """Delete Credential from datastore.""" 248 249 if self._cache: 250 self._cache.delete(self._key_name) 251 252 entity = self._model.get_by_key_name(self._key_name) 253 if entity is not None: 254 entity.delete()
255
256 257 -class CredentialsModel(db.Model):
258 """Storage for OAuth 2.0 Credentials 259 260 Storage of the model is keyed by the user.user_id(). 261 """ 262 credentials = CredentialsProperty()
263
264 265 -class OAuth2Decorator(object):
266 """Utility for making OAuth 2.0 easier. 267 268 Instantiate and then use with oauth_required or oauth_aware 269 as decorators on webapp.RequestHandler methods. 270 271 Example: 272 273 decorator = OAuth2Decorator( 274 client_id='837...ent.com', 275 client_secret='Qh...wwI', 276 scope='https://www.googleapis.com/auth/plus') 277 278 279 class MainHandler(webapp.RequestHandler): 280 281 @decorator.oauth_required 282 def get(self): 283 http = decorator.http() 284 # http is authorized with the user's Credentials and can be used 285 # in API calls 286 287 """ 288
289 - def __init__(self, client_id, client_secret, scope, 290 auth_uri='https://accounts.google.com/o/oauth2/auth', 291 token_uri='https://accounts.google.com/o/oauth2/token', 292 user_agent=None, 293 message=None, **kwargs):
294 295 """Constructor for OAuth2Decorator 296 297 Args: 298 client_id: string, client identifier. 299 client_secret: string client secret. 300 scope: string or list of strings, scope(s) of the credentials being 301 requested. 302 auth_uri: string, URI for authorization endpoint. For convenience 303 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 304 token_uri: string, URI for token endpoint. For convenience 305 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 306 user_agent: string, User agent of your application, default to None. 307 message: Message to display if there are problems with the OAuth 2.0 308 configuration. The message may contain HTML and will be presented on the 309 web interface for any method that uses the decorator. 310 **kwargs: dict, Keyword arguments are be passed along as kwargs to the 311 OAuth2WebServerFlow constructor. 312 """ 313 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent, 314 auth_uri, token_uri, **kwargs) 315 self.credentials = None 316 self._request_handler = None 317 self._message = message 318 self._in_error = False
319
320 - def _display_error_message(self, request_handler):
321 request_handler.response.out.write('<html><body>') 322 request_handler.response.out.write(self._message) 323 request_handler.response.out.write('</body></html>')
324
325 - def oauth_required(self, method):
326 """Decorator that starts the OAuth 2.0 dance. 327 328 Starts the OAuth dance for the logged in user if they haven't already 329 granted access for this application. 330 331 Args: 332 method: callable, to be decorated method of a webapp.RequestHandler 333 instance. 334 """ 335 336 def check_oauth(request_handler, *args, **kwargs): 337 if self._in_error: 338 self._display_error_message(request_handler) 339 return 340 341 user = users.get_current_user() 342 # Don't use @login_decorator as this could be used in a POST request. 343 if not user: 344 request_handler.redirect(users.create_login_url( 345 request_handler.request.uri)) 346 return 347 # Store the request URI in 'state' so we can use it later 348 self.flow.params['state'] = request_handler.request.url 349 self._request_handler = request_handler 350 self.credentials = StorageByKeyName( 351 CredentialsModel, user.user_id(), 'credentials').get() 352 353 if not self.has_credentials(): 354 return request_handler.redirect(self.authorize_url()) 355 try: 356 method(request_handler, *args, **kwargs) 357 except AccessTokenRefreshError: 358 return request_handler.redirect(self.authorize_url())
359 360 return check_oauth
361
362 - def oauth_aware(self, method):
363 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. 364 365 Does all the setup for the OAuth dance, but doesn't initiate it. 366 This decorator is useful if you want to create a page that knows 367 whether or not the user has granted access to this application. 368 From within a method decorated with @oauth_aware the has_credentials() 369 and authorize_url() methods can be called. 370 371 Args: 372 method: callable, to be decorated method of a webapp.RequestHandler 373 instance. 374 """ 375 376 def setup_oauth(request_handler, *args, **kwargs): 377 if self._in_error: 378 self._display_error_message(request_handler) 379 return 380 381 user = users.get_current_user() 382 # Don't use @login_decorator as this could be used in a POST request. 383 if not user: 384 request_handler.redirect(users.create_login_url( 385 request_handler.request.uri)) 386 return 387 388 389 self.flow.params['state'] = request_handler.request.url 390 self._request_handler = request_handler 391 self.credentials = StorageByKeyName( 392 CredentialsModel, user.user_id(), 'credentials').get() 393 method(request_handler, *args, **kwargs)
394 return setup_oauth 395
396 - def has_credentials(self):
397 """True if for the logged in user there are valid access Credentials. 398 399 Must only be called from with a webapp.RequestHandler subclassed method 400 that had been decorated with either @oauth_required or @oauth_aware. 401 """ 402 return self.credentials is not None and not self.credentials.invalid
403
404 - def authorize_url(self):
405 """Returns the URL to start the OAuth dance. 406 407 Must only be called from with a webapp.RequestHandler subclassed method 408 that had been decorated with either @oauth_required or @oauth_aware. 409 """ 410 callback = self._request_handler.request.relative_url('/oauth2callback') 411 url = self.flow.step1_get_authorize_url(callback) 412 user = users.get_current_user() 413 memcache.set(user.user_id(), pickle.dumps(self.flow), 414 namespace=OAUTH2CLIENT_NAMESPACE) 415 return str(url)
416
417 - def http(self):
418 """Returns an authorized http instance. 419 420 Must only be called from within an @oauth_required decorated method, or 421 from within an @oauth_aware decorated method where has_credentials() 422 returns True. 423 """ 424 return self.credentials.authorize(httplib2.Http())
425
426 427 -class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
428 """An OAuth2Decorator that builds from a clientsecrets file. 429 430 Uses a clientsecrets file as the source for all the information when 431 constructing an OAuth2Decorator. 432 433 Example: 434 435 decorator = OAuth2DecoratorFromClientSecrets( 436 os.path.join(os.path.dirname(__file__), 'client_secrets.json') 437 scope='https://www.googleapis.com/auth/plus') 438 439 440 class MainHandler(webapp.RequestHandler): 441 442 @decorator.oauth_required 443 def get(self): 444 http = decorator.http() 445 # http is authorized with the user's Credentials and can be used 446 # in API calls 447 """ 448
449 - def __init__(self, filename, scope, message=None, cache=None):
450 """Constructor 451 452 Args: 453 filename: string, File name of client secrets. 454 scope: string or list of strings, scope(s) of the credentials being 455 requested. 456 message: string, A friendly string to display to the user if the 457 clientsecrets file is missing or invalid. The message may contain HTML and 458 will be presented on the web interface for any method that uses the 459 decorator. 460 cache: An optional cache service client that implements get() and set() 461 methods. See clientsecrets.loadfile() for details. 462 """ 463 try: 464 client_type, client_info = clientsecrets.loadfile(filename, cache=cache) 465 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]: 466 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.') 467 super(OAuth2DecoratorFromClientSecrets, 468 self).__init__( 469 client_info['client_id'], 470 client_info['client_secret'], 471 scope, 472 client_info['auth_uri'], 473 client_info['token_uri'], 474 message) 475 except clientsecrets.InvalidClientSecretsError: 476 self._in_error = True 477 if message is not None: 478 self._message = message 479 else: 480 self._message = "Please configure your application for OAuth 2.0"
481
482 483 -def oauth2decorator_from_clientsecrets(filename, scope, 484 message=None, cache=None):
485 """Creates an OAuth2Decorator populated from a clientsecrets file. 486 487 Args: 488 filename: string, File name of client secrets. 489 scope: string or list of strings, scope(s) of the credentials being 490 requested. 491 message: string, A friendly string to display to the user if the 492 clientsecrets file is missing or invalid. The message may contain HTML and 493 will be presented on the web interface for any method that uses the 494 decorator. 495 cache: An optional cache service client that implements get() and set() 496 methods. See clientsecrets.loadfile() for details. 497 498 Returns: An OAuth2Decorator 499 500 """ 501 return OAuth2DecoratorFromClientSecrets(filename, scope, 502 message=message, cache=cache)
503
504 505 -class OAuth2Handler(webapp.RequestHandler):
506 """Handler for the redirect_uri of the OAuth 2.0 dance.""" 507 508 @login_required
509 - def get(self):
510 error = self.request.get('error') 511 if error: 512 errormsg = self.request.get('error_description', error) 513 self.response.out.write( 514 'The authorization request failed: %s' % errormsg) 515 else: 516 user = users.get_current_user() 517 flow = pickle.loads(memcache.get(user.user_id(), 518 namespace=OAUTH2CLIENT_NAMESPACE)) 519 # This code should be ammended with application specific error 520 # handling. The following cases should be considered: 521 # 1. What if the flow doesn't exist in memcache? Or is corrupt? 522 # 2. What if the step2_exchange fails? 523 if flow: 524 credentials = flow.step2_exchange(self.request.params) 525 StorageByKeyName( 526 CredentialsModel, user.user_id(), 'credentials').put(credentials) 527 self.redirect(str(self.request.get('state'))) 528 else: 529 # TODO Add error handling here. 530 pass
531 532 533 application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
534 535 536 -def main():
537 run_wsgi_app(application)
538