blob: 95414c9f67f09cfb44c387a41aa3d3f2f503b403 [file] [log] [blame]
Joe Gregorio695fdc12011-01-16 16:46:55 -05001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Utilities for Google App Engine
16
Joe Gregorio7c22ab22011-02-16 15:32:39 -050017Utilities for making it easier to use OAuth 2.0 on Google App Engine.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorio1daa71b2011-09-15 18:12:14 -040022import base64
Joe Gregorio77254c12012-08-27 14:13:22 -040023import cgi
Joe Gregorio432f17e2011-05-22 23:18:00 -040024import httplib2
Joe Gregorio1daa71b2011-09-15 18:12:14 -040025import logging
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040026import os
Joe Gregorio695fdc12011-01-16 16:46:55 -050027import pickle
JacobMoshenko8e905102011-06-20 09:53:10 -040028import time
JacobMoshenko8e905102011-06-20 09:53:10 -040029
Joe Gregoriod84d6b82012-02-28 14:53:00 -050030from google.appengine.api import app_identity
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040031from google.appengine.api import memcache
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040032from google.appengine.api import users
Joe Gregorio432f17e2011-05-22 23:18:00 -040033from google.appengine.ext import db
dhermes@google.com47154822012-11-26 10:44:09 -080034from google.appengine.ext import ndb
Joe Gregorio432f17e2011-05-22 23:18:00 -040035from google.appengine.ext import webapp
36from google.appengine.ext.webapp.util import login_required
37from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040038from oauth2client import clientsecrets
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040039from oauth2client import util
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040040from oauth2client import xsrfutil
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040041from oauth2client.anyjson import simplejson
42from oauth2client.client import AccessTokenRefreshError
43from oauth2client.client import AssertionCredentials
44from oauth2client.client import Credentials
45from oauth2client.client import Flow
46from oauth2client.client import OAuth2WebServerFlow
47from oauth2client.client import Storage
Joe Gregorioa19f3a72012-07-11 15:35:35 -040048
49logger = logging.getLogger(__name__)
50
Joe Gregorio432f17e2011-05-22 23:18:00 -040051OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050052
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040053XSRF_MEMCACHE_ID = 'xsrf_secret_key'
54
JacobMoshenko8e905102011-06-20 09:53:10 -040055
Joe Gregorio77254c12012-08-27 14:13:22 -040056def _safe_html(s):
57 """Escape text to make it safe to display.
58
59 Args:
60 s: string, The text to escape.
61
62 Returns:
63 The escaped text as a string.
64 """
65 return cgi.escape(s, quote=1).replace("'", ''')
66
67
Joe Gregoriof08a4982011-10-07 13:11:16 -040068class InvalidClientSecretsError(Exception):
69 """The client_secrets.json file is malformed or missing required fields."""
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040070
71
72class InvalidXsrfTokenError(Exception):
73 """The XSRF token is invalid or expired."""
74
75
76class SiteXsrfSecretKey(db.Model):
77 """Storage for the sites XSRF secret key.
78
79 There will only be one instance stored of this model, the one used for the
dhermes@google.com47154822012-11-26 10:44:09 -080080 site.
81 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040082 secret = db.StringProperty()
83
84
dhermes@google.com47154822012-11-26 10:44:09 -080085class SiteXsrfSecretKeyNDB(ndb.Model):
86 """NDB Model for storage for the sites XSRF secret key.
87
88 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
89 interchangeably. This simply provides an NDB model for interacting with the
90 same data the DB model interacts with.
91
92 There should only be one instance stored of this model, the one used for the
93 site.
94 """
95 secret = ndb.StringProperty()
96
97 @classmethod
98 def _get_kind(cls):
99 """Return the kind name for this class."""
100 return 'SiteXsrfSecretKey'
101
102
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400103def _generate_new_xsrf_secret_key():
104 """Returns a random XSRF secret key.
105 """
106 return os.urandom(16).encode("hex")
107
108
109def xsrf_secret_key():
110 """Return the secret key for use for XSRF protection.
111
112 If the Site entity does not have a secret key, this method will also create
113 one and persist it.
114
115 Returns:
116 The secret key.
117 """
118 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
119 if not secret:
120 # Load the one and only instance of SiteXsrfSecretKey.
121 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
122 if not model.secret:
123 model.secret = _generate_new_xsrf_secret_key()
124 model.put()
125 secret = model.secret
126 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
127
128 return str(secret)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400129
130
JacobMoshenko8e905102011-06-20 09:53:10 -0400131class AppAssertionCredentials(AssertionCredentials):
132 """Credentials object for App Engine Assertion Grants
133
134 This object will allow an App Engine application to identify itself to Google
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400135 and other OAuth 2.0 servers that can verify assertions. It can be used for the
136 purpose of accessing data stored under an account assigned to the App Engine
137 application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -0400138
139 This credential does not require a flow to instantiate because it represents
140 a two legged flow, and therefore has all of the required information to
141 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -0400142 """
143
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400144 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500145 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -0400146 """Constructor for AppAssertionCredentials
147
148 Args:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500149 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriofd08e432012-08-09 14:17:41 -0400150 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400151 """
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500152 self.scope = util.scopes_to_string(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400153
154 super(AppAssertionCredentials, self).__init__(
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400155 'ignored' # assertion_type is ignore in this subclass.
156 )
JacobMoshenko8e905102011-06-20 09:53:10 -0400157
Joe Gregorio562b7312011-09-15 09:06:38 -0400158 @classmethod
159 def from_json(cls, json):
160 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500161 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400162
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500163 def _refresh(self, http_request):
164 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400165
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500166 Since the underlying App Engine app_identity implementation does its own
167 caching we can skip all the storage hoops and just to a refresh using the
168 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400169
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500170 Args:
171 http_request: callable, a callable that matches the method signature of
172 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400173
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500174 Raises:
175 AccessTokenRefreshError: When the refresh fails.
176 """
177 try:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500178 scopes = self.scope.split()
179 (token, _) = app_identity.get_access_token(scopes)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500180 except app_identity.Error, e:
181 raise AccessTokenRefreshError(str(e))
182 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400183
184
Joe Gregorio695fdc12011-01-16 16:46:55 -0500185class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500186 """App Engine datastore Property for Flow.
187
dhermes@google.com47154822012-11-26 10:44:09 -0800188 Utility property that allows easy storage and retrieval of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500189 oauth2client.Flow"""
190
191 # Tell what the user type is.
192 data_type = Flow
193
194 # For writing to datastore.
195 def get_value_for_datastore(self, model_instance):
196 flow = super(FlowProperty,
197 self).get_value_for_datastore(model_instance)
198 return db.Blob(pickle.dumps(flow))
199
200 # For reading from datastore.
201 def make_value_from_datastore(self, value):
202 if value is None:
203 return None
204 return pickle.loads(value)
205
206 def validate(self, value):
207 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400208 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500209 'to a FlowThreeLegged instance (%s)' %
210 (self.name, value))
211 return super(FlowProperty, self).validate(value)
212
213 def empty(self, value):
214 return not value
215
216
dhermes@google.com47154822012-11-26 10:44:09 -0800217class FlowNDBProperty(ndb.PickleProperty):
218 """App Engine NDB datastore Property for Flow.
219
220 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
221 PickleProperty inherits from BlobProperty, the underlying representation of
222 the data in the datastore will be the same as in the DB case.
223
224 Utility property that allows easy storage and retrieval of an
225 oauth2client.Flow
226 """
227
228 def _validate(self, value):
229 """Validates a value as a proper Flow object.
230
231 Args:
232 value: A value to be set on the property.
233
234 Raises:
235 TypeError if the value is not an instance of Flow.
236 """
237 logger.info('validate: Got type %s', type(value))
238 if value is not None and not isinstance(value, Flow):
239 raise TypeError('Property %s must be convertible to a flow '
240 'instance; received: %s.' % (self._name, value))
241
242
Joe Gregorio695fdc12011-01-16 16:46:55 -0500243class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500244 """App Engine datastore Property for Credentials.
245
246 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500247 oath2client.Credentials
248 """
249
250 # Tell what the user type is.
251 data_type = Credentials
252
253 # For writing to datastore.
254 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400255 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500256 cred = super(CredentialsProperty,
257 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400258 if cred is None:
259 cred = ''
260 else:
261 cred = cred.to_json()
262 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500263
264 # For reading from datastore.
265 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400266 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500267 if value is None:
268 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400269 if len(value) == 0:
270 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400271 try:
272 credentials = Credentials.new_from_json(value)
273 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400274 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400275 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500276
277 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400278 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400279 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500280 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400281 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400282 'to a Credentials instance (%s)' %
283 (self.name, value))
284 #if value is not None and not isinstance(value, Credentials):
285 # return None
286 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500287
288
dhermes@google.com47154822012-11-26 10:44:09 -0800289# TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
290# and subclass mechanics to use new_from_dict, to_dict,
291# from_dict, etc.
292class CredentialsNDBProperty(ndb.BlobProperty):
293 """App Engine NDB datastore Property for Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500294
dhermes@google.com47154822012-11-26 10:44:09 -0800295 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
296 Since CredentialsProperty stores data as a blob and this inherits from
297 BlobProperty, the data in the datastore will be the same as in the DB case.
298
299 Utility property that allows easy storage and retrieval of Credentials and
300 subclasses.
301 """
302 def _validate(self, value):
303 """Validates a value as a proper credentials object.
304
305 Args:
306 value: A value to be set on the property.
307
308 Raises:
309 TypeError if the value is not an instance of Credentials.
310 """
311 logger.info('validate: Got type %s', type(value))
312 if value is not None and not isinstance(value, Credentials):
313 raise TypeError('Property %s must be convertible to a credentials '
314 'instance; received: %s.' % (self._name, value))
315
316 def _to_base_type(self, value):
317 """Converts our validated value to a JSON serialized string.
318
319 Args:
320 value: A value to be set in the datastore.
321
322 Returns:
323 A JSON serialized version of the credential, else '' if value is None.
324 """
325 if value is None:
326 return ''
327 else:
328 return value.to_json()
329
330 def _from_base_type(self, value):
331 """Converts our stored JSON string back to the desired type.
332
333 Args:
334 value: A value from the datastore to be converted to the desired type.
335
336 Returns:
337 A deserialized Credentials (or subclass) object, else None if the
338 value can't be parsed.
339 """
340 if not value:
341 return None
342 try:
343 # Uses the from_json method of the implied class of value
344 credentials = Credentials.new_from_json(value)
345 except ValueError:
346 credentials = None
347 return credentials
348
349
350class StorageByKeyName(Storage):
351 """Store and retrieve a credential to and from the App Engine datastore.
352
353 This Storage helper presumes the Credentials have been stored as a
354 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
355 that entities are stored by key_name.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500356 """
357
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400358 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400359 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500360 """Constructor for Storage.
361
362 Args:
dhermes@google.com47154822012-11-26 10:44:09 -0800363 model: db.Model or ndb.Model, model class
Joe Gregorio695fdc12011-01-16 16:46:55 -0500364 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400365 property_name: string, name of the property that is a CredentialsProperty
dhermes@google.com47154822012-11-26 10:44:09 -0800366 or CredentialsNDBProperty.
367 cache: memcache, a write-through cache to put in front of the datastore.
368 If the model you are using is an NDB model, using a cache will be
369 redundant since the model uses an instance cache and memcache for you.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500370 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500371 self._model = model
372 self._key_name = key_name
373 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400374 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500375
dhermes@google.com47154822012-11-26 10:44:09 -0800376 def _is_ndb(self):
377 """Determine whether the model of the instance is an NDB model.
378
379 Returns:
380 Boolean indicating whether or not the model is an NDB or DB model.
381 """
382 # issubclass will fail if one of the arguments is not a class, only need
383 # worry about new-style classes since ndb and db models are new-style
384 if isinstance(self._model, type):
385 if issubclass(self._model, ndb.Model):
386 return True
387 elif issubclass(self._model, db.Model):
388 return False
389
390 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
391
392 def _get_entity(self):
393 """Retrieve entity from datastore.
394
395 Uses a different model method for db or ndb models.
396
397 Returns:
398 Instance of the model corresponding to the current storage object
399 and stored using the key name of the storage object.
400 """
401 if self._is_ndb():
402 return self._model.get_by_id(self._key_name)
403 else:
404 return self._model.get_by_key_name(self._key_name)
405
406 def _delete_entity(self):
407 """Delete entity from datastore.
408
409 Attempts to delete using the key_name stored on the object, whether or not
410 the given key is in the datastore.
411 """
412 if self._is_ndb():
413 ndb.Key(self._model, self._key_name).delete()
414 else:
415 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
416 db.delete(entity_key)
417
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400418 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500419 """Retrieve Credential from datastore.
420
421 Returns:
422 oauth2client.Credentials
423 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400424 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400425 json = self._cache.get(self._key_name)
426 if json:
427 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500428
dhermes@google.com47154822012-11-26 10:44:09 -0800429 credentials = None
430 entity = self._get_entity()
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500431 if entity is not None:
dhermes@google.com47154822012-11-26 10:44:09 -0800432 credentials = getattr(entity, self._property_name)
433 if credentials and hasattr(credentials, 'set_store'):
434 credentials.set_store(self)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500435 if self._cache:
dhermes@google.com47154822012-11-26 10:44:09 -0800436 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400437
dhermes@google.com47154822012-11-26 10:44:09 -0800438 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500439
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400440 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500441 """Write a Credentials to the datastore.
442
443 Args:
444 credentials: Credentials, the credentials to store.
445 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500446 entity = self._model.get_or_insert(self._key_name)
447 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500448 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400449 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400450 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400451
Joe Gregorioec75dc12012-02-06 13:40:42 -0500452 def locked_delete(self):
453 """Delete Credential from datastore."""
454
455 if self._cache:
456 self._cache.delete(self._key_name)
457
dhermes@google.com47154822012-11-26 10:44:09 -0800458 self._delete_entity()
Joe Gregorioec75dc12012-02-06 13:40:42 -0500459
Joe Gregorio432f17e2011-05-22 23:18:00 -0400460
461class CredentialsModel(db.Model):
462 """Storage for OAuth 2.0 Credentials
463
464 Storage of the model is keyed by the user.user_id().
465 """
466 credentials = CredentialsProperty()
467
468
dhermes@google.com47154822012-11-26 10:44:09 -0800469class CredentialsNDBModel(ndb.Model):
470 """NDB Model for storage of OAuth 2.0 Credentials
471
472 Since this model uses the same kind as CredentialsModel and has a property
473 which can serialize and deserialize Credentials correctly, it can be used
474 interchangeably with a CredentialsModel to access, insert and delete the same
475 entities. This simply provides an NDB model for interacting with the
476 same data the DB model interacts with.
477
478 Storage of the model is keyed by the user.user_id().
479 """
480 credentials = CredentialsNDBProperty()
481
482 @classmethod
483 def _get_kind(cls):
484 """Return the kind name for this class."""
485 return 'CredentialsModel'
486
487
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400488def _build_state_value(request_handler, user):
489 """Composes the value for the 'state' parameter.
490
491 Packs the current request URI and an XSRF token into an opaque string that
492 can be passed to the authentication server via the 'state' parameter.
493
494 Args:
495 request_handler: webapp.RequestHandler, The request.
496 user: google.appengine.api.users.User, The current user.
497
498 Returns:
499 The state value as a string.
500 """
501 uri = request_handler.request.url
502 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
503 action_id=str(uri))
504 return uri + ':' + token
505
506
507def _parse_state_value(state, user):
508 """Parse the value of the 'state' parameter.
509
510 Parses the value and validates the XSRF token in the state parameter.
511
512 Args:
513 state: string, The value of the state parameter.
514 user: google.appengine.api.users.User, The current user.
515
516 Raises:
517 InvalidXsrfTokenError: if the XSRF token is invalid.
518
519 Returns:
520 The redirect URI.
521 """
522 uri, token = state.rsplit(':', 1)
523 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
524 action_id=uri):
525 raise InvalidXsrfTokenError()
526
527 return uri
528
529
Joe Gregorio432f17e2011-05-22 23:18:00 -0400530class OAuth2Decorator(object):
531 """Utility for making OAuth 2.0 easier.
532
533 Instantiate and then use with oauth_required or oauth_aware
534 as decorators on webapp.RequestHandler methods.
535
536 Example:
537
538 decorator = OAuth2Decorator(
539 client_id='837...ent.com',
540 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500541 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400542
543
544 class MainHandler(webapp.RequestHandler):
545
546 @decorator.oauth_required
547 def get(self):
548 http = decorator.http()
549 # http is authorized with the user's Credentials and can be used
550 # in API calls
551
552 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400553
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400554 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400555 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400556 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400557 token_uri='https://accounts.google.com/o/oauth2/token',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100558 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400559 message=None,
560 callback_path='/oauth2callback',
561 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400562
563 """Constructor for OAuth2Decorator
564
565 Args:
566 client_id: string, client identifier.
567 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500568 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400569 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400570 auth_uri: string, URI for authorization endpoint. For convenience
571 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
572 token_uri: string, URI for token endpoint. For convenience
573 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100574 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400575 message: Message to display if there are problems with the OAuth 2.0
576 configuration. The message may contain HTML and will be presented on the
577 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400578 callback_path: string, The absolute path to use as the callback URI. Note
579 that this must match up with the URI given when registering the
580 application in the APIs Console.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500581 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
582 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400583 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400584 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400585 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400586 self._client_id = client_id
587 self._client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500588 self._scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400589 self._auth_uri = auth_uri
590 self._token_uri = token_uri
591 self._user_agent = user_agent
592 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400593 self._message = message
594 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400595 self._callback_path = callback_path
Joe Gregoriof08a4982011-10-07 13:11:16 -0400596
597 def _display_error_message(self, request_handler):
598 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400599 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400600 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400601
602 def oauth_required(self, method):
603 """Decorator that starts the OAuth 2.0 dance.
604
605 Starts the OAuth dance for the logged in user if they haven't already
606 granted access for this application.
607
608 Args:
609 method: callable, to be decorated method of a webapp.RequestHandler
610 instance.
611 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400612
Joe Gregorio17774972012-03-01 11:11:59 -0500613 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400614 if self._in_error:
615 self._display_error_message(request_handler)
616 return
617
Joe Gregoriof427c532011-06-13 09:35:26 -0400618 user = users.get_current_user()
619 # Don't use @login_decorator as this could be used in a POST request.
620 if not user:
621 request_handler.redirect(users.create_login_url(
622 request_handler.request.uri))
623 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400624
625 self._create_flow(request_handler)
626
Joe Gregorio432f17e2011-05-22 23:18:00 -0400627 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400628 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400629 self.credentials = StorageByKeyName(
630 CredentialsModel, user.user_id(), 'credentials').get()
631
632 if not self.has_credentials():
633 return request_handler.redirect(self.authorize_url())
634 try:
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400635 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400636 except AccessTokenRefreshError:
637 return request_handler.redirect(self.authorize_url())
638
639 return check_oauth
640
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400641 def _create_flow(self, request_handler):
642 """Create the Flow object.
643
644 The Flow is calculated lazily since we don't know where this app is
645 running until it receives a request, at which point redirect_uri can be
646 calculated and then the Flow object can be constructed.
647
648 Args:
649 request_handler: webapp.RequestHandler, the request handler.
650 """
651 if self.flow is None:
652 redirect_uri = request_handler.request.relative_url(
653 self._callback_path) # Usually /oauth2callback
654 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
655 self._scope, redirect_uri=redirect_uri,
656 user_agent=self._user_agent,
657 auth_uri=self._auth_uri,
658 token_uri=self._token_uri, **self._kwargs)
659
660
Joe Gregorio432f17e2011-05-22 23:18:00 -0400661 def oauth_aware(self, method):
662 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
663
664 Does all the setup for the OAuth dance, but doesn't initiate it.
665 This decorator is useful if you want to create a page that knows
666 whether or not the user has granted access to this application.
667 From within a method decorated with @oauth_aware the has_credentials()
668 and authorize_url() methods can be called.
669
670 Args:
671 method: callable, to be decorated method of a webapp.RequestHandler
672 instance.
673 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400674
Joe Gregorio17774972012-03-01 11:11:59 -0500675 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400676 if self._in_error:
677 self._display_error_message(request_handler)
678 return
679
Joe Gregoriof427c532011-06-13 09:35:26 -0400680 user = users.get_current_user()
681 # Don't use @login_decorator as this could be used in a POST request.
682 if not user:
683 request_handler.redirect(users.create_login_url(
684 request_handler.request.uri))
685 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400686
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400687 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400688
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400689 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400690 self.credentials = StorageByKeyName(
691 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400692 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400693 return setup_oauth
694
695 def has_credentials(self):
696 """True if for the logged in user there are valid access Credentials.
697
698 Must only be called from with a webapp.RequestHandler subclassed method
699 that had been decorated with either @oauth_required or @oauth_aware.
700 """
701 return self.credentials is not None and not self.credentials.invalid
702
703 def authorize_url(self):
704 """Returns the URL to start the OAuth dance.
705
706 Must only be called from with a webapp.RequestHandler subclassed method
707 that had been decorated with either @oauth_required or @oauth_aware.
708 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400709 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500710 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400711
712 def http(self):
713 """Returns an authorized http instance.
714
715 Must only be called from within an @oauth_required decorated method, or
716 from within an @oauth_aware decorated method where has_credentials()
717 returns True.
718 """
719 return self.credentials.authorize(httplib2.Http())
720
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400721 @property
722 def callback_path(self):
723 """The absolute path where the callback will occur.
724
725 Note this is the absolute path, not the absolute URI, that will be
726 calculated by the decorator at runtime. See callback_handler() for how this
727 should be used.
728
729 Returns:
730 The callback path as a string.
731 """
732 return self._callback_path
733
734
735 def callback_handler(self):
736 """RequestHandler for the OAuth 2.0 redirect callback.
737
738 Usage:
739 app = webapp.WSGIApplication([
740 ('/index', MyIndexHandler),
741 ...,
742 (decorator.callback_path, decorator.callback_handler())
743 ])
744
745 Returns:
746 A webapp.RequestHandler that handles the redirect back from the
747 server during the OAuth 2.0 dance.
748 """
749 decorator = self
750
751 class OAuth2Handler(webapp.RequestHandler):
752 """Handler for the redirect_uri of the OAuth 2.0 dance."""
753
754 @login_required
755 def get(self):
756 error = self.request.get('error')
757 if error:
758 errormsg = self.request.get('error_description', error)
759 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400760 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400761 else:
762 user = users.get_current_user()
763 decorator._create_flow(self)
764 credentials = decorator.flow.step2_exchange(self.request.params)
765 StorageByKeyName(
766 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400767 redirect_uri = _parse_state_value(str(self.request.get('state')),
768 user)
769 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400770
771 return OAuth2Handler
772
773 def callback_application(self):
774 """WSGI application for handling the OAuth 2.0 redirect callback.
775
776 If you need finer grained control use `callback_handler` which returns just
777 the webapp.RequestHandler.
778
779 Returns:
780 A webapp.WSGIApplication that handles the redirect back from the
781 server during the OAuth 2.0 dance.
782 """
783 return webapp.WSGIApplication([
784 (self.callback_path, self.callback_handler())
785 ])
786
Joe Gregorio432f17e2011-05-22 23:18:00 -0400787
Joe Gregoriof08a4982011-10-07 13:11:16 -0400788class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
789 """An OAuth2Decorator that builds from a clientsecrets file.
790
791 Uses a clientsecrets file as the source for all the information when
792 constructing an OAuth2Decorator.
793
794 Example:
795
796 decorator = OAuth2DecoratorFromClientSecrets(
797 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500798 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400799
800
801 class MainHandler(webapp.RequestHandler):
802
803 @decorator.oauth_required
804 def get(self):
805 http = decorator.http()
806 # http is authorized with the user's Credentials and can be used
807 # in API calls
808 """
809
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400810 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400811 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400812 """Constructor
813
814 Args:
815 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500816 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400817 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400818 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400819 clientsecrets file is missing or invalid. The message may contain HTML
820 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400821 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400822 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400823 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400824 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400825 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
826 if client_type not in [
827 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
828 raise InvalidClientSecretsError(
829 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
830 super(OAuth2DecoratorFromClientSecrets, self).__init__(
831 client_info['client_id'],
832 client_info['client_secret'],
833 scope,
834 auth_uri=client_info['auth_uri'],
835 token_uri=client_info['token_uri'],
836 message=message)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400837 if message is not None:
838 self._message = message
839 else:
840 self._message = "Please configure your application for OAuth 2.0"
841
842
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400843@util.positional(2)
844def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400845 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400846 """Creates an OAuth2Decorator populated from a clientsecrets file.
847
848 Args:
849 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400850 scope: string or list of strings, scope(s) of the credentials being
851 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400852 message: string, A friendly string to display to the user if the
853 clientsecrets file is missing or invalid. The message may contain HTML and
854 will be presented on the web interface for any method that uses the
855 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400856 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400857 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400858
859 Returns: An OAuth2Decorator
860
861 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400862 return OAuth2DecoratorFromClientSecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400863 message=message, cache=cache)