blob: fc148b8fc861b4d6b1f466edf43e38502a1ec63c [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 Gregorioa19f3a72012-07-11 15:35:35 -040058logger = logging.getLogger(__name__)
59
Joe Gregorio432f17e2011-05-22 23:18:00 -040060OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050061
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040062XSRF_MEMCACHE_ID = 'xsrf_secret_key'
63
JacobMoshenko8e905102011-06-20 09:53:10 -040064
Joe Gregorio77254c12012-08-27 14:13:22 -040065def _safe_html(s):
66 """Escape text to make it safe to display.
67
68 Args:
69 s: string, The text to escape.
70
71 Returns:
72 The escaped text as a string.
73 """
74 return cgi.escape(s, quote=1).replace("'", ''')
75
76
Joe Gregoriof08a4982011-10-07 13:11:16 -040077class InvalidClientSecretsError(Exception):
78 """The client_secrets.json file is malformed or missing required fields."""
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040079
80
81class InvalidXsrfTokenError(Exception):
82 """The XSRF token is invalid or expired."""
83
84
85class SiteXsrfSecretKey(db.Model):
86 """Storage for the sites XSRF secret key.
87
88 There will only be one instance stored of this model, the one used for the
dhermes@google.com47154822012-11-26 10:44:09 -080089 site.
90 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040091 secret = db.StringProperty()
92
Joe Gregorio78787b62013-02-08 15:36:21 -050093if ndb is not None:
94 class SiteXsrfSecretKeyNDB(ndb.Model):
95 """NDB Model for storage for the sites XSRF secret key.
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040096
Joe Gregorio78787b62013-02-08 15:36:21 -050097 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
98 interchangeably. This simply provides an NDB model for interacting with the
99 same data the DB model interacts with.
dhermes@google.com47154822012-11-26 10:44:09 -0800100
Joe Gregorio78787b62013-02-08 15:36:21 -0500101 There should only be one instance stored of this model, the one used for the
102 site.
103 """
104 secret = ndb.StringProperty()
dhermes@google.com47154822012-11-26 10:44:09 -0800105
Joe Gregorio78787b62013-02-08 15:36:21 -0500106 @classmethod
107 def _get_kind(cls):
108 """Return the kind name for this class."""
109 return 'SiteXsrfSecretKey'
dhermes@google.com47154822012-11-26 10:44:09 -0800110
111
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400112def _generate_new_xsrf_secret_key():
113 """Returns a random XSRF secret key.
114 """
115 return os.urandom(16).encode("hex")
116
117
118def xsrf_secret_key():
119 """Return the secret key for use for XSRF protection.
120
121 If the Site entity does not have a secret key, this method will also create
122 one and persist it.
123
124 Returns:
125 The secret key.
126 """
127 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
128 if not secret:
129 # Load the one and only instance of SiteXsrfSecretKey.
130 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
131 if not model.secret:
132 model.secret = _generate_new_xsrf_secret_key()
133 model.put()
134 secret = model.secret
135 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
136
137 return str(secret)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400138
139
JacobMoshenko8e905102011-06-20 09:53:10 -0400140class AppAssertionCredentials(AssertionCredentials):
141 """Credentials object for App Engine Assertion Grants
142
143 This object will allow an App Engine application to identify itself to Google
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400144 and other OAuth 2.0 servers that can verify assertions. It can be used for the
145 purpose of accessing data stored under an account assigned to the App Engine
146 application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -0400147
148 This credential does not require a flow to instantiate because it represents
149 a two legged flow, and therefore has all of the required information to
150 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -0400151 """
152
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400153 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500154 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -0400155 """Constructor for AppAssertionCredentials
156
157 Args:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500158 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriofd08e432012-08-09 14:17:41 -0400159 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400160 """
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500161 self.scope = util.scopes_to_string(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400162
dhermes@google.com2cc09382013-02-11 08:42:18 -0800163 # Assertion type is no longer used, but still in the parent class signature.
164 super(AppAssertionCredentials, self).__init__(None)
JacobMoshenko8e905102011-06-20 09:53:10 -0400165
Joe Gregorio562b7312011-09-15 09:06:38 -0400166 @classmethod
167 def from_json(cls, json):
168 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500169 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400170
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500171 def _refresh(self, http_request):
172 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400173
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500174 Since the underlying App Engine app_identity implementation does its own
175 caching we can skip all the storage hoops and just to a refresh using the
176 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400177
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500178 Args:
179 http_request: callable, a callable that matches the method signature of
180 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400181
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500182 Raises:
183 AccessTokenRefreshError: When the refresh fails.
184 """
185 try:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500186 scopes = self.scope.split()
187 (token, _) = app_identity.get_access_token(scopes)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500188 except app_identity.Error, e:
189 raise AccessTokenRefreshError(str(e))
190 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400191
192
Joe Gregorio695fdc12011-01-16 16:46:55 -0500193class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500194 """App Engine datastore Property for Flow.
195
dhermes@google.com47154822012-11-26 10:44:09 -0800196 Utility property that allows easy storage and retrieval of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500197 oauth2client.Flow"""
198
199 # Tell what the user type is.
200 data_type = Flow
201
202 # For writing to datastore.
203 def get_value_for_datastore(self, model_instance):
204 flow = super(FlowProperty,
205 self).get_value_for_datastore(model_instance)
206 return db.Blob(pickle.dumps(flow))
207
208 # For reading from datastore.
209 def make_value_from_datastore(self, value):
210 if value is None:
211 return None
212 return pickle.loads(value)
213
214 def validate(self, value):
215 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400216 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500217 'to a FlowThreeLegged instance (%s)' %
218 (self.name, value))
219 return super(FlowProperty, self).validate(value)
220
221 def empty(self, value):
222 return not value
223
224
Joe Gregorio78787b62013-02-08 15:36:21 -0500225if ndb is not None:
226 class FlowNDBProperty(ndb.PickleProperty):
227 """App Engine NDB datastore Property for Flow.
dhermes@google.com47154822012-11-26 10:44:09 -0800228
Joe Gregorio78787b62013-02-08 15:36:21 -0500229 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
230 PickleProperty inherits from BlobProperty, the underlying representation of
231 the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800232
Joe Gregorio78787b62013-02-08 15:36:21 -0500233 Utility property that allows easy storage and retrieval of an
234 oauth2client.Flow
dhermes@google.com47154822012-11-26 10:44:09 -0800235 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500236
237 def _validate(self, value):
238 """Validates a value as a proper Flow object.
239
240 Args:
241 value: A value to be set on the property.
242
243 Raises:
244 TypeError if the value is not an instance of Flow.
245 """
246 logger.info('validate: Got type %s', type(value))
247 if value is not None and not isinstance(value, Flow):
248 raise TypeError('Property %s must be convertible to a flow '
249 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800250
251
Joe Gregorio695fdc12011-01-16 16:46:55 -0500252class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500253 """App Engine datastore Property for Credentials.
254
255 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500256 oath2client.Credentials
257 """
258
259 # Tell what the user type is.
260 data_type = Credentials
261
262 # For writing to datastore.
263 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400264 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500265 cred = super(CredentialsProperty,
266 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400267 if cred is None:
268 cred = ''
269 else:
270 cred = cred.to_json()
271 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500272
273 # For reading from datastore.
274 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400275 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500276 if value is None:
277 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400278 if len(value) == 0:
279 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400280 try:
281 credentials = Credentials.new_from_json(value)
282 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400283 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400284 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500285
286 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400287 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400288 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500289 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400290 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400291 'to a Credentials instance (%s)' %
292 (self.name, value))
293 #if value is not None and not isinstance(value, Credentials):
294 # return None
295 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500296
297
Joe Gregorio78787b62013-02-08 15:36:21 -0500298if ndb is not None:
299 # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
300 # and subclass mechanics to use new_from_dict, to_dict,
301 # from_dict, etc.
302 class CredentialsNDBProperty(ndb.BlobProperty):
303 """App Engine NDB datastore Property for Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500304
Joe Gregorio78787b62013-02-08 15:36:21 -0500305 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
306 Since CredentialsProperty stores data as a blob and this inherits from
307 BlobProperty, the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800308
Joe Gregorio78787b62013-02-08 15:36:21 -0500309 Utility property that allows easy storage and retrieval of Credentials and
310 subclasses.
dhermes@google.com47154822012-11-26 10:44:09 -0800311 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500312 def _validate(self, value):
313 """Validates a value as a proper credentials object.
dhermes@google.com47154822012-11-26 10:44:09 -0800314
Joe Gregorio78787b62013-02-08 15:36:21 -0500315 Args:
316 value: A value to be set on the property.
dhermes@google.com47154822012-11-26 10:44:09 -0800317
Joe Gregorio78787b62013-02-08 15:36:21 -0500318 Raises:
319 TypeError if the value is not an instance of Credentials.
320 """
321 logger.info('validate: Got type %s', type(value))
322 if value is not None and not isinstance(value, Credentials):
323 raise TypeError('Property %s must be convertible to a credentials '
324 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800325
Joe Gregorio78787b62013-02-08 15:36:21 -0500326 def _to_base_type(self, value):
327 """Converts our validated value to a JSON serialized string.
dhermes@google.com47154822012-11-26 10:44:09 -0800328
Joe Gregorio78787b62013-02-08 15:36:21 -0500329 Args:
330 value: A value to be set in the datastore.
dhermes@google.com47154822012-11-26 10:44:09 -0800331
Joe Gregorio78787b62013-02-08 15:36:21 -0500332 Returns:
333 A JSON serialized version of the credential, else '' if value is None.
334 """
335 if value is None:
336 return ''
337 else:
338 return value.to_json()
dhermes@google.com47154822012-11-26 10:44:09 -0800339
Joe Gregorio78787b62013-02-08 15:36:21 -0500340 def _from_base_type(self, value):
341 """Converts our stored JSON string back to the desired type.
342
343 Args:
344 value: A value from the datastore to be converted to the desired type.
345
346 Returns:
347 A deserialized Credentials (or subclass) object, else None if the
348 value can't be parsed.
349 """
350 if not value:
351 return None
352 try:
353 # Uses the from_json method of the implied class of value
354 credentials = Credentials.new_from_json(value)
355 except ValueError:
356 credentials = None
357 return credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800358
359
360class StorageByKeyName(Storage):
361 """Store and retrieve a credential to and from the App Engine datastore.
362
363 This Storage helper presumes the Credentials have been stored as a
364 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
365 that entities are stored by key_name.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500366 """
367
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400368 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400369 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500370 """Constructor for Storage.
371
372 Args:
dhermes@google.com47154822012-11-26 10:44:09 -0800373 model: db.Model or ndb.Model, model class
Joe Gregorio695fdc12011-01-16 16:46:55 -0500374 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400375 property_name: string, name of the property that is a CredentialsProperty
dhermes@google.com47154822012-11-26 10:44:09 -0800376 or CredentialsNDBProperty.
377 cache: memcache, a write-through cache to put in front of the datastore.
378 If the model you are using is an NDB model, using a cache will be
379 redundant since the model uses an instance cache and memcache for you.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500380 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500381 self._model = model
382 self._key_name = key_name
383 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400384 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500385
dhermes@google.com47154822012-11-26 10:44:09 -0800386 def _is_ndb(self):
387 """Determine whether the model of the instance is an NDB model.
388
389 Returns:
390 Boolean indicating whether or not the model is an NDB or DB model.
391 """
392 # issubclass will fail if one of the arguments is not a class, only need
393 # worry about new-style classes since ndb and db models are new-style
394 if isinstance(self._model, type):
Joe Gregorio78787b62013-02-08 15:36:21 -0500395 if ndb is not None and issubclass(self._model, ndb.Model):
dhermes@google.com47154822012-11-26 10:44:09 -0800396 return True
397 elif issubclass(self._model, db.Model):
398 return False
399
400 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
401
402 def _get_entity(self):
403 """Retrieve entity from datastore.
404
405 Uses a different model method for db or ndb models.
406
407 Returns:
408 Instance of the model corresponding to the current storage object
409 and stored using the key name of the storage object.
410 """
411 if self._is_ndb():
412 return self._model.get_by_id(self._key_name)
413 else:
414 return self._model.get_by_key_name(self._key_name)
415
416 def _delete_entity(self):
417 """Delete entity from datastore.
418
419 Attempts to delete using the key_name stored on the object, whether or not
420 the given key is in the datastore.
421 """
422 if self._is_ndb():
423 ndb.Key(self._model, self._key_name).delete()
424 else:
425 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
426 db.delete(entity_key)
427
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400428 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500429 """Retrieve Credential from datastore.
430
431 Returns:
432 oauth2client.Credentials
433 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400434 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400435 json = self._cache.get(self._key_name)
436 if json:
437 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500438
dhermes@google.com47154822012-11-26 10:44:09 -0800439 credentials = None
440 entity = self._get_entity()
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500441 if entity is not None:
dhermes@google.com47154822012-11-26 10:44:09 -0800442 credentials = getattr(entity, self._property_name)
443 if credentials and hasattr(credentials, 'set_store'):
444 credentials.set_store(self)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500445 if self._cache:
dhermes@google.com47154822012-11-26 10:44:09 -0800446 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400447
dhermes@google.com47154822012-11-26 10:44:09 -0800448 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500449
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400450 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500451 """Write a Credentials to the datastore.
452
453 Args:
454 credentials: Credentials, the credentials to store.
455 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500456 entity = self._model.get_or_insert(self._key_name)
457 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500458 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400459 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400460 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400461
Joe Gregorioec75dc12012-02-06 13:40:42 -0500462 def locked_delete(self):
463 """Delete Credential from datastore."""
464
465 if self._cache:
466 self._cache.delete(self._key_name)
467
dhermes@google.com47154822012-11-26 10:44:09 -0800468 self._delete_entity()
Joe Gregorioec75dc12012-02-06 13:40:42 -0500469
Joe Gregorio432f17e2011-05-22 23:18:00 -0400470
471class CredentialsModel(db.Model):
472 """Storage for OAuth 2.0 Credentials
473
474 Storage of the model is keyed by the user.user_id().
475 """
476 credentials = CredentialsProperty()
477
478
Joe Gregorio78787b62013-02-08 15:36:21 -0500479if ndb is not None:
480 class CredentialsNDBModel(ndb.Model):
481 """NDB Model for storage of OAuth 2.0 Credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800482
Joe Gregorio78787b62013-02-08 15:36:21 -0500483 Since this model uses the same kind as CredentialsModel and has a property
484 which can serialize and deserialize Credentials correctly, it can be used
485 interchangeably with a CredentialsModel to access, insert and delete the
486 same entities. This simply provides an NDB model for interacting with the
487 same data the DB model interacts with.
dhermes@google.com47154822012-11-26 10:44:09 -0800488
Joe Gregorio78787b62013-02-08 15:36:21 -0500489 Storage of the model is keyed by the user.user_id().
490 """
491 credentials = CredentialsNDBProperty()
dhermes@google.com47154822012-11-26 10:44:09 -0800492
Joe Gregorio78787b62013-02-08 15:36:21 -0500493 @classmethod
494 def _get_kind(cls):
495 """Return the kind name for this class."""
496 return 'CredentialsModel'
dhermes@google.com47154822012-11-26 10:44:09 -0800497
498
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400499def _build_state_value(request_handler, user):
500 """Composes the value for the 'state' parameter.
501
502 Packs the current request URI and an XSRF token into an opaque string that
503 can be passed to the authentication server via the 'state' parameter.
504
505 Args:
506 request_handler: webapp.RequestHandler, The request.
507 user: google.appengine.api.users.User, The current user.
508
509 Returns:
510 The state value as a string.
511 """
512 uri = request_handler.request.url
513 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
514 action_id=str(uri))
515 return uri + ':' + token
516
517
518def _parse_state_value(state, user):
519 """Parse the value of the 'state' parameter.
520
521 Parses the value and validates the XSRF token in the state parameter.
522
523 Args:
524 state: string, The value of the state parameter.
525 user: google.appengine.api.users.User, The current user.
526
527 Raises:
528 InvalidXsrfTokenError: if the XSRF token is invalid.
529
530 Returns:
531 The redirect URI.
532 """
533 uri, token = state.rsplit(':', 1)
534 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
535 action_id=uri):
536 raise InvalidXsrfTokenError()
537
538 return uri
539
540
Joe Gregorio432f17e2011-05-22 23:18:00 -0400541class OAuth2Decorator(object):
542 """Utility for making OAuth 2.0 easier.
543
544 Instantiate and then use with oauth_required or oauth_aware
545 as decorators on webapp.RequestHandler methods.
546
547 Example:
548
549 decorator = OAuth2Decorator(
550 client_id='837...ent.com',
551 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500552 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400553
554
555 class MainHandler(webapp.RequestHandler):
556
557 @decorator.oauth_required
558 def get(self):
559 http = decorator.http()
560 # http is authorized with the user's Credentials and can be used
561 # in API calls
562
563 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400564
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400565 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400566 def __init__(self, client_id, client_secret, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800567 auth_uri=GOOGLE_AUTH_URI,
568 token_uri=GOOGLE_TOKEN_URI,
569 revoke_uri=GOOGLE_REVOKE_URI,
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100570 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400571 message=None,
572 callback_path='/oauth2callback',
573 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400574
575 """Constructor for OAuth2Decorator
576
577 Args:
578 client_id: string, client identifier.
579 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500580 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400581 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400582 auth_uri: string, URI for authorization endpoint. For convenience
583 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
584 token_uri: string, URI for token endpoint. For convenience
585 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800586 revoke_uri: string, URI for revoke endpoint. For convenience
587 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100588 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400589 message: Message to display if there are problems with the OAuth 2.0
590 configuration. The message may contain HTML and will be presented on the
591 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400592 callback_path: string, The absolute path to use as the callback URI. Note
593 that this must match up with the URI given when registering the
594 application in the APIs Console.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500595 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
596 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400597 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400598 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400599 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400600 self._client_id = client_id
601 self._client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500602 self._scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400603 self._auth_uri = auth_uri
604 self._token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800605 self._revoke_uri = revoke_uri
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400606 self._user_agent = user_agent
607 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400608 self._message = message
609 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400610 self._callback_path = callback_path
Joe Gregoriof08a4982011-10-07 13:11:16 -0400611
612 def _display_error_message(self, request_handler):
613 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400614 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400615 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400616
617 def oauth_required(self, method):
618 """Decorator that starts the OAuth 2.0 dance.
619
620 Starts the OAuth dance for the logged in user if they haven't already
621 granted access for this application.
622
623 Args:
624 method: callable, to be decorated method of a webapp.RequestHandler
625 instance.
626 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400627
Joe Gregorio17774972012-03-01 11:11:59 -0500628 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400629 if self._in_error:
630 self._display_error_message(request_handler)
631 return
632
Joe Gregoriof427c532011-06-13 09:35:26 -0400633 user = users.get_current_user()
634 # Don't use @login_decorator as this could be used in a POST request.
635 if not user:
636 request_handler.redirect(users.create_login_url(
637 request_handler.request.uri))
638 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400639
640 self._create_flow(request_handler)
641
Joe Gregorio432f17e2011-05-22 23:18:00 -0400642 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400643 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400644 self.credentials = StorageByKeyName(
645 CredentialsModel, user.user_id(), 'credentials').get()
646
647 if not self.has_credentials():
648 return request_handler.redirect(self.authorize_url())
649 try:
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400650 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400651 except AccessTokenRefreshError:
652 return request_handler.redirect(self.authorize_url())
653
654 return check_oauth
655
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400656 def _create_flow(self, request_handler):
657 """Create the Flow object.
658
659 The Flow is calculated lazily since we don't know where this app is
660 running until it receives a request, at which point redirect_uri can be
661 calculated and then the Flow object can be constructed.
662
663 Args:
664 request_handler: webapp.RequestHandler, the request handler.
665 """
666 if self.flow is None:
667 redirect_uri = request_handler.request.relative_url(
668 self._callback_path) # Usually /oauth2callback
669 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
670 self._scope, redirect_uri=redirect_uri,
671 user_agent=self._user_agent,
672 auth_uri=self._auth_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800673 token_uri=self._token_uri,
674 revoke_uri=self._revoke_uri,
675 **self._kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400676
Joe Gregorio432f17e2011-05-22 23:18:00 -0400677 def oauth_aware(self, method):
678 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
679
680 Does all the setup for the OAuth dance, but doesn't initiate it.
681 This decorator is useful if you want to create a page that knows
682 whether or not the user has granted access to this application.
683 From within a method decorated with @oauth_aware the has_credentials()
684 and authorize_url() methods can be called.
685
686 Args:
687 method: callable, to be decorated method of a webapp.RequestHandler
688 instance.
689 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400690
Joe Gregorio17774972012-03-01 11:11:59 -0500691 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400692 if self._in_error:
693 self._display_error_message(request_handler)
694 return
695
Joe Gregoriof427c532011-06-13 09:35:26 -0400696 user = users.get_current_user()
697 # Don't use @login_decorator as this could be used in a POST request.
698 if not user:
699 request_handler.redirect(users.create_login_url(
700 request_handler.request.uri))
701 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400702
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400703 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400704
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400705 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400706 self.credentials = StorageByKeyName(
707 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400708 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400709 return setup_oauth
710
711 def has_credentials(self):
712 """True if for the logged in user there are valid access Credentials.
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 """
717 return self.credentials is not None and not self.credentials.invalid
718
719 def authorize_url(self):
720 """Returns the URL to start the OAuth dance.
721
722 Must only be called from with a webapp.RequestHandler subclassed method
723 that had been decorated with either @oauth_required or @oauth_aware.
724 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400725 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500726 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400727
728 def http(self):
729 """Returns an authorized http instance.
730
731 Must only be called from within an @oauth_required decorated method, or
732 from within an @oauth_aware decorated method where has_credentials()
733 returns True.
734 """
735 return self.credentials.authorize(httplib2.Http())
736
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400737 @property
738 def callback_path(self):
739 """The absolute path where the callback will occur.
740
741 Note this is the absolute path, not the absolute URI, that will be
742 calculated by the decorator at runtime. See callback_handler() for how this
743 should be used.
744
745 Returns:
746 The callback path as a string.
747 """
748 return self._callback_path
749
750
751 def callback_handler(self):
752 """RequestHandler for the OAuth 2.0 redirect callback.
753
754 Usage:
755 app = webapp.WSGIApplication([
756 ('/index', MyIndexHandler),
757 ...,
758 (decorator.callback_path, decorator.callback_handler())
759 ])
760
761 Returns:
762 A webapp.RequestHandler that handles the redirect back from the
763 server during the OAuth 2.0 dance.
764 """
765 decorator = self
766
767 class OAuth2Handler(webapp.RequestHandler):
768 """Handler for the redirect_uri of the OAuth 2.0 dance."""
769
770 @login_required
771 def get(self):
772 error = self.request.get('error')
773 if error:
774 errormsg = self.request.get('error_description', error)
775 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400776 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400777 else:
778 user = users.get_current_user()
779 decorator._create_flow(self)
780 credentials = decorator.flow.step2_exchange(self.request.params)
781 StorageByKeyName(
782 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400783 redirect_uri = _parse_state_value(str(self.request.get('state')),
784 user)
785 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400786
787 return OAuth2Handler
788
789 def callback_application(self):
790 """WSGI application for handling the OAuth 2.0 redirect callback.
791
792 If you need finer grained control use `callback_handler` which returns just
793 the webapp.RequestHandler.
794
795 Returns:
796 A webapp.WSGIApplication that handles the redirect back from the
797 server during the OAuth 2.0 dance.
798 """
799 return webapp.WSGIApplication([
800 (self.callback_path, self.callback_handler())
801 ])
802
Joe Gregorio432f17e2011-05-22 23:18:00 -0400803
Joe Gregoriof08a4982011-10-07 13:11:16 -0400804class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
805 """An OAuth2Decorator that builds from a clientsecrets file.
806
807 Uses a clientsecrets file as the source for all the information when
808 constructing an OAuth2Decorator.
809
810 Example:
811
812 decorator = OAuth2DecoratorFromClientSecrets(
813 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500814 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400815
816
817 class MainHandler(webapp.RequestHandler):
818
819 @decorator.oauth_required
820 def get(self):
821 http = decorator.http()
822 # http is authorized with the user's Credentials and can be used
823 # in API calls
824 """
825
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400826 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400827 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400828 """Constructor
829
830 Args:
831 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500832 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400833 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400834 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400835 clientsecrets file is missing or invalid. The message may contain HTML
836 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400837 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400838 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400839 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400840 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400841 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
842 if client_type not in [
843 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
844 raise InvalidClientSecretsError(
845 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800846 constructor_kwargs = {
847 'auth_uri': client_info['auth_uri'],
848 'token_uri': client_info['token_uri'],
849 'message': message,
850 }
851 revoke_uri = client_info.get('revoke_uri')
852 if revoke_uri is not None:
853 constructor_kwargs['revoke_uri'] = revoke_uri
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400854 super(OAuth2DecoratorFromClientSecrets, self).__init__(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800855 client_info['client_id'], client_info['client_secret'],
856 scope, **constructor_kwargs)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400857 if message is not None:
858 self._message = message
859 else:
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800860 self._message = 'Please configure your application for OAuth 2.0.'
Joe Gregoriof08a4982011-10-07 13:11:16 -0400861
862
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400863@util.positional(2)
864def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400865 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400866 """Creates an OAuth2Decorator populated from a clientsecrets file.
867
868 Args:
869 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400870 scope: string or list of strings, scope(s) of the credentials being
871 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400872 message: string, A friendly string to display to the user if the
873 clientsecrets file is missing or invalid. The message may contain HTML and
874 will be presented on the web interface for any method that uses the
875 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400876 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400877 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400878
879 Returns: An OAuth2Decorator
880
881 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400882 return OAuth2DecoratorFromClientSecrets(filename, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800883 message=message, cache=cache)