blob: a4738f80552fce72ef4d6ff1b4ea9e4a27a25e24 [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
Joe Gregoriocda87522013-02-22 16:22:48 -050029import urllib
30import urlparse
JacobMoshenko8e905102011-06-20 09:53:10 -040031
Joe Gregoriod84d6b82012-02-28 14:53:00 -050032from google.appengine.api import app_identity
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040033from google.appengine.api import memcache
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040034from google.appengine.api import users
Joe Gregorio432f17e2011-05-22 23:18:00 -040035from google.appengine.ext import db
36from google.appengine.ext import webapp
37from google.appengine.ext.webapp.util import login_required
38from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregoriocda87522013-02-22 16:22:48 -050039from apiclient import discovery
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -080040from oauth2client import GOOGLE_AUTH_URI
41from oauth2client import GOOGLE_REVOKE_URI
42from oauth2client import GOOGLE_TOKEN_URI
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040043from oauth2client import clientsecrets
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040044from oauth2client import util
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040045from oauth2client import xsrfutil
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040046from oauth2client.anyjson import simplejson
47from oauth2client.client import AccessTokenRefreshError
48from oauth2client.client import AssertionCredentials
49from oauth2client.client import Credentials
50from oauth2client.client import Flow
51from oauth2client.client import OAuth2WebServerFlow
52from oauth2client.client import Storage
Joe Gregorioa19f3a72012-07-11 15:35:35 -040053
Joe Gregorio78787b62013-02-08 15:36:21 -050054# TODO(dhermes): Resolve import issue.
55# This is a temporary fix for a Google internal issue.
56try:
57 from google.appengine.ext import ndb
58except ImportError:
59 ndb = None
60
Joe Gregoriocda87522013-02-22 16:22:48 -050061try:
62 from urlparse import parse_qsl
63except ImportError:
64 from cgi import parse_qsl
65
Joe Gregorioa19f3a72012-07-11 15:35:35 -040066logger = logging.getLogger(__name__)
67
Joe Gregorio432f17e2011-05-22 23:18:00 -040068OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050069
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040070XSRF_MEMCACHE_ID = 'xsrf_secret_key'
71
JacobMoshenko8e905102011-06-20 09:53:10 -040072
Joe Gregorio77254c12012-08-27 14:13:22 -040073def _safe_html(s):
74 """Escape text to make it safe to display.
75
76 Args:
77 s: string, The text to escape.
78
79 Returns:
80 The escaped text as a string.
81 """
82 return cgi.escape(s, quote=1).replace("'", ''')
83
84
Joe Gregoriof08a4982011-10-07 13:11:16 -040085class InvalidClientSecretsError(Exception):
86 """The client_secrets.json file is malformed or missing required fields."""
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040087
88
89class InvalidXsrfTokenError(Exception):
90 """The XSRF token is invalid or expired."""
91
92
93class SiteXsrfSecretKey(db.Model):
94 """Storage for the sites XSRF secret key.
95
96 There will only be one instance stored of this model, the one used for the
dhermes@google.com47154822012-11-26 10:44:09 -080097 site.
98 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040099 secret = db.StringProperty()
100
Joe Gregorio78787b62013-02-08 15:36:21 -0500101if ndb is not None:
102 class SiteXsrfSecretKeyNDB(ndb.Model):
103 """NDB Model for storage for the sites XSRF secret key.
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400104
Joe Gregorio78787b62013-02-08 15:36:21 -0500105 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
106 interchangeably. This simply provides an NDB model for interacting with the
107 same data the DB model interacts with.
dhermes@google.com47154822012-11-26 10:44:09 -0800108
Joe Gregorio78787b62013-02-08 15:36:21 -0500109 There should only be one instance stored of this model, the one used for the
110 site.
111 """
112 secret = ndb.StringProperty()
dhermes@google.com47154822012-11-26 10:44:09 -0800113
Joe Gregorio78787b62013-02-08 15:36:21 -0500114 @classmethod
115 def _get_kind(cls):
116 """Return the kind name for this class."""
117 return 'SiteXsrfSecretKey'
dhermes@google.com47154822012-11-26 10:44:09 -0800118
119
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400120def _generate_new_xsrf_secret_key():
121 """Returns a random XSRF secret key.
122 """
123 return os.urandom(16).encode("hex")
124
125
126def xsrf_secret_key():
127 """Return the secret key for use for XSRF protection.
128
129 If the Site entity does not have a secret key, this method will also create
130 one and persist it.
131
132 Returns:
133 The secret key.
134 """
135 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
136 if not secret:
137 # Load the one and only instance of SiteXsrfSecretKey.
138 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
139 if not model.secret:
140 model.secret = _generate_new_xsrf_secret_key()
141 model.put()
142 secret = model.secret
143 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
144
145 return str(secret)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400146
147
JacobMoshenko8e905102011-06-20 09:53:10 -0400148class AppAssertionCredentials(AssertionCredentials):
149 """Credentials object for App Engine Assertion Grants
150
151 This object will allow an App Engine application to identify itself to Google
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400152 and other OAuth 2.0 servers that can verify assertions. It can be used for the
153 purpose of accessing data stored under an account assigned to the App Engine
154 application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -0400155
156 This credential does not require a flow to instantiate because it represents
157 a two legged flow, and therefore has all of the required information to
158 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -0400159 """
160
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400161 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500162 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -0400163 """Constructor for AppAssertionCredentials
164
165 Args:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500166 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriofd08e432012-08-09 14:17:41 -0400167 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400168 """
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500169 self.scope = util.scopes_to_string(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400170
dhermes@google.com2cc09382013-02-11 08:42:18 -0800171 # Assertion type is no longer used, but still in the parent class signature.
172 super(AppAssertionCredentials, self).__init__(None)
JacobMoshenko8e905102011-06-20 09:53:10 -0400173
Joe Gregorio562b7312011-09-15 09:06:38 -0400174 @classmethod
175 def from_json(cls, json):
176 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500177 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400178
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500179 def _refresh(self, http_request):
180 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400181
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500182 Since the underlying App Engine app_identity implementation does its own
183 caching we can skip all the storage hoops and just to a refresh using the
184 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400185
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500186 Args:
187 http_request: callable, a callable that matches the method signature of
188 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400189
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500190 Raises:
191 AccessTokenRefreshError: When the refresh fails.
192 """
193 try:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500194 scopes = self.scope.split()
195 (token, _) = app_identity.get_access_token(scopes)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500196 except app_identity.Error, e:
197 raise AccessTokenRefreshError(str(e))
198 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400199
200
Joe Gregorio695fdc12011-01-16 16:46:55 -0500201class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500202 """App Engine datastore Property for Flow.
203
dhermes@google.com47154822012-11-26 10:44:09 -0800204 Utility property that allows easy storage and retrieval of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500205 oauth2client.Flow"""
206
207 # Tell what the user type is.
208 data_type = Flow
209
210 # For writing to datastore.
211 def get_value_for_datastore(self, model_instance):
212 flow = super(FlowProperty,
213 self).get_value_for_datastore(model_instance)
214 return db.Blob(pickle.dumps(flow))
215
216 # For reading from datastore.
217 def make_value_from_datastore(self, value):
218 if value is None:
219 return None
220 return pickle.loads(value)
221
222 def validate(self, value):
223 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400224 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500225 'to a FlowThreeLegged instance (%s)' %
226 (self.name, value))
227 return super(FlowProperty, self).validate(value)
228
229 def empty(self, value):
230 return not value
231
232
Joe Gregorio78787b62013-02-08 15:36:21 -0500233if ndb is not None:
234 class FlowNDBProperty(ndb.PickleProperty):
235 """App Engine NDB datastore Property for Flow.
dhermes@google.com47154822012-11-26 10:44:09 -0800236
Joe Gregorio78787b62013-02-08 15:36:21 -0500237 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
238 PickleProperty inherits from BlobProperty, the underlying representation of
239 the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800240
Joe Gregorio78787b62013-02-08 15:36:21 -0500241 Utility property that allows easy storage and retrieval of an
242 oauth2client.Flow
dhermes@google.com47154822012-11-26 10:44:09 -0800243 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500244
245 def _validate(self, value):
246 """Validates a value as a proper Flow object.
247
248 Args:
249 value: A value to be set on the property.
250
251 Raises:
252 TypeError if the value is not an instance of Flow.
253 """
254 logger.info('validate: Got type %s', type(value))
255 if value is not None and not isinstance(value, Flow):
256 raise TypeError('Property %s must be convertible to a flow '
257 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800258
259
Joe Gregorio695fdc12011-01-16 16:46:55 -0500260class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500261 """App Engine datastore Property for Credentials.
262
263 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500264 oath2client.Credentials
265 """
266
267 # Tell what the user type is.
268 data_type = Credentials
269
270 # For writing to datastore.
271 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400272 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500273 cred = super(CredentialsProperty,
274 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400275 if cred is None:
276 cred = ''
277 else:
278 cred = cred.to_json()
279 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500280
281 # For reading from datastore.
282 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400283 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500284 if value is None:
285 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400286 if len(value) == 0:
287 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400288 try:
289 credentials = Credentials.new_from_json(value)
290 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400291 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400292 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500293
294 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400295 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400296 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500297 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400298 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400299 'to a Credentials instance (%s)' %
300 (self.name, value))
301 #if value is not None and not isinstance(value, Credentials):
302 # return None
303 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500304
305
Joe Gregorio78787b62013-02-08 15:36:21 -0500306if ndb is not None:
307 # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
308 # and subclass mechanics to use new_from_dict, to_dict,
309 # from_dict, etc.
310 class CredentialsNDBProperty(ndb.BlobProperty):
311 """App Engine NDB datastore Property for Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500312
Joe Gregorio78787b62013-02-08 15:36:21 -0500313 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
314 Since CredentialsProperty stores data as a blob and this inherits from
315 BlobProperty, the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800316
Joe Gregorio78787b62013-02-08 15:36:21 -0500317 Utility property that allows easy storage and retrieval of Credentials and
318 subclasses.
dhermes@google.com47154822012-11-26 10:44:09 -0800319 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500320 def _validate(self, value):
321 """Validates a value as a proper credentials object.
dhermes@google.com47154822012-11-26 10:44:09 -0800322
Joe Gregorio78787b62013-02-08 15:36:21 -0500323 Args:
324 value: A value to be set on the property.
dhermes@google.com47154822012-11-26 10:44:09 -0800325
Joe Gregorio78787b62013-02-08 15:36:21 -0500326 Raises:
327 TypeError if the value is not an instance of Credentials.
328 """
329 logger.info('validate: Got type %s', type(value))
330 if value is not None and not isinstance(value, Credentials):
331 raise TypeError('Property %s must be convertible to a credentials '
332 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800333
Joe Gregorio78787b62013-02-08 15:36:21 -0500334 def _to_base_type(self, value):
335 """Converts our validated value to a JSON serialized string.
dhermes@google.com47154822012-11-26 10:44:09 -0800336
Joe Gregorio78787b62013-02-08 15:36:21 -0500337 Args:
338 value: A value to be set in the datastore.
dhermes@google.com47154822012-11-26 10:44:09 -0800339
Joe Gregorio78787b62013-02-08 15:36:21 -0500340 Returns:
341 A JSON serialized version of the credential, else '' if value is None.
342 """
343 if value is None:
344 return ''
345 else:
346 return value.to_json()
dhermes@google.com47154822012-11-26 10:44:09 -0800347
Joe Gregorio78787b62013-02-08 15:36:21 -0500348 def _from_base_type(self, value):
349 """Converts our stored JSON string back to the desired type.
350
351 Args:
352 value: A value from the datastore to be converted to the desired type.
353
354 Returns:
355 A deserialized Credentials (or subclass) object, else None if the
356 value can't be parsed.
357 """
358 if not value:
359 return None
360 try:
361 # Uses the from_json method of the implied class of value
362 credentials = Credentials.new_from_json(value)
363 except ValueError:
364 credentials = None
365 return credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800366
367
368class StorageByKeyName(Storage):
369 """Store and retrieve a credential to and from the App Engine datastore.
370
371 This Storage helper presumes the Credentials have been stored as a
372 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
373 that entities are stored by key_name.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500374 """
375
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400376 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400377 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500378 """Constructor for Storage.
379
380 Args:
dhermes@google.com47154822012-11-26 10:44:09 -0800381 model: db.Model or ndb.Model, model class
Joe Gregorio695fdc12011-01-16 16:46:55 -0500382 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400383 property_name: string, name of the property that is a CredentialsProperty
dhermes@google.com47154822012-11-26 10:44:09 -0800384 or CredentialsNDBProperty.
385 cache: memcache, a write-through cache to put in front of the datastore.
386 If the model you are using is an NDB model, using a cache will be
387 redundant since the model uses an instance cache and memcache for you.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500388 """
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,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400582 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400583
584 """Constructor for OAuth2Decorator
585
586 Args:
587 client_id: string, client identifier.
588 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500589 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400590 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400591 auth_uri: string, URI for authorization endpoint. For convenience
592 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
593 token_uri: string, URI for token endpoint. For convenience
594 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800595 revoke_uri: string, URI for revoke endpoint. For convenience
596 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100597 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400598 message: Message to display if there are problems with the OAuth 2.0
599 configuration. The message may contain HTML and will be presented on the
600 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400601 callback_path: string, The absolute path to use as the callback URI. Note
602 that this must match up with the URI given when registering the
603 application in the APIs Console.
Joe Gregoriocda87522013-02-22 16:22:48 -0500604 token_response_param: string. If provided, the full JSON response
605 to the access token request will be encoded and included in this query
606 parameter in the callback URI. This is useful with providers (e.g.
607 wordpress.com) that include extra fields that the client may want.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500608 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
609 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400610 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400611 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400612 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400613 self._client_id = client_id
614 self._client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500615 self._scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400616 self._auth_uri = auth_uri
617 self._token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800618 self._revoke_uri = revoke_uri
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400619 self._user_agent = user_agent
620 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400621 self._message = message
622 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400623 self._callback_path = callback_path
Joe Gregoriocda87522013-02-22 16:22:48 -0500624 self._token_response_param = token_response_param
Joe Gregoriof08a4982011-10-07 13:11:16 -0400625
626 def _display_error_message(self, request_handler):
627 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400628 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400629 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400630
631 def oauth_required(self, method):
632 """Decorator that starts the OAuth 2.0 dance.
633
634 Starts the OAuth dance for the logged in user if they haven't already
635 granted access for this application.
636
637 Args:
638 method: callable, to be decorated method of a webapp.RequestHandler
639 instance.
640 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400641
Joe Gregorio17774972012-03-01 11:11:59 -0500642 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400643 if self._in_error:
644 self._display_error_message(request_handler)
645 return
646
Joe Gregoriof427c532011-06-13 09:35:26 -0400647 user = users.get_current_user()
648 # Don't use @login_decorator as this could be used in a POST request.
649 if not user:
650 request_handler.redirect(users.create_login_url(
651 request_handler.request.uri))
652 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400653
654 self._create_flow(request_handler)
655
Joe Gregorio432f17e2011-05-22 23:18:00 -0400656 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400657 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400658 self.credentials = StorageByKeyName(
659 CredentialsModel, user.user_id(), 'credentials').get()
660
661 if not self.has_credentials():
662 return request_handler.redirect(self.authorize_url())
663 try:
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400664 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400665 except AccessTokenRefreshError:
666 return request_handler.redirect(self.authorize_url())
667
668 return check_oauth
669
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400670 def _create_flow(self, request_handler):
671 """Create the Flow object.
672
673 The Flow is calculated lazily since we don't know where this app is
674 running until it receives a request, at which point redirect_uri can be
675 calculated and then the Flow object can be constructed.
676
677 Args:
678 request_handler: webapp.RequestHandler, the request handler.
679 """
680 if self.flow is None:
681 redirect_uri = request_handler.request.relative_url(
682 self._callback_path) # Usually /oauth2callback
683 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
684 self._scope, redirect_uri=redirect_uri,
685 user_agent=self._user_agent,
686 auth_uri=self._auth_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800687 token_uri=self._token_uri,
688 revoke_uri=self._revoke_uri,
689 **self._kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400690
Joe Gregorio432f17e2011-05-22 23:18:00 -0400691 def oauth_aware(self, method):
692 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
693
694 Does all the setup for the OAuth dance, but doesn't initiate it.
695 This decorator is useful if you want to create a page that knows
696 whether or not the user has granted access to this application.
697 From within a method decorated with @oauth_aware the has_credentials()
698 and authorize_url() methods can be called.
699
700 Args:
701 method: callable, to be decorated method of a webapp.RequestHandler
702 instance.
703 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400704
Joe Gregorio17774972012-03-01 11:11:59 -0500705 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400706 if self._in_error:
707 self._display_error_message(request_handler)
708 return
709
Joe Gregoriof427c532011-06-13 09:35:26 -0400710 user = users.get_current_user()
711 # Don't use @login_decorator as this could be used in a POST request.
712 if not user:
713 request_handler.redirect(users.create_login_url(
714 request_handler.request.uri))
715 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400716
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400717 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400718
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400719 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400720 self.credentials = StorageByKeyName(
721 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400722 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400723 return setup_oauth
724
725 def has_credentials(self):
726 """True if for the logged in user there are valid access Credentials.
727
728 Must only be called from with a webapp.RequestHandler subclassed method
729 that had been decorated with either @oauth_required or @oauth_aware.
730 """
731 return self.credentials is not None and not self.credentials.invalid
732
733 def authorize_url(self):
734 """Returns the URL to start the OAuth dance.
735
736 Must only be called from with a webapp.RequestHandler subclassed method
737 that had been decorated with either @oauth_required or @oauth_aware.
738 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400739 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500740 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400741
742 def http(self):
743 """Returns an authorized http instance.
744
745 Must only be called from within an @oauth_required decorated method, or
746 from within an @oauth_aware decorated method where has_credentials()
747 returns True.
748 """
749 return self.credentials.authorize(httplib2.Http())
750
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400751 @property
752 def callback_path(self):
753 """The absolute path where the callback will occur.
754
755 Note this is the absolute path, not the absolute URI, that will be
756 calculated by the decorator at runtime. See callback_handler() for how this
757 should be used.
758
759 Returns:
760 The callback path as a string.
761 """
762 return self._callback_path
763
764
765 def callback_handler(self):
766 """RequestHandler for the OAuth 2.0 redirect callback.
767
768 Usage:
769 app = webapp.WSGIApplication([
770 ('/index', MyIndexHandler),
771 ...,
772 (decorator.callback_path, decorator.callback_handler())
773 ])
774
775 Returns:
776 A webapp.RequestHandler that handles the redirect back from the
777 server during the OAuth 2.0 dance.
778 """
779 decorator = self
780
781 class OAuth2Handler(webapp.RequestHandler):
782 """Handler for the redirect_uri of the OAuth 2.0 dance."""
783
784 @login_required
785 def get(self):
786 error = self.request.get('error')
787 if error:
788 errormsg = self.request.get('error_description', error)
789 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400790 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400791 else:
792 user = users.get_current_user()
793 decorator._create_flow(self)
794 credentials = decorator.flow.step2_exchange(self.request.params)
795 StorageByKeyName(
796 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400797 redirect_uri = _parse_state_value(str(self.request.get('state')),
798 user)
Joe Gregoriocda87522013-02-22 16:22:48 -0500799
800 if decorator._token_response_param and credentials.token_response:
801 resp_json = simplejson.dumps(credentials.token_response)
802 redirect_uri = discovery._add_query_parameter(
803 redirect_uri, decorator._token_response_param, resp_json)
804
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400805 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400806
807 return OAuth2Handler
808
809 def callback_application(self):
810 """WSGI application for handling the OAuth 2.0 redirect callback.
811
812 If you need finer grained control use `callback_handler` which returns just
813 the webapp.RequestHandler.
814
815 Returns:
816 A webapp.WSGIApplication that handles the redirect back from the
817 server during the OAuth 2.0 dance.
818 """
819 return webapp.WSGIApplication([
820 (self.callback_path, self.callback_handler())
821 ])
822
Joe Gregorio432f17e2011-05-22 23:18:00 -0400823
Joe Gregoriof08a4982011-10-07 13:11:16 -0400824class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
825 """An OAuth2Decorator that builds from a clientsecrets file.
826
827 Uses a clientsecrets file as the source for all the information when
828 constructing an OAuth2Decorator.
829
830 Example:
831
832 decorator = OAuth2DecoratorFromClientSecrets(
833 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500834 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400835
836
837 class MainHandler(webapp.RequestHandler):
838
839 @decorator.oauth_required
840 def get(self):
841 http = decorator.http()
842 # http is authorized with the user's Credentials and can be used
843 # in API calls
844 """
845
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400846 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400847 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400848 """Constructor
849
850 Args:
851 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500852 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400853 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400854 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400855 clientsecrets file is missing or invalid. The message may contain HTML
856 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400857 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400858 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400859 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400860 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400861 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
862 if client_type not in [
863 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
864 raise InvalidClientSecretsError(
865 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800866 constructor_kwargs = {
867 'auth_uri': client_info['auth_uri'],
868 'token_uri': client_info['token_uri'],
869 'message': message,
870 }
871 revoke_uri = client_info.get('revoke_uri')
872 if revoke_uri is not None:
873 constructor_kwargs['revoke_uri'] = revoke_uri
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400874 super(OAuth2DecoratorFromClientSecrets, self).__init__(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800875 client_info['client_id'], client_info['client_secret'],
876 scope, **constructor_kwargs)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400877 if message is not None:
878 self._message = message
879 else:
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800880 self._message = 'Please configure your application for OAuth 2.0.'
Joe Gregoriof08a4982011-10-07 13:11:16 -0400881
882
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400883@util.positional(2)
884def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400885 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400886 """Creates an OAuth2Decorator populated from a clientsecrets file.
887
888 Args:
889 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400890 scope: string or list of strings, scope(s) of the credentials being
891 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400892 message: string, A friendly string to display to the user if the
893 clientsecrets file is missing or invalid. The message may contain HTML and
894 will be presented on the web interface for any method that uses the
895 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400896 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400897 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400898
899 Returns: An OAuth2Decorator
900
901 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400902 return OAuth2DecoratorFromClientSecrets(filename, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800903 message=message, cache=cache)