1
2
3
4
5
6
7
8
9
10
11
12
13
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'
49 """The client_secrets.json file is malformed or missing required fields."""
50 pass
51
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
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
85
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
108 """App Engine datastore Property for Flow.
109
110 Utility property that allows easy storage and retreival of an
111 oauth2client.Flow"""
112
113
114 data_type = Flow
115
116
121
122
124 if value is None:
125 return None
126 return pickle.loads(value)
127
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
137
140 """App Engine datastore Property for Credentials.
141
142 Utility property that allows easy storage and retrieval of
143 oath2client.Credentials
144 """
145
146
147 data_type = Credentials
148
149
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
172
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
181
182 return value
183
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
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
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
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
260
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
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
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
340 if not user:
341 request_handler.redirect(users.create_login_url(
342 request_handler.request.uri))
343 return
344
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
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
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
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
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
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
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
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
497 """Handler for the redirect_uri of the OAuth 2.0 dance."""
498
499 @login_required
522
523
524 application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
528 run_wsgi_app(application)
529