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 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'
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
68 """The client_secrets.json file is malformed or missing required fields."""
69
72 """The XSRF token is invalid or expired."""
73
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
84 """Returns a random XSRF secret key.
85 """
86 return os.urandom(16).encode("hex")
87
109
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)
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'
138 )
139
140 @classmethod
144
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
167 """App Engine datastore Property for Flow.
168
169 Utility property that allows easy storage and retreival of an
170 oauth2client.Flow"""
171
172
173 data_type = Flow
174
175
180
181
183 if value is None:
184 return None
185 return pickle.loads(value)
186
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
196
199 """App Engine datastore Property for Credentials.
200
201 Utility property that allows easy storage and retrieval of
202 oath2client.Credentials
203 """
204
205
206 data_type = Credentials
207
208
218
219
231
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
240
241 return value
242
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
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
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
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
320
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
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
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
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
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
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
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
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)
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
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
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
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
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
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
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
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
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
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
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