blob: 5bf562632d143d343a9c43fc1005d95f36178b28 [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
34from google.appengine.ext import webapp
35from google.appengine.ext.webapp.util import login_required
36from google.appengine.ext.webapp.util import run_wsgi_app
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -080037from oauth2client import GOOGLE_AUTH_URI
38from oauth2client import GOOGLE_REVOKE_URI
39from oauth2client import GOOGLE_TOKEN_URI
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040040from oauth2client import clientsecrets
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040041from oauth2client import util
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040042from oauth2client import xsrfutil
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040043from oauth2client.anyjson import simplejson
44from oauth2client.client import AccessTokenRefreshError
45from oauth2client.client import AssertionCredentials
46from oauth2client.client import Credentials
47from oauth2client.client import Flow
48from oauth2client.client import OAuth2WebServerFlow
49from oauth2client.client import Storage
Joe Gregorioa19f3a72012-07-11 15:35:35 -040050
Joe Gregorio78787b62013-02-08 15:36:21 -050051# TODO(dhermes): Resolve import issue.
52# This is a temporary fix for a Google internal issue.
53try:
54 from google.appengine.ext import ndb
55except ImportError:
56 ndb = None
57
Joe Gregoriocda87522013-02-22 16:22:48 -050058
Joe Gregorioa19f3a72012-07-11 15:35:35 -040059logger = logging.getLogger(__name__)
60
Joe Gregorio432f17e2011-05-22 23:18:00 -040061OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050062
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040063XSRF_MEMCACHE_ID = 'xsrf_secret_key'
64
JacobMoshenko8e905102011-06-20 09:53:10 -040065
Joe Gregorio77254c12012-08-27 14:13:22 -040066def _safe_html(s):
67 """Escape text to make it safe to display.
68
69 Args:
70 s: string, The text to escape.
71
72 Returns:
73 The escaped text as a string.
74 """
75 return cgi.escape(s, quote=1).replace("'", ''')
76
77
Joe Gregoriof08a4982011-10-07 13:11:16 -040078class InvalidClientSecretsError(Exception):
79 """The client_secrets.json file is malformed or missing required fields."""
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040080
81
82class InvalidXsrfTokenError(Exception):
83 """The XSRF token is invalid or expired."""
84
85
86class SiteXsrfSecretKey(db.Model):
87 """Storage for the sites XSRF secret key.
88
89 There will only be one instance stored of this model, the one used for the
dhermes@google.com47154822012-11-26 10:44:09 -080090 site.
91 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040092 secret = db.StringProperty()
93
Joe Gregorio78787b62013-02-08 15:36:21 -050094if ndb is not None:
95 class SiteXsrfSecretKeyNDB(ndb.Model):
96 """NDB Model for storage for the sites XSRF secret key.
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040097
Joe Gregorio78787b62013-02-08 15:36:21 -050098 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
99 interchangeably. This simply provides an NDB model for interacting with the
100 same data the DB model interacts with.
dhermes@google.com47154822012-11-26 10:44:09 -0800101
Joe Gregorio78787b62013-02-08 15:36:21 -0500102 There should only be one instance stored of this model, the one used for the
103 site.
104 """
105 secret = ndb.StringProperty()
dhermes@google.com47154822012-11-26 10:44:09 -0800106
Joe Gregorio78787b62013-02-08 15:36:21 -0500107 @classmethod
108 def _get_kind(cls):
109 """Return the kind name for this class."""
110 return 'SiteXsrfSecretKey'
dhermes@google.com47154822012-11-26 10:44:09 -0800111
112
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400113def _generate_new_xsrf_secret_key():
114 """Returns a random XSRF secret key.
115 """
116 return os.urandom(16).encode("hex")
117
118
119def xsrf_secret_key():
120 """Return the secret key for use for XSRF protection.
121
122 If the Site entity does not have a secret key, this method will also create
123 one and persist it.
124
125 Returns:
126 The secret key.
127 """
128 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
129 if not secret:
130 # Load the one and only instance of SiteXsrfSecretKey.
131 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
132 if not model.secret:
133 model.secret = _generate_new_xsrf_secret_key()
134 model.put()
135 secret = model.secret
136 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
137
138 return str(secret)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400139
140
JacobMoshenko8e905102011-06-20 09:53:10 -0400141class AppAssertionCredentials(AssertionCredentials):
142 """Credentials object for App Engine Assertion Grants
143
144 This object will allow an App Engine application to identify itself to Google
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400145 and other OAuth 2.0 servers that can verify assertions. It can be used for the
146 purpose of accessing data stored under an account assigned to the App Engine
147 application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -0400148
149 This credential does not require a flow to instantiate because it represents
150 a two legged flow, and therefore has all of the required information to
151 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -0400152 """
153
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400154 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500155 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -0400156 """Constructor for AppAssertionCredentials
157
158 Args:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500159 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriofd08e432012-08-09 14:17:41 -0400160 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400161 """
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500162 self.scope = util.scopes_to_string(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400163
dhermes@google.com2cc09382013-02-11 08:42:18 -0800164 # Assertion type is no longer used, but still in the parent class signature.
165 super(AppAssertionCredentials, self).__init__(None)
JacobMoshenko8e905102011-06-20 09:53:10 -0400166
Joe Gregorio562b7312011-09-15 09:06:38 -0400167 @classmethod
168 def from_json(cls, json):
169 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500170 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400171
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500172 def _refresh(self, http_request):
173 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400174
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500175 Since the underlying App Engine app_identity implementation does its own
176 caching we can skip all the storage hoops and just to a refresh using the
177 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400178
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500179 Args:
180 http_request: callable, a callable that matches the method signature of
181 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400182
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500183 Raises:
184 AccessTokenRefreshError: When the refresh fails.
185 """
186 try:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500187 scopes = self.scope.split()
188 (token, _) = app_identity.get_access_token(scopes)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500189 except app_identity.Error, e:
190 raise AccessTokenRefreshError(str(e))
191 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400192
193
Joe Gregorio695fdc12011-01-16 16:46:55 -0500194class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500195 """App Engine datastore Property for Flow.
196
dhermes@google.com47154822012-11-26 10:44:09 -0800197 Utility property that allows easy storage and retrieval of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500198 oauth2client.Flow"""
199
200 # Tell what the user type is.
201 data_type = Flow
202
203 # For writing to datastore.
204 def get_value_for_datastore(self, model_instance):
205 flow = super(FlowProperty,
206 self).get_value_for_datastore(model_instance)
207 return db.Blob(pickle.dumps(flow))
208
209 # For reading from datastore.
210 def make_value_from_datastore(self, value):
211 if value is None:
212 return None
213 return pickle.loads(value)
214
215 def validate(self, value):
216 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400217 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500218 'to a FlowThreeLegged instance (%s)' %
219 (self.name, value))
220 return super(FlowProperty, self).validate(value)
221
222 def empty(self, value):
223 return not value
224
225
Joe Gregorio78787b62013-02-08 15:36:21 -0500226if ndb is not None:
227 class FlowNDBProperty(ndb.PickleProperty):
228 """App Engine NDB datastore Property for Flow.
dhermes@google.com47154822012-11-26 10:44:09 -0800229
Joe Gregorio78787b62013-02-08 15:36:21 -0500230 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
231 PickleProperty inherits from BlobProperty, the underlying representation of
232 the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800233
Joe Gregorio78787b62013-02-08 15:36:21 -0500234 Utility property that allows easy storage and retrieval of an
235 oauth2client.Flow
dhermes@google.com47154822012-11-26 10:44:09 -0800236 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500237
238 def _validate(self, value):
239 """Validates a value as a proper Flow object.
240
241 Args:
242 value: A value to be set on the property.
243
244 Raises:
245 TypeError if the value is not an instance of Flow.
246 """
247 logger.info('validate: Got type %s', type(value))
248 if value is not None and not isinstance(value, Flow):
249 raise TypeError('Property %s must be convertible to a flow '
250 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800251
252
Joe Gregorio695fdc12011-01-16 16:46:55 -0500253class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500254 """App Engine datastore Property for Credentials.
255
256 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500257 oath2client.Credentials
258 """
259
260 # Tell what the user type is.
261 data_type = Credentials
262
263 # For writing to datastore.
264 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400265 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500266 cred = super(CredentialsProperty,
267 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400268 if cred is None:
269 cred = ''
270 else:
271 cred = cred.to_json()
272 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500273
274 # For reading from datastore.
275 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400276 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500277 if value is None:
278 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400279 if len(value) == 0:
280 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400281 try:
282 credentials = Credentials.new_from_json(value)
283 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400284 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400285 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500286
287 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400288 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400289 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500290 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400291 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400292 'to a Credentials instance (%s)' %
293 (self.name, value))
294 #if value is not None and not isinstance(value, Credentials):
295 # return None
296 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500297
298
Joe Gregorio78787b62013-02-08 15:36:21 -0500299if ndb is not None:
300 # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
301 # and subclass mechanics to use new_from_dict, to_dict,
302 # from_dict, etc.
303 class CredentialsNDBProperty(ndb.BlobProperty):
304 """App Engine NDB datastore Property for Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500305
Joe Gregorio78787b62013-02-08 15:36:21 -0500306 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
307 Since CredentialsProperty stores data as a blob and this inherits from
308 BlobProperty, the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800309
Joe Gregorio78787b62013-02-08 15:36:21 -0500310 Utility property that allows easy storage and retrieval of Credentials and
311 subclasses.
dhermes@google.com47154822012-11-26 10:44:09 -0800312 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500313 def _validate(self, value):
314 """Validates a value as a proper credentials object.
dhermes@google.com47154822012-11-26 10:44:09 -0800315
Joe Gregorio78787b62013-02-08 15:36:21 -0500316 Args:
317 value: A value to be set on the property.
dhermes@google.com47154822012-11-26 10:44:09 -0800318
Joe Gregorio78787b62013-02-08 15:36:21 -0500319 Raises:
320 TypeError if the value is not an instance of Credentials.
321 """
322 logger.info('validate: Got type %s', type(value))
323 if value is not None and not isinstance(value, Credentials):
324 raise TypeError('Property %s must be convertible to a credentials '
325 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800326
Joe Gregorio78787b62013-02-08 15:36:21 -0500327 def _to_base_type(self, value):
328 """Converts our validated value to a JSON serialized string.
dhermes@google.com47154822012-11-26 10:44:09 -0800329
Joe Gregorio78787b62013-02-08 15:36:21 -0500330 Args:
331 value: A value to be set in the datastore.
dhermes@google.com47154822012-11-26 10:44:09 -0800332
Joe Gregorio78787b62013-02-08 15:36:21 -0500333 Returns:
334 A JSON serialized version of the credential, else '' if value is None.
335 """
336 if value is None:
337 return ''
338 else:
339 return value.to_json()
dhermes@google.com47154822012-11-26 10:44:09 -0800340
Joe Gregorio78787b62013-02-08 15:36:21 -0500341 def _from_base_type(self, value):
342 """Converts our stored JSON string back to the desired type.
343
344 Args:
345 value: A value from the datastore to be converted to the desired type.
346
347 Returns:
348 A deserialized Credentials (or subclass) object, else None if the
349 value can't be parsed.
350 """
351 if not value:
352 return None
353 try:
354 # Uses the from_json method of the implied class of value
355 credentials = Credentials.new_from_json(value)
356 except ValueError:
357 credentials = None
358 return credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800359
360
361class StorageByKeyName(Storage):
362 """Store and retrieve a credential to and from the App Engine datastore.
363
364 This Storage helper presumes the Credentials have been stored as a
365 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
366 that entities are stored by key_name.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500367 """
368
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400369 @util.positional(4)
Daniel Hermes58341a02013-04-05 09:58:16 -0700370 def __init__(self, model, key_name, property_name, cache=None, user=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500371 """Constructor for Storage.
372
373 Args:
dhermes@google.com47154822012-11-26 10:44:09 -0800374 model: db.Model or ndb.Model, model class
Joe Gregorio695fdc12011-01-16 16:46:55 -0500375 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400376 property_name: string, name of the property that is a CredentialsProperty
dhermes@google.com47154822012-11-26 10:44:09 -0800377 or CredentialsNDBProperty.
378 cache: memcache, a write-through cache to put in front of the datastore.
379 If the model you are using is an NDB model, using a cache will be
380 redundant since the model uses an instance cache and memcache for you.
Daniel Hermes58341a02013-04-05 09:58:16 -0700381 user: users.User object, optional. Can be used to grab user ID as a
382 key_name if no key name is specified.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500383 """
Daniel Hermes58341a02013-04-05 09:58:16 -0700384 if key_name is None:
385 if user is None:
386 raise ValueError('StorageByKeyName called with no key name or user.')
387 key_name = user.user_id()
388
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500389 self._model = model
390 self._key_name = key_name
391 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400392 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500393
dhermes@google.com47154822012-11-26 10:44:09 -0800394 def _is_ndb(self):
395 """Determine whether the model of the instance is an NDB model.
396
397 Returns:
398 Boolean indicating whether or not the model is an NDB or DB model.
399 """
400 # issubclass will fail if one of the arguments is not a class, only need
401 # worry about new-style classes since ndb and db models are new-style
402 if isinstance(self._model, type):
Joe Gregorio78787b62013-02-08 15:36:21 -0500403 if ndb is not None and issubclass(self._model, ndb.Model):
dhermes@google.com47154822012-11-26 10:44:09 -0800404 return True
405 elif issubclass(self._model, db.Model):
406 return False
407
408 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
409
410 def _get_entity(self):
411 """Retrieve entity from datastore.
412
413 Uses a different model method for db or ndb models.
414
415 Returns:
416 Instance of the model corresponding to the current storage object
417 and stored using the key name of the storage object.
418 """
419 if self._is_ndb():
420 return self._model.get_by_id(self._key_name)
421 else:
422 return self._model.get_by_key_name(self._key_name)
423
424 def _delete_entity(self):
425 """Delete entity from datastore.
426
427 Attempts to delete using the key_name stored on the object, whether or not
428 the given key is in the datastore.
429 """
430 if self._is_ndb():
431 ndb.Key(self._model, self._key_name).delete()
432 else:
433 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
434 db.delete(entity_key)
435
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400436 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500437 """Retrieve Credential from datastore.
438
439 Returns:
440 oauth2client.Credentials
441 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400442 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400443 json = self._cache.get(self._key_name)
444 if json:
445 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500446
dhermes@google.com47154822012-11-26 10:44:09 -0800447 credentials = None
448 entity = self._get_entity()
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500449 if entity is not None:
dhermes@google.com47154822012-11-26 10:44:09 -0800450 credentials = getattr(entity, self._property_name)
451 if credentials and hasattr(credentials, 'set_store'):
452 credentials.set_store(self)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500453 if self._cache:
dhermes@google.com47154822012-11-26 10:44:09 -0800454 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400455
dhermes@google.com47154822012-11-26 10:44:09 -0800456 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500457
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400458 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500459 """Write a Credentials to the datastore.
460
461 Args:
462 credentials: Credentials, the credentials to store.
463 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500464 entity = self._model.get_or_insert(self._key_name)
465 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500466 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400467 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400468 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400469
Joe Gregorioec75dc12012-02-06 13:40:42 -0500470 def locked_delete(self):
471 """Delete Credential from datastore."""
472
473 if self._cache:
474 self._cache.delete(self._key_name)
475
dhermes@google.com47154822012-11-26 10:44:09 -0800476 self._delete_entity()
Joe Gregorioec75dc12012-02-06 13:40:42 -0500477
Joe Gregorio432f17e2011-05-22 23:18:00 -0400478
479class CredentialsModel(db.Model):
480 """Storage for OAuth 2.0 Credentials
481
482 Storage of the model is keyed by the user.user_id().
483 """
484 credentials = CredentialsProperty()
485
486
Joe Gregorio78787b62013-02-08 15:36:21 -0500487if ndb is not None:
488 class CredentialsNDBModel(ndb.Model):
489 """NDB Model for storage of OAuth 2.0 Credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800490
Joe Gregorio78787b62013-02-08 15:36:21 -0500491 Since this model uses the same kind as CredentialsModel and has a property
492 which can serialize and deserialize Credentials correctly, it can be used
493 interchangeably with a CredentialsModel to access, insert and delete the
494 same entities. This simply provides an NDB model for interacting with the
495 same data the DB model interacts with.
dhermes@google.com47154822012-11-26 10:44:09 -0800496
Joe Gregorio78787b62013-02-08 15:36:21 -0500497 Storage of the model is keyed by the user.user_id().
498 """
499 credentials = CredentialsNDBProperty()
dhermes@google.com47154822012-11-26 10:44:09 -0800500
Joe Gregorio78787b62013-02-08 15:36:21 -0500501 @classmethod
502 def _get_kind(cls):
503 """Return the kind name for this class."""
504 return 'CredentialsModel'
dhermes@google.com47154822012-11-26 10:44:09 -0800505
506
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400507def _build_state_value(request_handler, user):
508 """Composes the value for the 'state' parameter.
509
510 Packs the current request URI and an XSRF token into an opaque string that
511 can be passed to the authentication server via the 'state' parameter.
512
513 Args:
514 request_handler: webapp.RequestHandler, The request.
515 user: google.appengine.api.users.User, The current user.
516
517 Returns:
518 The state value as a string.
519 """
520 uri = request_handler.request.url
521 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
522 action_id=str(uri))
523 return uri + ':' + token
524
525
526def _parse_state_value(state, user):
527 """Parse the value of the 'state' parameter.
528
529 Parses the value and validates the XSRF token in the state parameter.
530
531 Args:
532 state: string, The value of the state parameter.
533 user: google.appengine.api.users.User, The current user.
534
535 Raises:
536 InvalidXsrfTokenError: if the XSRF token is invalid.
537
538 Returns:
539 The redirect URI.
540 """
541 uri, token = state.rsplit(':', 1)
542 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
543 action_id=uri):
544 raise InvalidXsrfTokenError()
545
546 return uri
547
548
Joe Gregorio432f17e2011-05-22 23:18:00 -0400549class OAuth2Decorator(object):
550 """Utility for making OAuth 2.0 easier.
551
552 Instantiate and then use with oauth_required or oauth_aware
553 as decorators on webapp.RequestHandler methods.
554
555 Example:
556
557 decorator = OAuth2Decorator(
558 client_id='837...ent.com',
559 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500560 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400561
562
563 class MainHandler(webapp.RequestHandler):
564
565 @decorator.oauth_required
566 def get(self):
567 http = decorator.http()
568 # http is authorized with the user's Credentials and can be used
569 # in API calls
570
571 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400572
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400573 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400574 def __init__(self, client_id, client_secret, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800575 auth_uri=GOOGLE_AUTH_URI,
576 token_uri=GOOGLE_TOKEN_URI,
577 revoke_uri=GOOGLE_REVOKE_URI,
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100578 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400579 message=None,
580 callback_path='/oauth2callback',
Joe Gregoriocda87522013-02-22 16:22:48 -0500581 token_response_param=None,
Daniel Hermes58341a02013-04-05 09:58:16 -0700582 _storage_class=StorageByKeyName,
583 _credentials_class=CredentialsModel,
584 _credentials_property_name='credentials',
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400585 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400586
587 """Constructor for OAuth2Decorator
588
589 Args:
590 client_id: string, client identifier.
591 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500592 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400593 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400594 auth_uri: string, URI for authorization endpoint. For convenience
595 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
596 token_uri: string, URI for token endpoint. For convenience
597 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800598 revoke_uri: string, URI for revoke endpoint. For convenience
599 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100600 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400601 message: Message to display if there are problems with the OAuth 2.0
602 configuration. The message may contain HTML and will be presented on the
603 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400604 callback_path: string, The absolute path to use as the callback URI. Note
605 that this must match up with the URI given when registering the
606 application in the APIs Console.
Joe Gregoriocda87522013-02-22 16:22:48 -0500607 token_response_param: string. If provided, the full JSON response
608 to the access token request will be encoded and included in this query
609 parameter in the callback URI. This is useful with providers (e.g.
610 wordpress.com) that include extra fields that the client may want.
Daniel Hermes58341a02013-04-05 09:58:16 -0700611 _storage_class: "Protected" keyword argument not typically provided to
612 this constructor. A storage class to aid in storing a Credentials object
613 for a user in the datastore. Defaults to StorageByKeyName.
614 _credentials_class: "Protected" keyword argument not typically provided to
615 this constructor. A db or ndb Model class to hold credentials. Defaults
616 to CredentialsModel.
617 _credentials_property_name: "Protected" keyword argument not typically
618 provided to this constructor. A string indicating the name of the field
619 on the _credentials_class where a Credentials object will be stored.
620 Defaults to 'credentials'.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500621 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
622 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400623 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400624 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400625 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400626 self._client_id = client_id
627 self._client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500628 self._scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400629 self._auth_uri = auth_uri
630 self._token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800631 self._revoke_uri = revoke_uri
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400632 self._user_agent = user_agent
633 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400634 self._message = message
635 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400636 self._callback_path = callback_path
Joe Gregoriocda87522013-02-22 16:22:48 -0500637 self._token_response_param = token_response_param
Daniel Hermes58341a02013-04-05 09:58:16 -0700638 self._storage_class = _storage_class
639 self._credentials_class = _credentials_class
640 self._credentials_property_name = _credentials_property_name
Joe Gregoriof08a4982011-10-07 13:11:16 -0400641
642 def _display_error_message(self, request_handler):
643 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400644 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400645 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400646
647 def oauth_required(self, method):
648 """Decorator that starts the OAuth 2.0 dance.
649
650 Starts the OAuth dance for the logged in user if they haven't already
651 granted access for this application.
652
653 Args:
654 method: callable, to be decorated method of a webapp.RequestHandler
655 instance.
656 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400657
Joe Gregorio17774972012-03-01 11:11:59 -0500658 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400659 if self._in_error:
660 self._display_error_message(request_handler)
661 return
662
Joe Gregoriof427c532011-06-13 09:35:26 -0400663 user = users.get_current_user()
664 # Don't use @login_decorator as this could be used in a POST request.
665 if not user:
666 request_handler.redirect(users.create_login_url(
667 request_handler.request.uri))
668 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400669
670 self._create_flow(request_handler)
671
Joe Gregorio432f17e2011-05-22 23:18:00 -0400672 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400673 self.flow.params['state'] = _build_state_value(request_handler, user)
Daniel Hermes58341a02013-04-05 09:58:16 -0700674 self.credentials = self._storage_class(
675 self._credentials_class, None,
676 self._credentials_property_name, user=user).get()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400677
678 if not self.has_credentials():
679 return request_handler.redirect(self.authorize_url())
680 try:
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400681 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400682 except AccessTokenRefreshError:
683 return request_handler.redirect(self.authorize_url())
684
685 return check_oauth
686
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400687 def _create_flow(self, request_handler):
688 """Create the Flow object.
689
690 The Flow is calculated lazily since we don't know where this app is
691 running until it receives a request, at which point redirect_uri can be
692 calculated and then the Flow object can be constructed.
693
694 Args:
695 request_handler: webapp.RequestHandler, the request handler.
696 """
697 if self.flow is None:
698 redirect_uri = request_handler.request.relative_url(
699 self._callback_path) # Usually /oauth2callback
700 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
701 self._scope, redirect_uri=redirect_uri,
702 user_agent=self._user_agent,
703 auth_uri=self._auth_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800704 token_uri=self._token_uri,
705 revoke_uri=self._revoke_uri,
706 **self._kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400707
Joe Gregorio432f17e2011-05-22 23:18:00 -0400708 def oauth_aware(self, method):
709 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
710
711 Does all the setup for the OAuth dance, but doesn't initiate it.
712 This decorator is useful if you want to create a page that knows
713 whether or not the user has granted access to this application.
714 From within a method decorated with @oauth_aware the has_credentials()
715 and authorize_url() methods can be called.
716
717 Args:
718 method: callable, to be decorated method of a webapp.RequestHandler
719 instance.
720 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400721
Joe Gregorio17774972012-03-01 11:11:59 -0500722 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400723 if self._in_error:
724 self._display_error_message(request_handler)
725 return
726
Joe Gregoriof427c532011-06-13 09:35:26 -0400727 user = users.get_current_user()
728 # Don't use @login_decorator as this could be used in a POST request.
729 if not user:
730 request_handler.redirect(users.create_login_url(
731 request_handler.request.uri))
732 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400733
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400734 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400735
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400736 self.flow.params['state'] = _build_state_value(request_handler, user)
Daniel Hermes58341a02013-04-05 09:58:16 -0700737 self.credentials = self._storage_class(
738 self._credentials_class, None,
739 self._credentials_property_name, user=user).get()
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400740 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400741 return setup_oauth
742
743 def has_credentials(self):
744 """True if for the logged in user there are valid access Credentials.
745
746 Must only be called from with a webapp.RequestHandler subclassed method
747 that had been decorated with either @oauth_required or @oauth_aware.
748 """
749 return self.credentials is not None and not self.credentials.invalid
750
751 def authorize_url(self):
752 """Returns the URL to start the OAuth dance.
753
754 Must only be called from with a webapp.RequestHandler subclassed method
755 that had been decorated with either @oauth_required or @oauth_aware.
756 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400757 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500758 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400759
760 def http(self):
761 """Returns an authorized http instance.
762
763 Must only be called from within an @oauth_required decorated method, or
764 from within an @oauth_aware decorated method where has_credentials()
765 returns True.
766 """
767 return self.credentials.authorize(httplib2.Http())
768
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400769 @property
770 def callback_path(self):
771 """The absolute path where the callback will occur.
772
773 Note this is the absolute path, not the absolute URI, that will be
774 calculated by the decorator at runtime. See callback_handler() for how this
775 should be used.
776
777 Returns:
778 The callback path as a string.
779 """
780 return self._callback_path
781
782
783 def callback_handler(self):
784 """RequestHandler for the OAuth 2.0 redirect callback.
785
786 Usage:
787 app = webapp.WSGIApplication([
788 ('/index', MyIndexHandler),
789 ...,
790 (decorator.callback_path, decorator.callback_handler())
791 ])
792
793 Returns:
794 A webapp.RequestHandler that handles the redirect back from the
795 server during the OAuth 2.0 dance.
796 """
797 decorator = self
798
799 class OAuth2Handler(webapp.RequestHandler):
800 """Handler for the redirect_uri of the OAuth 2.0 dance."""
801
802 @login_required
803 def get(self):
804 error = self.request.get('error')
805 if error:
806 errormsg = self.request.get('error_description', error)
807 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400808 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400809 else:
810 user = users.get_current_user()
811 decorator._create_flow(self)
812 credentials = decorator.flow.step2_exchange(self.request.params)
Daniel Hermes58341a02013-04-05 09:58:16 -0700813 decorator._storage_class(
814 decorator._credentials_class, None,
815 decorator._credentials_property_name, user=user).put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400816 redirect_uri = _parse_state_value(str(self.request.get('state')),
817 user)
Joe Gregoriocda87522013-02-22 16:22:48 -0500818
819 if decorator._token_response_param and credentials.token_response:
820 resp_json = simplejson.dumps(credentials.token_response)
Joe Gregorio10244032013-03-06 09:48:04 -0500821 redirect_uri = util._add_query_parameter(
Daniel Hermesf7b648f2013-03-06 09:38:53 -0800822 redirect_uri, decorator._token_response_param, resp_json)
Joe Gregoriocda87522013-02-22 16:22:48 -0500823
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400824 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400825
826 return OAuth2Handler
827
828 def callback_application(self):
829 """WSGI application for handling the OAuth 2.0 redirect callback.
830
831 If you need finer grained control use `callback_handler` which returns just
832 the webapp.RequestHandler.
833
834 Returns:
835 A webapp.WSGIApplication that handles the redirect back from the
836 server during the OAuth 2.0 dance.
837 """
838 return webapp.WSGIApplication([
839 (self.callback_path, self.callback_handler())
840 ])
841
Joe Gregorio432f17e2011-05-22 23:18:00 -0400842
Joe Gregoriof08a4982011-10-07 13:11:16 -0400843class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
844 """An OAuth2Decorator that builds from a clientsecrets file.
845
846 Uses a clientsecrets file as the source for all the information when
847 constructing an OAuth2Decorator.
848
849 Example:
850
851 decorator = OAuth2DecoratorFromClientSecrets(
852 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500853 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400854
855
856 class MainHandler(webapp.RequestHandler):
857
858 @decorator.oauth_required
859 def get(self):
860 http = decorator.http()
861 # http is authorized with the user's Credentials and can be used
862 # in API calls
863 """
864
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400865 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400866 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400867 """Constructor
868
869 Args:
870 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500871 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400872 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400873 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400874 clientsecrets file is missing or invalid. The message may contain HTML
875 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400876 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400877 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400878 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400879 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400880 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
881 if client_type not in [
882 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
883 raise InvalidClientSecretsError(
884 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800885 constructor_kwargs = {
886 'auth_uri': client_info['auth_uri'],
887 'token_uri': client_info['token_uri'],
888 'message': message,
889 }
890 revoke_uri = client_info.get('revoke_uri')
891 if revoke_uri is not None:
892 constructor_kwargs['revoke_uri'] = revoke_uri
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400893 super(OAuth2DecoratorFromClientSecrets, self).__init__(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800894 client_info['client_id'], client_info['client_secret'],
895 scope, **constructor_kwargs)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400896 if message is not None:
897 self._message = message
898 else:
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800899 self._message = 'Please configure your application for OAuth 2.0.'
Joe Gregoriof08a4982011-10-07 13:11:16 -0400900
901
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400902@util.positional(2)
903def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400904 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400905 """Creates an OAuth2Decorator populated from a clientsecrets file.
906
907 Args:
908 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400909 scope: string or list of strings, scope(s) of the credentials being
910 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400911 message: string, A friendly string to display to the user if the
912 clientsecrets file is missing or invalid. The message may contain HTML and
913 will be presented on the web interface for any method that uses the
914 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400915 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400916 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400917
918 Returns: An OAuth2Decorator
919
920 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400921 return OAuth2DecoratorFromClientSecrets(filename, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800922 message=message, cache=cache)