blob: 799e063357bf7681fdedab06456582ac69235ac8 [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
Joe Gregoriob8b6fea2013-05-16 15:52:57 -040028import threading
JacobMoshenko8e905102011-06-20 09:53:10 -040029import time
JacobMoshenko8e905102011-06-20 09:53:10 -040030
Joe Gregoriod84d6b82012-02-28 14:53:00 -050031from google.appengine.api import app_identity
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040032from google.appengine.api import memcache
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040033from google.appengine.api import users
Joe Gregorio432f17e2011-05-22 23:18:00 -040034from google.appengine.ext import db
35from google.appengine.ext import webapp
36from google.appengine.ext.webapp.util import login_required
37from google.appengine.ext.webapp.util import run_wsgi_app
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -080038from oauth2client import GOOGLE_AUTH_URI
39from oauth2client import GOOGLE_REVOKE_URI
40from oauth2client import GOOGLE_TOKEN_URI
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040041from oauth2client import clientsecrets
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040042from oauth2client import util
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040043from oauth2client import xsrfutil
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040044from oauth2client.anyjson import simplejson
45from oauth2client.client import AccessTokenRefreshError
46from oauth2client.client import AssertionCredentials
47from oauth2client.client import Credentials
48from oauth2client.client import Flow
49from oauth2client.client import OAuth2WebServerFlow
50from oauth2client.client import Storage
Joe Gregorioa19f3a72012-07-11 15:35:35 -040051
Joe Gregorio78787b62013-02-08 15:36:21 -050052# TODO(dhermes): Resolve import issue.
53# This is a temporary fix for a Google internal issue.
54try:
55 from google.appengine.ext import ndb
56except ImportError:
57 ndb = None
58
Joe Gregoriocda87522013-02-22 16:22:48 -050059
Joe Gregorioa19f3a72012-07-11 15:35:35 -040060logger = logging.getLogger(__name__)
61
Joe Gregorio432f17e2011-05-22 23:18:00 -040062OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050063
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040064XSRF_MEMCACHE_ID = 'xsrf_secret_key'
65
JacobMoshenko8e905102011-06-20 09:53:10 -040066
Joe Gregorio77254c12012-08-27 14:13:22 -040067def _safe_html(s):
68 """Escape text to make it safe to display.
69
70 Args:
71 s: string, The text to escape.
72
73 Returns:
74 The escaped text as a string.
75 """
76 return cgi.escape(s, quote=1).replace("'", ''')
77
78
Joe Gregoriof08a4982011-10-07 13:11:16 -040079class InvalidClientSecretsError(Exception):
80 """The client_secrets.json file is malformed or missing required fields."""
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040081
82
83class InvalidXsrfTokenError(Exception):
84 """The XSRF token is invalid or expired."""
85
86
87class SiteXsrfSecretKey(db.Model):
88 """Storage for the sites XSRF secret key.
89
90 There will only be one instance stored of this model, the one used for the
dhermes@google.com47154822012-11-26 10:44:09 -080091 site.
92 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040093 secret = db.StringProperty()
94
Joe Gregorio78787b62013-02-08 15:36:21 -050095if ndb is not None:
96 class SiteXsrfSecretKeyNDB(ndb.Model):
97 """NDB Model for storage for the sites XSRF secret key.
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040098
Joe Gregorio78787b62013-02-08 15:36:21 -050099 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
100 interchangeably. This simply provides an NDB model for interacting with the
101 same data the DB model interacts with.
dhermes@google.com47154822012-11-26 10:44:09 -0800102
Joe Gregorio78787b62013-02-08 15:36:21 -0500103 There should only be one instance stored of this model, the one used for the
104 site.
105 """
106 secret = ndb.StringProperty()
dhermes@google.com47154822012-11-26 10:44:09 -0800107
Joe Gregorio78787b62013-02-08 15:36:21 -0500108 @classmethod
109 def _get_kind(cls):
110 """Return the kind name for this class."""
111 return 'SiteXsrfSecretKey'
dhermes@google.com47154822012-11-26 10:44:09 -0800112
113
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400114def _generate_new_xsrf_secret_key():
115 """Returns a random XSRF secret key.
116 """
117 return os.urandom(16).encode("hex")
118
119
120def xsrf_secret_key():
121 """Return the secret key for use for XSRF protection.
122
123 If the Site entity does not have a secret key, this method will also create
124 one and persist it.
125
126 Returns:
127 The secret key.
128 """
129 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
130 if not secret:
131 # Load the one and only instance of SiteXsrfSecretKey.
132 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
133 if not model.secret:
134 model.secret = _generate_new_xsrf_secret_key()
135 model.put()
136 secret = model.secret
137 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
138
139 return str(secret)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400140
141
JacobMoshenko8e905102011-06-20 09:53:10 -0400142class AppAssertionCredentials(AssertionCredentials):
143 """Credentials object for App Engine Assertion Grants
144
145 This object will allow an App Engine application to identify itself to Google
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400146 and other OAuth 2.0 servers that can verify assertions. It can be used for the
147 purpose of accessing data stored under an account assigned to the App Engine
148 application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -0400149
150 This credential does not require a flow to instantiate because it represents
151 a two legged flow, and therefore has all of the required information to
152 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -0400153 """
154
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400155 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500156 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -0400157 """Constructor for AppAssertionCredentials
158
159 Args:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500160 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriofd08e432012-08-09 14:17:41 -0400161 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400162 """
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500163 self.scope = util.scopes_to_string(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400164
dhermes@google.com2cc09382013-02-11 08:42:18 -0800165 # Assertion type is no longer used, but still in the parent class signature.
166 super(AppAssertionCredentials, self).__init__(None)
JacobMoshenko8e905102011-06-20 09:53:10 -0400167
Joe Gregorio562b7312011-09-15 09:06:38 -0400168 @classmethod
169 def from_json(cls, json):
170 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500171 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400172
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500173 def _refresh(self, http_request):
174 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400175
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500176 Since the underlying App Engine app_identity implementation does its own
177 caching we can skip all the storage hoops and just to a refresh using the
178 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400179
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500180 Args:
181 http_request: callable, a callable that matches the method signature of
182 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400183
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500184 Raises:
185 AccessTokenRefreshError: When the refresh fails.
186 """
187 try:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500188 scopes = self.scope.split()
189 (token, _) = app_identity.get_access_token(scopes)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500190 except app_identity.Error, e:
191 raise AccessTokenRefreshError(str(e))
192 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400193
194
Joe Gregorio695fdc12011-01-16 16:46:55 -0500195class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500196 """App Engine datastore Property for Flow.
197
dhermes@google.com47154822012-11-26 10:44:09 -0800198 Utility property that allows easy storage and retrieval of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500199 oauth2client.Flow"""
200
201 # Tell what the user type is.
202 data_type = Flow
203
204 # For writing to datastore.
205 def get_value_for_datastore(self, model_instance):
206 flow = super(FlowProperty,
207 self).get_value_for_datastore(model_instance)
208 return db.Blob(pickle.dumps(flow))
209
210 # For reading from datastore.
211 def make_value_from_datastore(self, value):
212 if value is None:
213 return None
214 return pickle.loads(value)
215
216 def validate(self, value):
217 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400218 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500219 'to a FlowThreeLegged instance (%s)' %
220 (self.name, value))
221 return super(FlowProperty, self).validate(value)
222
223 def empty(self, value):
224 return not value
225
226
Joe Gregorio78787b62013-02-08 15:36:21 -0500227if ndb is not None:
228 class FlowNDBProperty(ndb.PickleProperty):
229 """App Engine NDB datastore Property for Flow.
dhermes@google.com47154822012-11-26 10:44:09 -0800230
Joe Gregorio78787b62013-02-08 15:36:21 -0500231 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
232 PickleProperty inherits from BlobProperty, the underlying representation of
233 the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800234
Joe Gregorio78787b62013-02-08 15:36:21 -0500235 Utility property that allows easy storage and retrieval of an
236 oauth2client.Flow
dhermes@google.com47154822012-11-26 10:44:09 -0800237 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500238
239 def _validate(self, value):
240 """Validates a value as a proper Flow object.
241
242 Args:
243 value: A value to be set on the property.
244
245 Raises:
246 TypeError if the value is not an instance of Flow.
247 """
248 logger.info('validate: Got type %s', type(value))
249 if value is not None and not isinstance(value, Flow):
250 raise TypeError('Property %s must be convertible to a flow '
251 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800252
253
Joe Gregorio695fdc12011-01-16 16:46:55 -0500254class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500255 """App Engine datastore Property for Credentials.
256
257 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500258 oath2client.Credentials
259 """
260
261 # Tell what the user type is.
262 data_type = Credentials
263
264 # For writing to datastore.
265 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400266 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500267 cred = super(CredentialsProperty,
268 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400269 if cred is None:
270 cred = ''
271 else:
272 cred = cred.to_json()
273 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500274
275 # For reading from datastore.
276 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400277 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500278 if value is None:
279 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400280 if len(value) == 0:
281 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400282 try:
283 credentials = Credentials.new_from_json(value)
284 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400285 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400286 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500287
288 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400289 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400290 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500291 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400292 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400293 'to a Credentials instance (%s)' %
294 (self.name, value))
295 #if value is not None and not isinstance(value, Credentials):
296 # return None
297 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500298
299
Joe Gregorio78787b62013-02-08 15:36:21 -0500300if ndb is not None:
301 # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
302 # and subclass mechanics to use new_from_dict, to_dict,
303 # from_dict, etc.
304 class CredentialsNDBProperty(ndb.BlobProperty):
305 """App Engine NDB datastore Property for Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500306
Joe Gregorio78787b62013-02-08 15:36:21 -0500307 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
308 Since CredentialsProperty stores data as a blob and this inherits from
309 BlobProperty, the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800310
Joe Gregorio78787b62013-02-08 15:36:21 -0500311 Utility property that allows easy storage and retrieval of Credentials and
312 subclasses.
dhermes@google.com47154822012-11-26 10:44:09 -0800313 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500314 def _validate(self, value):
315 """Validates a value as a proper credentials object.
dhermes@google.com47154822012-11-26 10:44:09 -0800316
Joe Gregorio78787b62013-02-08 15:36:21 -0500317 Args:
318 value: A value to be set on the property.
dhermes@google.com47154822012-11-26 10:44:09 -0800319
Joe Gregorio78787b62013-02-08 15:36:21 -0500320 Raises:
321 TypeError if the value is not an instance of Credentials.
322 """
323 logger.info('validate: Got type %s', type(value))
324 if value is not None and not isinstance(value, Credentials):
325 raise TypeError('Property %s must be convertible to a credentials '
326 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800327
Joe Gregorio78787b62013-02-08 15:36:21 -0500328 def _to_base_type(self, value):
329 """Converts our validated value to a JSON serialized string.
dhermes@google.com47154822012-11-26 10:44:09 -0800330
Joe Gregorio78787b62013-02-08 15:36:21 -0500331 Args:
332 value: A value to be set in the datastore.
dhermes@google.com47154822012-11-26 10:44:09 -0800333
Joe Gregorio78787b62013-02-08 15:36:21 -0500334 Returns:
335 A JSON serialized version of the credential, else '' if value is None.
336 """
337 if value is None:
338 return ''
339 else:
340 return value.to_json()
dhermes@google.com47154822012-11-26 10:44:09 -0800341
Joe Gregorio78787b62013-02-08 15:36:21 -0500342 def _from_base_type(self, value):
343 """Converts our stored JSON string back to the desired type.
344
345 Args:
346 value: A value from the datastore to be converted to the desired type.
347
348 Returns:
349 A deserialized Credentials (or subclass) object, else None if the
350 value can't be parsed.
351 """
352 if not value:
353 return None
354 try:
355 # Uses the from_json method of the implied class of value
356 credentials = Credentials.new_from_json(value)
357 except ValueError:
358 credentials = None
359 return credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800360
361
362class StorageByKeyName(Storage):
363 """Store and retrieve a credential to and from the App Engine datastore.
364
365 This Storage helper presumes the Credentials have been stored as a
366 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
367 that entities are stored by key_name.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500368 """
369
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400370 @util.positional(4)
Daniel Hermes58341a02013-04-05 09:58:16 -0700371 def __init__(self, model, key_name, property_name, cache=None, user=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500372 """Constructor for Storage.
373
374 Args:
dhermes@google.com47154822012-11-26 10:44:09 -0800375 model: db.Model or ndb.Model, model class
Joe Gregorio695fdc12011-01-16 16:46:55 -0500376 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400377 property_name: string, name of the property that is a CredentialsProperty
dhermes@google.com47154822012-11-26 10:44:09 -0800378 or CredentialsNDBProperty.
379 cache: memcache, a write-through cache to put in front of the datastore.
380 If the model you are using is an NDB model, using a cache will be
381 redundant since the model uses an instance cache and memcache for you.
Daniel Hermes58341a02013-04-05 09:58:16 -0700382 user: users.User object, optional. Can be used to grab user ID as a
383 key_name if no key name is specified.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500384 """
Daniel Hermes58341a02013-04-05 09:58:16 -0700385 if key_name is None:
386 if user is None:
387 raise ValueError('StorageByKeyName called with no key name or user.')
388 key_name = user.user_id()
389
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500390 self._model = model
391 self._key_name = key_name
392 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400393 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500394
dhermes@google.com47154822012-11-26 10:44:09 -0800395 def _is_ndb(self):
396 """Determine whether the model of the instance is an NDB model.
397
398 Returns:
399 Boolean indicating whether or not the model is an NDB or DB model.
400 """
401 # issubclass will fail if one of the arguments is not a class, only need
402 # worry about new-style classes since ndb and db models are new-style
403 if isinstance(self._model, type):
Joe Gregorio78787b62013-02-08 15:36:21 -0500404 if ndb is not None and issubclass(self._model, ndb.Model):
dhermes@google.com47154822012-11-26 10:44:09 -0800405 return True
406 elif issubclass(self._model, db.Model):
407 return False
408
409 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
410
411 def _get_entity(self):
412 """Retrieve entity from datastore.
413
414 Uses a different model method for db or ndb models.
415
416 Returns:
417 Instance of the model corresponding to the current storage object
418 and stored using the key name of the storage object.
419 """
420 if self._is_ndb():
421 return self._model.get_by_id(self._key_name)
422 else:
423 return self._model.get_by_key_name(self._key_name)
424
425 def _delete_entity(self):
426 """Delete entity from datastore.
427
428 Attempts to delete using the key_name stored on the object, whether or not
429 the given key is in the datastore.
430 """
431 if self._is_ndb():
432 ndb.Key(self._model, self._key_name).delete()
433 else:
434 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
435 db.delete(entity_key)
436
Joe Gregorioaa2f6c92013-12-13 11:09:24 -0500437 @db.non_transactional(allow_existing=True)
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400438 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500439 """Retrieve Credential from datastore.
440
441 Returns:
442 oauth2client.Credentials
443 """
Joe Gregorioe912d182013-08-06 11:30:44 -0400444 credentials = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400445 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400446 json = self._cache.get(self._key_name)
447 if json:
Joe Gregorioe912d182013-08-06 11:30:44 -0400448 credentials = Credentials.new_from_json(json)
449 if credentials is None:
450 entity = self._get_entity()
451 if entity is not None:
452 credentials = getattr(entity, self._property_name)
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
Joe Gregorioe912d182013-08-06 11:30:44 -0400456 if credentials and hasattr(credentials, 'set_store'):
457 credentials.set_store(self)
dhermes@google.com47154822012-11-26 10:44:09 -0800458 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500459
Joe Gregorioaa2f6c92013-12-13 11:09:24 -0500460 @db.non_transactional(allow_existing=True)
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400461 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500462 """Write a Credentials to the datastore.
463
464 Args:
465 credentials: Credentials, the credentials to store.
466 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500467 entity = self._model.get_or_insert(self._key_name)
468 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500469 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400470 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400471 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400472
Joe Gregorioaa2f6c92013-12-13 11:09:24 -0500473 @db.non_transactional(allow_existing=True)
Joe Gregorioec75dc12012-02-06 13:40:42 -0500474 def locked_delete(self):
475 """Delete Credential from datastore."""
476
477 if self._cache:
478 self._cache.delete(self._key_name)
479
dhermes@google.com47154822012-11-26 10:44:09 -0800480 self._delete_entity()
Joe Gregorioec75dc12012-02-06 13:40:42 -0500481
Joe Gregorio432f17e2011-05-22 23:18:00 -0400482
483class CredentialsModel(db.Model):
484 """Storage for OAuth 2.0 Credentials
485
486 Storage of the model is keyed by the user.user_id().
487 """
488 credentials = CredentialsProperty()
489
490
Joe Gregorio78787b62013-02-08 15:36:21 -0500491if ndb is not None:
492 class CredentialsNDBModel(ndb.Model):
493 """NDB Model for storage of OAuth 2.0 Credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800494
Joe Gregorio78787b62013-02-08 15:36:21 -0500495 Since this model uses the same kind as CredentialsModel and has a property
496 which can serialize and deserialize Credentials correctly, it can be used
497 interchangeably with a CredentialsModel to access, insert and delete the
498 same entities. This simply provides an NDB model for interacting with the
499 same data the DB model interacts with.
dhermes@google.com47154822012-11-26 10:44:09 -0800500
Joe Gregorio78787b62013-02-08 15:36:21 -0500501 Storage of the model is keyed by the user.user_id().
502 """
503 credentials = CredentialsNDBProperty()
dhermes@google.com47154822012-11-26 10:44:09 -0800504
Joe Gregorio78787b62013-02-08 15:36:21 -0500505 @classmethod
506 def _get_kind(cls):
507 """Return the kind name for this class."""
508 return 'CredentialsModel'
dhermes@google.com47154822012-11-26 10:44:09 -0800509
510
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400511def _build_state_value(request_handler, user):
512 """Composes the value for the 'state' parameter.
513
514 Packs the current request URI and an XSRF token into an opaque string that
515 can be passed to the authentication server via the 'state' parameter.
516
517 Args:
518 request_handler: webapp.RequestHandler, The request.
519 user: google.appengine.api.users.User, The current user.
520
521 Returns:
522 The state value as a string.
523 """
524 uri = request_handler.request.url
525 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
526 action_id=str(uri))
527 return uri + ':' + token
528
529
530def _parse_state_value(state, user):
531 """Parse the value of the 'state' parameter.
532
533 Parses the value and validates the XSRF token in the state parameter.
534
535 Args:
536 state: string, The value of the state parameter.
537 user: google.appengine.api.users.User, The current user.
538
539 Raises:
540 InvalidXsrfTokenError: if the XSRF token is invalid.
541
542 Returns:
543 The redirect URI.
544 """
545 uri, token = state.rsplit(':', 1)
546 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
547 action_id=uri):
548 raise InvalidXsrfTokenError()
549
550 return uri
551
552
Joe Gregorio432f17e2011-05-22 23:18:00 -0400553class OAuth2Decorator(object):
554 """Utility for making OAuth 2.0 easier.
555
556 Instantiate and then use with oauth_required or oauth_aware
557 as decorators on webapp.RequestHandler methods.
558
559 Example:
560
561 decorator = OAuth2Decorator(
562 client_id='837...ent.com',
563 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500564 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400565
566
567 class MainHandler(webapp.RequestHandler):
568
569 @decorator.oauth_required
570 def get(self):
571 http = decorator.http()
572 # http is authorized with the user's Credentials and can be used
573 # in API calls
574
575 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400576
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400577 def set_credentials(self, credentials):
578 self._tls.credentials = credentials
579
580 def get_credentials(self):
Joe Gregorioc211bec2013-08-06 12:06:54 -0400581 """A thread local Credentials object.
582
583 Returns:
584 A client.Credentials object, or None if credentials hasn't been set in
585 this thread yet, which may happen when calling has_credentials inside
586 oauth_aware.
587 """
588 return getattr(self._tls, 'credentials', None)
589
590 credentials = property(get_credentials, set_credentials)
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400591
592 def set_flow(self, flow):
593 self._tls.flow = flow
594
595 def get_flow(self):
Joe Gregorioc211bec2013-08-06 12:06:54 -0400596 """A thread local Flow object.
597
598 Returns:
599 A credentials.Flow object, or None if the flow hasn't been set in this
600 thread yet, which happens in _create_flow() since Flows are created
601 lazily.
602 """
603 return getattr(self._tls, 'flow', None)
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400604
605 flow = property(get_flow, set_flow)
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400606
607
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400608 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400609 def __init__(self, client_id, client_secret, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800610 auth_uri=GOOGLE_AUTH_URI,
611 token_uri=GOOGLE_TOKEN_URI,
612 revoke_uri=GOOGLE_REVOKE_URI,
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100613 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400614 message=None,
615 callback_path='/oauth2callback',
Joe Gregoriocda87522013-02-22 16:22:48 -0500616 token_response_param=None,
Daniel Hermes58341a02013-04-05 09:58:16 -0700617 _storage_class=StorageByKeyName,
618 _credentials_class=CredentialsModel,
619 _credentials_property_name='credentials',
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400620 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400621
622 """Constructor for OAuth2Decorator
623
624 Args:
625 client_id: string, client identifier.
626 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500627 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400628 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400629 auth_uri: string, URI for authorization endpoint. For convenience
630 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
631 token_uri: string, URI for token endpoint. For convenience
632 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800633 revoke_uri: string, URI for revoke endpoint. For convenience
634 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100635 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400636 message: Message to display if there are problems with the OAuth 2.0
637 configuration. The message may contain HTML and will be presented on the
638 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400639 callback_path: string, The absolute path to use as the callback URI. Note
640 that this must match up with the URI given when registering the
641 application in the APIs Console.
Joe Gregoriocda87522013-02-22 16:22:48 -0500642 token_response_param: string. If provided, the full JSON response
643 to the access token request will be encoded and included in this query
644 parameter in the callback URI. This is useful with providers (e.g.
645 wordpress.com) that include extra fields that the client may want.
Daniel Hermes58341a02013-04-05 09:58:16 -0700646 _storage_class: "Protected" keyword argument not typically provided to
647 this constructor. A storage class to aid in storing a Credentials object
648 for a user in the datastore. Defaults to StorageByKeyName.
649 _credentials_class: "Protected" keyword argument not typically provided to
650 this constructor. A db or ndb Model class to hold credentials. Defaults
651 to CredentialsModel.
652 _credentials_property_name: "Protected" keyword argument not typically
653 provided to this constructor. A string indicating the name of the field
654 on the _credentials_class where a Credentials object will be stored.
655 Defaults to 'credentials'.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500656 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
657 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400658 """
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400659 self._tls = threading.local()
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400660 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400661 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400662 self._client_id = client_id
663 self._client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500664 self._scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400665 self._auth_uri = auth_uri
666 self._token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800667 self._revoke_uri = revoke_uri
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400668 self._user_agent = user_agent
669 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400670 self._message = message
671 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400672 self._callback_path = callback_path
Joe Gregoriocda87522013-02-22 16:22:48 -0500673 self._token_response_param = token_response_param
Daniel Hermes58341a02013-04-05 09:58:16 -0700674 self._storage_class = _storage_class
675 self._credentials_class = _credentials_class
676 self._credentials_property_name = _credentials_property_name
Joe Gregoriof08a4982011-10-07 13:11:16 -0400677
678 def _display_error_message(self, request_handler):
679 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400680 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400681 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400682
683 def oauth_required(self, method):
684 """Decorator that starts the OAuth 2.0 dance.
685
686 Starts the OAuth dance for the logged in user if they haven't already
687 granted access for this application.
688
689 Args:
690 method: callable, to be decorated method of a webapp.RequestHandler
691 instance.
692 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400693
Joe Gregorio17774972012-03-01 11:11:59 -0500694 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400695 if self._in_error:
696 self._display_error_message(request_handler)
697 return
698
Joe Gregoriof427c532011-06-13 09:35:26 -0400699 user = users.get_current_user()
700 # Don't use @login_decorator as this could be used in a POST request.
701 if not user:
702 request_handler.redirect(users.create_login_url(
703 request_handler.request.uri))
704 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400705
706 self._create_flow(request_handler)
707
Joe Gregorio432f17e2011-05-22 23:18:00 -0400708 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400709 self.flow.params['state'] = _build_state_value(request_handler, user)
Daniel Hermes58341a02013-04-05 09:58:16 -0700710 self.credentials = self._storage_class(
711 self._credentials_class, None,
712 self._credentials_property_name, user=user).get()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400713
714 if not self.has_credentials():
715 return request_handler.redirect(self.authorize_url())
716 try:
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400717 resp = method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400718 except AccessTokenRefreshError:
719 return request_handler.redirect(self.authorize_url())
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400720 finally:
721 self.credentials = None
722 return resp
Joe Gregorio432f17e2011-05-22 23:18:00 -0400723
724 return check_oauth
725
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400726 def _create_flow(self, request_handler):
727 """Create the Flow object.
728
729 The Flow is calculated lazily since we don't know where this app is
730 running until it receives a request, at which point redirect_uri can be
731 calculated and then the Flow object can be constructed.
732
733 Args:
734 request_handler: webapp.RequestHandler, the request handler.
735 """
736 if self.flow is None:
737 redirect_uri = request_handler.request.relative_url(
738 self._callback_path) # Usually /oauth2callback
739 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
740 self._scope, redirect_uri=redirect_uri,
741 user_agent=self._user_agent,
742 auth_uri=self._auth_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800743 token_uri=self._token_uri,
744 revoke_uri=self._revoke_uri,
745 **self._kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400746
Joe Gregorio432f17e2011-05-22 23:18:00 -0400747 def oauth_aware(self, method):
748 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
749
750 Does all the setup for the OAuth dance, but doesn't initiate it.
751 This decorator is useful if you want to create a page that knows
752 whether or not the user has granted access to this application.
753 From within a method decorated with @oauth_aware the has_credentials()
754 and authorize_url() methods can be called.
755
756 Args:
757 method: callable, to be decorated method of a webapp.RequestHandler
758 instance.
759 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400760
Joe Gregorio17774972012-03-01 11:11:59 -0500761 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400762 if self._in_error:
763 self._display_error_message(request_handler)
764 return
765
Joe Gregoriof427c532011-06-13 09:35:26 -0400766 user = users.get_current_user()
767 # Don't use @login_decorator as this could be used in a POST request.
768 if not user:
769 request_handler.redirect(users.create_login_url(
770 request_handler.request.uri))
771 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400772
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400773 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400774
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400775 self.flow.params['state'] = _build_state_value(request_handler, user)
Daniel Hermes58341a02013-04-05 09:58:16 -0700776 self.credentials = self._storage_class(
777 self._credentials_class, None,
778 self._credentials_property_name, user=user).get()
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400779 try:
780 resp = method(request_handler, *args, **kwargs)
781 finally:
782 self.credentials = None
783 return resp
Joe Gregorio432f17e2011-05-22 23:18:00 -0400784 return setup_oauth
785
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400786
Joe Gregorio432f17e2011-05-22 23:18:00 -0400787 def has_credentials(self):
788 """True if for the logged in user there are valid access Credentials.
789
790 Must only be called from with a webapp.RequestHandler subclassed method
791 that had been decorated with either @oauth_required or @oauth_aware.
792 """
793 return self.credentials is not None and not self.credentials.invalid
794
795 def authorize_url(self):
796 """Returns the URL to start the OAuth dance.
797
798 Must only be called from with a webapp.RequestHandler subclassed method
799 that had been decorated with either @oauth_required or @oauth_aware.
800 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400801 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500802 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400803
Joe Gregoriod8cc4582013-11-26 16:06:56 -0500804 def http(self, *args, **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400805 """Returns an authorized http instance.
806
807 Must only be called from within an @oauth_required decorated method, or
808 from within an @oauth_aware decorated method where has_credentials()
809 returns True.
Joe Gregoriod8cc4582013-11-26 16:06:56 -0500810
811 Args:
812 args: Positional arguments passed to httplib2.Http constructor.
813 kwargs: Positional arguments passed to httplib2.Http constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400814 """
Joe Gregoriod8cc4582013-11-26 16:06:56 -0500815 return self.credentials.authorize(httplib2.Http(*args, **kwargs))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400816
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400817 @property
818 def callback_path(self):
819 """The absolute path where the callback will occur.
820
821 Note this is the absolute path, not the absolute URI, that will be
822 calculated by the decorator at runtime. See callback_handler() for how this
823 should be used.
824
825 Returns:
826 The callback path as a string.
827 """
828 return self._callback_path
829
830
831 def callback_handler(self):
832 """RequestHandler for the OAuth 2.0 redirect callback.
833
834 Usage:
835 app = webapp.WSGIApplication([
836 ('/index', MyIndexHandler),
837 ...,
838 (decorator.callback_path, decorator.callback_handler())
839 ])
840
841 Returns:
842 A webapp.RequestHandler that handles the redirect back from the
843 server during the OAuth 2.0 dance.
844 """
845 decorator = self
846
847 class OAuth2Handler(webapp.RequestHandler):
848 """Handler for the redirect_uri of the OAuth 2.0 dance."""
849
850 @login_required
851 def get(self):
852 error = self.request.get('error')
853 if error:
854 errormsg = self.request.get('error_description', error)
855 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400856 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400857 else:
858 user = users.get_current_user()
859 decorator._create_flow(self)
860 credentials = decorator.flow.step2_exchange(self.request.params)
Daniel Hermes58341a02013-04-05 09:58:16 -0700861 decorator._storage_class(
862 decorator._credentials_class, None,
863 decorator._credentials_property_name, user=user).put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400864 redirect_uri = _parse_state_value(str(self.request.get('state')),
865 user)
Joe Gregoriocda87522013-02-22 16:22:48 -0500866
867 if decorator._token_response_param and credentials.token_response:
868 resp_json = simplejson.dumps(credentials.token_response)
Joe Gregorio10244032013-03-06 09:48:04 -0500869 redirect_uri = util._add_query_parameter(
Daniel Hermesf7b648f2013-03-06 09:38:53 -0800870 redirect_uri, decorator._token_response_param, resp_json)
Joe Gregoriocda87522013-02-22 16:22:48 -0500871
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400872 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400873
874 return OAuth2Handler
875
876 def callback_application(self):
877 """WSGI application for handling the OAuth 2.0 redirect callback.
878
879 If you need finer grained control use `callback_handler` which returns just
880 the webapp.RequestHandler.
881
882 Returns:
883 A webapp.WSGIApplication that handles the redirect back from the
884 server during the OAuth 2.0 dance.
885 """
886 return webapp.WSGIApplication([
887 (self.callback_path, self.callback_handler())
888 ])
889
Joe Gregorio432f17e2011-05-22 23:18:00 -0400890
Joe Gregoriof08a4982011-10-07 13:11:16 -0400891class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
892 """An OAuth2Decorator that builds from a clientsecrets file.
893
894 Uses a clientsecrets file as the source for all the information when
895 constructing an OAuth2Decorator.
896
897 Example:
898
899 decorator = OAuth2DecoratorFromClientSecrets(
900 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500901 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400902
903
904 class MainHandler(webapp.RequestHandler):
905
906 @decorator.oauth_required
907 def get(self):
908 http = decorator.http()
909 # http is authorized with the user's Credentials and can be used
910 # in API calls
911 """
912
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400913 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400914 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400915 """Constructor
916
917 Args:
918 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500919 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400920 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400921 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400922 clientsecrets file is missing or invalid. The message may contain HTML
923 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400924 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400925 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400926 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400927 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400928 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
929 if client_type not in [
930 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
931 raise InvalidClientSecretsError(
932 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800933 constructor_kwargs = {
934 'auth_uri': client_info['auth_uri'],
935 'token_uri': client_info['token_uri'],
936 'message': message,
937 }
938 revoke_uri = client_info.get('revoke_uri')
939 if revoke_uri is not None:
940 constructor_kwargs['revoke_uri'] = revoke_uri
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400941 super(OAuth2DecoratorFromClientSecrets, self).__init__(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800942 client_info['client_id'], client_info['client_secret'],
943 scope, **constructor_kwargs)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400944 if message is not None:
945 self._message = message
946 else:
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800947 self._message = 'Please configure your application for OAuth 2.0.'
Joe Gregoriof08a4982011-10-07 13:11:16 -0400948
949
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400950@util.positional(2)
951def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400952 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400953 """Creates an OAuth2Decorator populated from a clientsecrets file.
954
955 Args:
956 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400957 scope: string or list of strings, scope(s) of the credentials being
958 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400959 message: string, A friendly string to display to the user if the
960 clientsecrets file is missing or invalid. The message may contain HTML and
961 will be presented on the web interface for any method that uses the
962 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400963 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400964 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400965
966 Returns: An OAuth2Decorator
967
968 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400969 return OAuth2DecoratorFromClientSecrets(filename, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800970 message=message, cache=cache)