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