blob: 8e05d8b358b8e0680b5630fab56ca192207b7047 [file] [log] [blame]
Joe Gregorio695fdc12011-01-16 16:46:55 -05001# 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
Joe Gregorio7c22ab22011-02-16 15:32:39 -050017Utilities for making it easier to use OAuth 2.0 on Google App Engine.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorio1daa71b2011-09-15 18:12:14 -040022import base64
Joe Gregorio77254c12012-08-27 14:13:22 -040023import cgi
Joe Gregorio432f17e2011-05-22 23:18:00 -040024import httplib2
Joe Gregorio1daa71b2011-09-15 18:12:14 -040025import logging
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040026import os
Joe Gregorio695fdc12011-01-16 16:46:55 -050027import pickle
JacobMoshenko8e905102011-06-20 09:53:10 -040028import time
JacobMoshenko8e905102011-06-20 09:53:10 -040029
Joe Gregoriod84d6b82012-02-28 14:53:00 -050030from google.appengine.api import app_identity
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040031from google.appengine.api import memcache
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040032from google.appengine.api import users
Joe Gregorio432f17e2011-05-22 23:18:00 -040033from google.appengine.ext import db
dhermes@google.com47154822012-11-26 10:44:09 -080034from google.appengine.ext import ndb
Joe Gregorio432f17e2011-05-22 23:18:00 -040035from google.appengine.ext import webapp
36from google.appengine.ext.webapp.util import login_required
37from google.appengine.ext.webapp.util import run_wsgi_app
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -080038from oauth2client import GOOGLE_AUTH_URI
39from oauth2client import GOOGLE_REVOKE_URI
40from oauth2client import GOOGLE_TOKEN_URI
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040041from oauth2client import clientsecrets
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040042from oauth2client import util
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040043from oauth2client import xsrfutil
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040044from oauth2client.anyjson import simplejson
45from oauth2client.client import AccessTokenRefreshError
46from oauth2client.client import AssertionCredentials
47from oauth2client.client import Credentials
48from oauth2client.client import Flow
49from oauth2client.client import OAuth2WebServerFlow
50from oauth2client.client import Storage
Joe Gregorioa19f3a72012-07-11 15:35:35 -040051
52logger = logging.getLogger(__name__)
53
Joe Gregorio432f17e2011-05-22 23:18:00 -040054OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050055
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040056XSRF_MEMCACHE_ID = 'xsrf_secret_key'
57
JacobMoshenko8e905102011-06-20 09:53:10 -040058
Joe Gregorio77254c12012-08-27 14:13:22 -040059def _safe_html(s):
60 """Escape text to make it safe to display.
61
62 Args:
63 s: string, The text to escape.
64
65 Returns:
66 The escaped text as a string.
67 """
68 return cgi.escape(s, quote=1).replace("'", ''')
69
70
Joe Gregoriof08a4982011-10-07 13:11:16 -040071class InvalidClientSecretsError(Exception):
72 """The client_secrets.json file is malformed or missing required fields."""
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040073
74
75class InvalidXsrfTokenError(Exception):
76 """The XSRF token is invalid or expired."""
77
78
79class SiteXsrfSecretKey(db.Model):
80 """Storage for the sites XSRF secret key.
81
82 There will only be one instance stored of this model, the one used for the
dhermes@google.com47154822012-11-26 10:44:09 -080083 site.
84 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040085 secret = db.StringProperty()
86
87
dhermes@google.com47154822012-11-26 10:44:09 -080088class SiteXsrfSecretKeyNDB(ndb.Model):
89 """NDB Model for storage for the sites XSRF secret key.
90
91 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
92 interchangeably. This simply provides an NDB model for interacting with the
93 same data the DB model interacts with.
94
95 There should only be one instance stored of this model, the one used for the
96 site.
97 """
98 secret = ndb.StringProperty()
99
100 @classmethod
101 def _get_kind(cls):
102 """Return the kind name for this class."""
103 return 'SiteXsrfSecretKey'
104
105
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400106def _generate_new_xsrf_secret_key():
107 """Returns a random XSRF secret key.
108 """
109 return os.urandom(16).encode("hex")
110
111
112def xsrf_secret_key():
113 """Return the secret key for use for XSRF protection.
114
115 If the Site entity does not have a secret key, this method will also create
116 one and persist it.
117
118 Returns:
119 The secret key.
120 """
121 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
122 if not secret:
123 # Load the one and only instance of SiteXsrfSecretKey.
124 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
125 if not model.secret:
126 model.secret = _generate_new_xsrf_secret_key()
127 model.put()
128 secret = model.secret
129 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
130
131 return str(secret)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400132
133
JacobMoshenko8e905102011-06-20 09:53:10 -0400134class AppAssertionCredentials(AssertionCredentials):
135 """Credentials object for App Engine Assertion Grants
136
137 This object will allow an App Engine application to identify itself to Google
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400138 and other OAuth 2.0 servers that can verify assertions. It can be used for the
139 purpose of accessing data stored under an account assigned to the App Engine
140 application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -0400141
142 This credential does not require a flow to instantiate because it represents
143 a two legged flow, and therefore has all of the required information to
144 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -0400145 """
146
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400147 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500148 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -0400149 """Constructor for AppAssertionCredentials
150
151 Args:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500152 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriofd08e432012-08-09 14:17:41 -0400153 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400154 """
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500155 self.scope = util.scopes_to_string(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400156
157 super(AppAssertionCredentials, self).__init__(
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400158 'ignored' # assertion_type is ignore in this subclass.
159 )
JacobMoshenko8e905102011-06-20 09:53:10 -0400160
Joe Gregorio562b7312011-09-15 09:06:38 -0400161 @classmethod
162 def from_json(cls, json):
163 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500164 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400165
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500166 def _refresh(self, http_request):
167 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400168
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500169 Since the underlying App Engine app_identity implementation does its own
170 caching we can skip all the storage hoops and just to a refresh using the
171 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400172
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500173 Args:
174 http_request: callable, a callable that matches the method signature of
175 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400176
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500177 Raises:
178 AccessTokenRefreshError: When the refresh fails.
179 """
180 try:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500181 scopes = self.scope.split()
182 (token, _) = app_identity.get_access_token(scopes)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500183 except app_identity.Error, e:
184 raise AccessTokenRefreshError(str(e))
185 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400186
187
Joe Gregorio695fdc12011-01-16 16:46:55 -0500188class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500189 """App Engine datastore Property for Flow.
190
dhermes@google.com47154822012-11-26 10:44:09 -0800191 Utility property that allows easy storage and retrieval of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500192 oauth2client.Flow"""
193
194 # Tell what the user type is.
195 data_type = Flow
196
197 # For writing to datastore.
198 def get_value_for_datastore(self, model_instance):
199 flow = super(FlowProperty,
200 self).get_value_for_datastore(model_instance)
201 return db.Blob(pickle.dumps(flow))
202
203 # For reading from datastore.
204 def make_value_from_datastore(self, value):
205 if value is None:
206 return None
207 return pickle.loads(value)
208
209 def validate(self, value):
210 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400211 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500212 'to a FlowThreeLegged instance (%s)' %
213 (self.name, value))
214 return super(FlowProperty, self).validate(value)
215
216 def empty(self, value):
217 return not value
218
219
dhermes@google.com47154822012-11-26 10:44:09 -0800220class FlowNDBProperty(ndb.PickleProperty):
221 """App Engine NDB datastore Property for Flow.
222
223 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
224 PickleProperty inherits from BlobProperty, the underlying representation of
225 the data in the datastore will be the same as in the DB case.
226
227 Utility property that allows easy storage and retrieval of an
228 oauth2client.Flow
229 """
230
231 def _validate(self, value):
232 """Validates a value as a proper Flow object.
233
234 Args:
235 value: A value to be set on the property.
236
237 Raises:
238 TypeError if the value is not an instance of Flow.
239 """
240 logger.info('validate: Got type %s', type(value))
241 if value is not None and not isinstance(value, Flow):
242 raise TypeError('Property %s must be convertible to a flow '
243 'instance; received: %s.' % (self._name, value))
244
245
Joe Gregorio695fdc12011-01-16 16:46:55 -0500246class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500247 """App Engine datastore Property for Credentials.
248
249 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500250 oath2client.Credentials
251 """
252
253 # Tell what the user type is.
254 data_type = Credentials
255
256 # For writing to datastore.
257 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400258 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500259 cred = super(CredentialsProperty,
260 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400261 if cred is None:
262 cred = ''
263 else:
264 cred = cred.to_json()
265 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500266
267 # For reading from datastore.
268 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400269 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500270 if value is None:
271 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400272 if len(value) == 0:
273 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400274 try:
275 credentials = Credentials.new_from_json(value)
276 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400277 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400278 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500279
280 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400281 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400282 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500283 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400284 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400285 'to a Credentials instance (%s)' %
286 (self.name, value))
287 #if value is not None and not isinstance(value, Credentials):
288 # return None
289 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500290
291
dhermes@google.com47154822012-11-26 10:44:09 -0800292# TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
293# and subclass mechanics to use new_from_dict, to_dict,
294# from_dict, etc.
295class CredentialsNDBProperty(ndb.BlobProperty):
296 """App Engine NDB datastore Property for Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500297
dhermes@google.com47154822012-11-26 10:44:09 -0800298 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
299 Since CredentialsProperty stores data as a blob and this inherits from
300 BlobProperty, the data in the datastore will be the same as in the DB case.
301
302 Utility property that allows easy storage and retrieval of Credentials and
303 subclasses.
304 """
305 def _validate(self, value):
306 """Validates a value as a proper credentials object.
307
308 Args:
309 value: A value to be set on the property.
310
311 Raises:
312 TypeError if the value is not an instance of Credentials.
313 """
314 logger.info('validate: Got type %s', type(value))
315 if value is not None and not isinstance(value, Credentials):
316 raise TypeError('Property %s must be convertible to a credentials '
317 'instance; received: %s.' % (self._name, value))
318
319 def _to_base_type(self, value):
320 """Converts our validated value to a JSON serialized string.
321
322 Args:
323 value: A value to be set in the datastore.
324
325 Returns:
326 A JSON serialized version of the credential, else '' if value is None.
327 """
328 if value is None:
329 return ''
330 else:
331 return value.to_json()
332
333 def _from_base_type(self, value):
334 """Converts our stored JSON string back to the desired type.
335
336 Args:
337 value: A value from the datastore to be converted to the desired type.
338
339 Returns:
340 A deserialized Credentials (or subclass) object, else None if the
341 value can't be parsed.
342 """
343 if not value:
344 return None
345 try:
346 # Uses the from_json method of the implied class of value
347 credentials = Credentials.new_from_json(value)
348 except ValueError:
349 credentials = None
350 return credentials
351
352
353class StorageByKeyName(Storage):
354 """Store and retrieve a credential to and from the App Engine datastore.
355
356 This Storage helper presumes the Credentials have been stored as a
357 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
358 that entities are stored by key_name.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500359 """
360
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400361 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400362 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500363 """Constructor for Storage.
364
365 Args:
dhermes@google.com47154822012-11-26 10:44:09 -0800366 model: db.Model or ndb.Model, model class
Joe Gregorio695fdc12011-01-16 16:46:55 -0500367 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400368 property_name: string, name of the property that is a CredentialsProperty
dhermes@google.com47154822012-11-26 10:44:09 -0800369 or CredentialsNDBProperty.
370 cache: memcache, a write-through cache to put in front of the datastore.
371 If the model you are using is an NDB model, using a cache will be
372 redundant since the model uses an instance cache and memcache for you.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500373 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500374 self._model = model
375 self._key_name = key_name
376 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400377 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500378
dhermes@google.com47154822012-11-26 10:44:09 -0800379 def _is_ndb(self):
380 """Determine whether the model of the instance is an NDB model.
381
382 Returns:
383 Boolean indicating whether or not the model is an NDB or DB model.
384 """
385 # issubclass will fail if one of the arguments is not a class, only need
386 # worry about new-style classes since ndb and db models are new-style
387 if isinstance(self._model, type):
388 if issubclass(self._model, ndb.Model):
389 return True
390 elif issubclass(self._model, db.Model):
391 return False
392
393 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
394
395 def _get_entity(self):
396 """Retrieve entity from datastore.
397
398 Uses a different model method for db or ndb models.
399
400 Returns:
401 Instance of the model corresponding to the current storage object
402 and stored using the key name of the storage object.
403 """
404 if self._is_ndb():
405 return self._model.get_by_id(self._key_name)
406 else:
407 return self._model.get_by_key_name(self._key_name)
408
409 def _delete_entity(self):
410 """Delete entity from datastore.
411
412 Attempts to delete using the key_name stored on the object, whether or not
413 the given key is in the datastore.
414 """
415 if self._is_ndb():
416 ndb.Key(self._model, self._key_name).delete()
417 else:
418 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
419 db.delete(entity_key)
420
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400421 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500422 """Retrieve Credential from datastore.
423
424 Returns:
425 oauth2client.Credentials
426 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400427 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400428 json = self._cache.get(self._key_name)
429 if json:
430 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500431
dhermes@google.com47154822012-11-26 10:44:09 -0800432 credentials = None
433 entity = self._get_entity()
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500434 if entity is not None:
dhermes@google.com47154822012-11-26 10:44:09 -0800435 credentials = getattr(entity, self._property_name)
436 if credentials and hasattr(credentials, 'set_store'):
437 credentials.set_store(self)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500438 if self._cache:
dhermes@google.com47154822012-11-26 10:44:09 -0800439 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400440
dhermes@google.com47154822012-11-26 10:44:09 -0800441 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500442
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400443 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500444 """Write a Credentials to the datastore.
445
446 Args:
447 credentials: Credentials, the credentials to store.
448 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500449 entity = self._model.get_or_insert(self._key_name)
450 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500451 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400452 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400453 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400454
Joe Gregorioec75dc12012-02-06 13:40:42 -0500455 def locked_delete(self):
456 """Delete Credential from datastore."""
457
458 if self._cache:
459 self._cache.delete(self._key_name)
460
dhermes@google.com47154822012-11-26 10:44:09 -0800461 self._delete_entity()
Joe Gregorioec75dc12012-02-06 13:40:42 -0500462
Joe Gregorio432f17e2011-05-22 23:18:00 -0400463
464class CredentialsModel(db.Model):
465 """Storage for OAuth 2.0 Credentials
466
467 Storage of the model is keyed by the user.user_id().
468 """
469 credentials = CredentialsProperty()
470
471
dhermes@google.com47154822012-11-26 10:44:09 -0800472class CredentialsNDBModel(ndb.Model):
473 """NDB Model for storage of OAuth 2.0 Credentials
474
475 Since this model uses the same kind as CredentialsModel and has a property
476 which can serialize and deserialize Credentials correctly, it can be used
477 interchangeably with a CredentialsModel to access, insert and delete the same
478 entities. This simply provides an NDB model for interacting with the
479 same data the DB model interacts with.
480
481 Storage of the model is keyed by the user.user_id().
482 """
483 credentials = CredentialsNDBProperty()
484
485 @classmethod
486 def _get_kind(cls):
487 """Return the kind name for this class."""
488 return 'CredentialsModel'
489
490
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400491def _build_state_value(request_handler, user):
492 """Composes the value for the 'state' parameter.
493
494 Packs the current request URI and an XSRF token into an opaque string that
495 can be passed to the authentication server via the 'state' parameter.
496
497 Args:
498 request_handler: webapp.RequestHandler, The request.
499 user: google.appengine.api.users.User, The current user.
500
501 Returns:
502 The state value as a string.
503 """
504 uri = request_handler.request.url
505 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
506 action_id=str(uri))
507 return uri + ':' + token
508
509
510def _parse_state_value(state, user):
511 """Parse the value of the 'state' parameter.
512
513 Parses the value and validates the XSRF token in the state parameter.
514
515 Args:
516 state: string, The value of the state parameter.
517 user: google.appengine.api.users.User, The current user.
518
519 Raises:
520 InvalidXsrfTokenError: if the XSRF token is invalid.
521
522 Returns:
523 The redirect URI.
524 """
525 uri, token = state.rsplit(':', 1)
526 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
527 action_id=uri):
528 raise InvalidXsrfTokenError()
529
530 return uri
531
532
Joe Gregorio432f17e2011-05-22 23:18:00 -0400533class OAuth2Decorator(object):
534 """Utility for making OAuth 2.0 easier.
535
536 Instantiate and then use with oauth_required or oauth_aware
537 as decorators on webapp.RequestHandler methods.
538
539 Example:
540
541 decorator = OAuth2Decorator(
542 client_id='837...ent.com',
543 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500544 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400545
546
547 class MainHandler(webapp.RequestHandler):
548
549 @decorator.oauth_required
550 def get(self):
551 http = decorator.http()
552 # http is authorized with the user's Credentials and can be used
553 # in API calls
554
555 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400556
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400557 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400558 def __init__(self, client_id, client_secret, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800559 auth_uri=GOOGLE_AUTH_URI,
560 token_uri=GOOGLE_TOKEN_URI,
561 revoke_uri=GOOGLE_REVOKE_URI,
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100562 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400563 message=None,
564 callback_path='/oauth2callback',
565 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400566
567 """Constructor for OAuth2Decorator
568
569 Args:
570 client_id: string, client identifier.
571 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500572 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400573 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400574 auth_uri: string, URI for authorization endpoint. For convenience
575 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
576 token_uri: string, URI for token endpoint. For convenience
577 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800578 revoke_uri: string, URI for revoke endpoint. For convenience
579 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100580 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400581 message: Message to display if there are problems with the OAuth 2.0
582 configuration. The message may contain HTML and will be presented on the
583 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400584 callback_path: string, The absolute path to use as the callback URI. Note
585 that this must match up with the URI given when registering the
586 application in the APIs Console.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500587 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
588 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400589 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400590 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400591 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400592 self._client_id = client_id
593 self._client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500594 self._scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400595 self._auth_uri = auth_uri
596 self._token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800597 self._revoke_uri = revoke_uri
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400598 self._user_agent = user_agent
599 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400600 self._message = message
601 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400602 self._callback_path = callback_path
Joe Gregoriof08a4982011-10-07 13:11:16 -0400603
604 def _display_error_message(self, request_handler):
605 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400606 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400607 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400608
609 def oauth_required(self, method):
610 """Decorator that starts the OAuth 2.0 dance.
611
612 Starts the OAuth dance for the logged in user if they haven't already
613 granted access for this application.
614
615 Args:
616 method: callable, to be decorated method of a webapp.RequestHandler
617 instance.
618 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400619
Joe Gregorio17774972012-03-01 11:11:59 -0500620 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400621 if self._in_error:
622 self._display_error_message(request_handler)
623 return
624
Joe Gregoriof427c532011-06-13 09:35:26 -0400625 user = users.get_current_user()
626 # Don't use @login_decorator as this could be used in a POST request.
627 if not user:
628 request_handler.redirect(users.create_login_url(
629 request_handler.request.uri))
630 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400631
632 self._create_flow(request_handler)
633
Joe Gregorio432f17e2011-05-22 23:18:00 -0400634 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400635 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400636 self.credentials = StorageByKeyName(
637 CredentialsModel, user.user_id(), 'credentials').get()
638
639 if not self.has_credentials():
640 return request_handler.redirect(self.authorize_url())
641 try:
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400642 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400643 except AccessTokenRefreshError:
644 return request_handler.redirect(self.authorize_url())
645
646 return check_oauth
647
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400648 def _create_flow(self, request_handler):
649 """Create the Flow object.
650
651 The Flow is calculated lazily since we don't know where this app is
652 running until it receives a request, at which point redirect_uri can be
653 calculated and then the Flow object can be constructed.
654
655 Args:
656 request_handler: webapp.RequestHandler, the request handler.
657 """
658 if self.flow is None:
659 redirect_uri = request_handler.request.relative_url(
660 self._callback_path) # Usually /oauth2callback
661 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
662 self._scope, redirect_uri=redirect_uri,
663 user_agent=self._user_agent,
664 auth_uri=self._auth_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800665 token_uri=self._token_uri,
666 revoke_uri=self._revoke_uri,
667 **self._kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400668
Joe Gregorio432f17e2011-05-22 23:18:00 -0400669 def oauth_aware(self, method):
670 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
671
672 Does all the setup for the OAuth dance, but doesn't initiate it.
673 This decorator is useful if you want to create a page that knows
674 whether or not the user has granted access to this application.
675 From within a method decorated with @oauth_aware the has_credentials()
676 and authorize_url() methods can be called.
677
678 Args:
679 method: callable, to be decorated method of a webapp.RequestHandler
680 instance.
681 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400682
Joe Gregorio17774972012-03-01 11:11:59 -0500683 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400684 if self._in_error:
685 self._display_error_message(request_handler)
686 return
687
Joe Gregoriof427c532011-06-13 09:35:26 -0400688 user = users.get_current_user()
689 # Don't use @login_decorator as this could be used in a POST request.
690 if not user:
691 request_handler.redirect(users.create_login_url(
692 request_handler.request.uri))
693 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400694
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400695 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400696
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400697 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400698 self.credentials = StorageByKeyName(
699 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400700 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400701 return setup_oauth
702
703 def has_credentials(self):
704 """True if for the logged in user there are valid access Credentials.
705
706 Must only be called from with a webapp.RequestHandler subclassed method
707 that had been decorated with either @oauth_required or @oauth_aware.
708 """
709 return self.credentials is not None and not self.credentials.invalid
710
711 def authorize_url(self):
712 """Returns the URL to start the OAuth dance.
713
714 Must only be called from with a webapp.RequestHandler subclassed method
715 that had been decorated with either @oauth_required or @oauth_aware.
716 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400717 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500718 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400719
720 def http(self):
721 """Returns an authorized http instance.
722
723 Must only be called from within an @oauth_required decorated method, or
724 from within an @oauth_aware decorated method where has_credentials()
725 returns True.
726 """
727 return self.credentials.authorize(httplib2.Http())
728
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400729 @property
730 def callback_path(self):
731 """The absolute path where the callback will occur.
732
733 Note this is the absolute path, not the absolute URI, that will be
734 calculated by the decorator at runtime. See callback_handler() for how this
735 should be used.
736
737 Returns:
738 The callback path as a string.
739 """
740 return self._callback_path
741
742
743 def callback_handler(self):
744 """RequestHandler for the OAuth 2.0 redirect callback.
745
746 Usage:
747 app = webapp.WSGIApplication([
748 ('/index', MyIndexHandler),
749 ...,
750 (decorator.callback_path, decorator.callback_handler())
751 ])
752
753 Returns:
754 A webapp.RequestHandler that handles the redirect back from the
755 server during the OAuth 2.0 dance.
756 """
757 decorator = self
758
759 class OAuth2Handler(webapp.RequestHandler):
760 """Handler for the redirect_uri of the OAuth 2.0 dance."""
761
762 @login_required
763 def get(self):
764 error = self.request.get('error')
765 if error:
766 errormsg = self.request.get('error_description', error)
767 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400768 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400769 else:
770 user = users.get_current_user()
771 decorator._create_flow(self)
772 credentials = decorator.flow.step2_exchange(self.request.params)
773 StorageByKeyName(
774 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400775 redirect_uri = _parse_state_value(str(self.request.get('state')),
776 user)
777 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400778
779 return OAuth2Handler
780
781 def callback_application(self):
782 """WSGI application for handling the OAuth 2.0 redirect callback.
783
784 If you need finer grained control use `callback_handler` which returns just
785 the webapp.RequestHandler.
786
787 Returns:
788 A webapp.WSGIApplication that handles the redirect back from the
789 server during the OAuth 2.0 dance.
790 """
791 return webapp.WSGIApplication([
792 (self.callback_path, self.callback_handler())
793 ])
794
Joe Gregorio432f17e2011-05-22 23:18:00 -0400795
Joe Gregoriof08a4982011-10-07 13:11:16 -0400796class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
797 """An OAuth2Decorator that builds from a clientsecrets file.
798
799 Uses a clientsecrets file as the source for all the information when
800 constructing an OAuth2Decorator.
801
802 Example:
803
804 decorator = OAuth2DecoratorFromClientSecrets(
805 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500806 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400807
808
809 class MainHandler(webapp.RequestHandler):
810
811 @decorator.oauth_required
812 def get(self):
813 http = decorator.http()
814 # http is authorized with the user's Credentials and can be used
815 # in API calls
816 """
817
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400818 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400819 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400820 """Constructor
821
822 Args:
823 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500824 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400825 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400826 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400827 clientsecrets file is missing or invalid. The message may contain HTML
828 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400829 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400830 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400831 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400832 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400833 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
834 if client_type not in [
835 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
836 raise InvalidClientSecretsError(
837 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800838 constructor_kwargs = {
839 'auth_uri': client_info['auth_uri'],
840 'token_uri': client_info['token_uri'],
841 'message': message,
842 }
843 revoke_uri = client_info.get('revoke_uri')
844 if revoke_uri is not None:
845 constructor_kwargs['revoke_uri'] = revoke_uri
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400846 super(OAuth2DecoratorFromClientSecrets, self).__init__(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800847 client_info['client_id'], client_info['client_secret'],
848 scope, **constructor_kwargs)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400849 if message is not None:
850 self._message = message
851 else:
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800852 self._message = 'Please configure your application for OAuth 2.0.'
Joe Gregoriof08a4982011-10-07 13:11:16 -0400853
854
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400855@util.positional(2)
856def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400857 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400858 """Creates an OAuth2Decorator populated from a clientsecrets file.
859
860 Args:
861 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400862 scope: string or list of strings, scope(s) of the credentials being
863 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400864 message: string, A friendly string to display to the user if the
865 clientsecrets file is missing or invalid. The message may contain HTML and
866 will be presented on the web interface for any method that uses the
867 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400868 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400869 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400870
871 Returns: An OAuth2Decorator
872
873 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400874 return OAuth2DecoratorFromClientSecrets(filename, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800875 message=message, cache=cache)