blob: b463d4a01d0b3ae539b61dee0e28d834c2b2d9f4 [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.
Joe Gregorio956c1202014-02-05 14:24:11 -0500162 kwargs: optional keyword args, including:
163 service_account_id: service account id of the application. If None or
164 unspecified, the default service account for the app is used.
JacobMoshenko8e905102011-06-20 09:53:10 -0400165 """
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500166 self.scope = util.scopes_to_string(scope)
Joe Gregorio956c1202014-02-05 14:24:11 -0500167 self.service_account_id = kwargs.get('service_account_id', None)
JacobMoshenko8e905102011-06-20 09:53:10 -0400168
dhermes@google.com2cc09382013-02-11 08:42:18 -0800169 # Assertion type is no longer used, but still in the parent class signature.
170 super(AppAssertionCredentials, self).__init__(None)
JacobMoshenko8e905102011-06-20 09:53:10 -0400171
Joe Gregorio562b7312011-09-15 09:06:38 -0400172 @classmethod
173 def from_json(cls, json):
174 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500175 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400176
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500177 def _refresh(self, http_request):
178 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400179
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500180 Since the underlying App Engine app_identity implementation does its own
181 caching we can skip all the storage hoops and just to a refresh using the
182 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400183
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500184 Args:
185 http_request: callable, a callable that matches the method signature of
186 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400187
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500188 Raises:
189 AccessTokenRefreshError: When the refresh fails.
190 """
191 try:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500192 scopes = self.scope.split()
Joe Gregorio956c1202014-02-05 14:24:11 -0500193 (token, _) = app_identity.get_access_token(
194 scopes, service_account_id=self.service_account_id)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500195 except app_identity.Error, e:
196 raise AccessTokenRefreshError(str(e))
197 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400198
199
Joe Gregorio695fdc12011-01-16 16:46:55 -0500200class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500201 """App Engine datastore Property for Flow.
202
dhermes@google.com47154822012-11-26 10:44:09 -0800203 Utility property that allows easy storage and retrieval of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500204 oauth2client.Flow"""
205
206 # Tell what the user type is.
207 data_type = Flow
208
209 # For writing to datastore.
210 def get_value_for_datastore(self, model_instance):
211 flow = super(FlowProperty,
212 self).get_value_for_datastore(model_instance)
213 return db.Blob(pickle.dumps(flow))
214
215 # For reading from datastore.
216 def make_value_from_datastore(self, value):
217 if value is None:
218 return None
219 return pickle.loads(value)
220
221 def validate(self, value):
222 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400223 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500224 'to a FlowThreeLegged instance (%s)' %
225 (self.name, value))
226 return super(FlowProperty, self).validate(value)
227
228 def empty(self, value):
229 return not value
230
231
Joe Gregorio78787b62013-02-08 15:36:21 -0500232if ndb is not None:
233 class FlowNDBProperty(ndb.PickleProperty):
234 """App Engine NDB datastore Property for Flow.
dhermes@google.com47154822012-11-26 10:44:09 -0800235
Joe Gregorio78787b62013-02-08 15:36:21 -0500236 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
237 PickleProperty inherits from BlobProperty, the underlying representation of
238 the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800239
Joe Gregorio78787b62013-02-08 15:36:21 -0500240 Utility property that allows easy storage and retrieval of an
241 oauth2client.Flow
dhermes@google.com47154822012-11-26 10:44:09 -0800242 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500243
244 def _validate(self, value):
245 """Validates a value as a proper Flow object.
246
247 Args:
248 value: A value to be set on the property.
249
250 Raises:
251 TypeError if the value is not an instance of Flow.
252 """
253 logger.info('validate: Got type %s', type(value))
254 if value is not None and not isinstance(value, Flow):
255 raise TypeError('Property %s must be convertible to a flow '
256 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800257
258
Joe Gregorio695fdc12011-01-16 16:46:55 -0500259class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500260 """App Engine datastore Property for Credentials.
261
262 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500263 oath2client.Credentials
264 """
265
266 # Tell what the user type is.
267 data_type = Credentials
268
269 # For writing to datastore.
270 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400271 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500272 cred = super(CredentialsProperty,
273 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400274 if cred is None:
275 cred = ''
276 else:
277 cred = cred.to_json()
278 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500279
280 # For reading from datastore.
281 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400282 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500283 if value is None:
284 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400285 if len(value) == 0:
286 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400287 try:
288 credentials = Credentials.new_from_json(value)
289 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400290 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400291 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500292
293 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400294 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400295 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500296 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400297 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400298 'to a Credentials instance (%s)' %
299 (self.name, value))
300 #if value is not None and not isinstance(value, Credentials):
301 # return None
302 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500303
304
Joe Gregorio78787b62013-02-08 15:36:21 -0500305if ndb is not None:
306 # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
307 # and subclass mechanics to use new_from_dict, to_dict,
308 # from_dict, etc.
309 class CredentialsNDBProperty(ndb.BlobProperty):
310 """App Engine NDB datastore Property for Credentials.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500311
Joe Gregorio78787b62013-02-08 15:36:21 -0500312 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
313 Since CredentialsProperty stores data as a blob and this inherits from
314 BlobProperty, the data in the datastore will be the same as in the DB case.
dhermes@google.com47154822012-11-26 10:44:09 -0800315
Joe Gregorio78787b62013-02-08 15:36:21 -0500316 Utility property that allows easy storage and retrieval of Credentials and
317 subclasses.
dhermes@google.com47154822012-11-26 10:44:09 -0800318 """
Joe Gregorio78787b62013-02-08 15:36:21 -0500319 def _validate(self, value):
320 """Validates a value as a proper credentials object.
dhermes@google.com47154822012-11-26 10:44:09 -0800321
Joe Gregorio78787b62013-02-08 15:36:21 -0500322 Args:
323 value: A value to be set on the property.
dhermes@google.com47154822012-11-26 10:44:09 -0800324
Joe Gregorio78787b62013-02-08 15:36:21 -0500325 Raises:
326 TypeError if the value is not an instance of Credentials.
327 """
328 logger.info('validate: Got type %s', type(value))
329 if value is not None and not isinstance(value, Credentials):
330 raise TypeError('Property %s must be convertible to a credentials '
331 'instance; received: %s.' % (self._name, value))
dhermes@google.com47154822012-11-26 10:44:09 -0800332
Joe Gregorio78787b62013-02-08 15:36:21 -0500333 def _to_base_type(self, value):
334 """Converts our validated value to a JSON serialized string.
dhermes@google.com47154822012-11-26 10:44:09 -0800335
Joe Gregorio78787b62013-02-08 15:36:21 -0500336 Args:
337 value: A value to be set in the datastore.
dhermes@google.com47154822012-11-26 10:44:09 -0800338
Joe Gregorio78787b62013-02-08 15:36:21 -0500339 Returns:
340 A JSON serialized version of the credential, else '' if value is None.
341 """
342 if value is None:
343 return ''
344 else:
345 return value.to_json()
dhermes@google.com47154822012-11-26 10:44:09 -0800346
Joe Gregorio78787b62013-02-08 15:36:21 -0500347 def _from_base_type(self, value):
348 """Converts our stored JSON string back to the desired type.
349
350 Args:
351 value: A value from the datastore to be converted to the desired type.
352
353 Returns:
354 A deserialized Credentials (or subclass) object, else None if the
355 value can't be parsed.
356 """
357 if not value:
358 return None
359 try:
360 # Uses the from_json method of the implied class of value
361 credentials = Credentials.new_from_json(value)
362 except ValueError:
363 credentials = None
364 return credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800365
366
367class StorageByKeyName(Storage):
368 """Store and retrieve a credential to and from the App Engine datastore.
369
370 This Storage helper presumes the Credentials have been stored as a
371 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
372 that entities are stored by key_name.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500373 """
374
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400375 @util.positional(4)
Daniel Hermes58341a02013-04-05 09:58:16 -0700376 def __init__(self, model, key_name, property_name, cache=None, user=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500377 """Constructor for Storage.
378
379 Args:
dhermes@google.com47154822012-11-26 10:44:09 -0800380 model: db.Model or ndb.Model, model class
Joe Gregorio695fdc12011-01-16 16:46:55 -0500381 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400382 property_name: string, name of the property that is a CredentialsProperty
dhermes@google.com47154822012-11-26 10:44:09 -0800383 or CredentialsNDBProperty.
384 cache: memcache, a write-through cache to put in front of the datastore.
385 If the model you are using is an NDB model, using a cache will be
386 redundant since the model uses an instance cache and memcache for you.
Daniel Hermes58341a02013-04-05 09:58:16 -0700387 user: users.User object, optional. Can be used to grab user ID as a
388 key_name if no key name is specified.
Joe Gregorio695fdc12011-01-16 16:46:55 -0500389 """
Daniel Hermes58341a02013-04-05 09:58:16 -0700390 if key_name is None:
391 if user is None:
392 raise ValueError('StorageByKeyName called with no key name or user.')
393 key_name = user.user_id()
394
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500395 self._model = model
396 self._key_name = key_name
397 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400398 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500399
dhermes@google.com47154822012-11-26 10:44:09 -0800400 def _is_ndb(self):
401 """Determine whether the model of the instance is an NDB model.
402
403 Returns:
404 Boolean indicating whether or not the model is an NDB or DB model.
405 """
406 # issubclass will fail if one of the arguments is not a class, only need
407 # worry about new-style classes since ndb and db models are new-style
408 if isinstance(self._model, type):
Joe Gregorio78787b62013-02-08 15:36:21 -0500409 if ndb is not None and issubclass(self._model, ndb.Model):
dhermes@google.com47154822012-11-26 10:44:09 -0800410 return True
411 elif issubclass(self._model, db.Model):
412 return False
413
414 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
415
416 def _get_entity(self):
417 """Retrieve entity from datastore.
418
419 Uses a different model method for db or ndb models.
420
421 Returns:
422 Instance of the model corresponding to the current storage object
423 and stored using the key name of the storage object.
424 """
425 if self._is_ndb():
426 return self._model.get_by_id(self._key_name)
427 else:
428 return self._model.get_by_key_name(self._key_name)
429
430 def _delete_entity(self):
431 """Delete entity from datastore.
432
433 Attempts to delete using the key_name stored on the object, whether or not
434 the given key is in the datastore.
435 """
436 if self._is_ndb():
437 ndb.Key(self._model, self._key_name).delete()
438 else:
439 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
440 db.delete(entity_key)
441
Joe Gregorioaa2f6c92013-12-13 11:09:24 -0500442 @db.non_transactional(allow_existing=True)
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400443 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500444 """Retrieve Credential from datastore.
445
446 Returns:
447 oauth2client.Credentials
448 """
Joe Gregorioe912d182013-08-06 11:30:44 -0400449 credentials = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400450 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400451 json = self._cache.get(self._key_name)
452 if json:
Joe Gregorioe912d182013-08-06 11:30:44 -0400453 credentials = Credentials.new_from_json(json)
454 if credentials is None:
455 entity = self._get_entity()
456 if entity is not None:
457 credentials = getattr(entity, self._property_name)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500458 if self._cache:
dhermes@google.com47154822012-11-26 10:44:09 -0800459 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400460
Joe Gregorioe912d182013-08-06 11:30:44 -0400461 if credentials and hasattr(credentials, 'set_store'):
462 credentials.set_store(self)
dhermes@google.com47154822012-11-26 10:44:09 -0800463 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500464
Joe Gregorioaa2f6c92013-12-13 11:09:24 -0500465 @db.non_transactional(allow_existing=True)
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400466 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500467 """Write a Credentials to the datastore.
468
469 Args:
470 credentials: Credentials, the credentials to store.
471 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500472 entity = self._model.get_or_insert(self._key_name)
473 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500474 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400475 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400476 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400477
Joe Gregorioaa2f6c92013-12-13 11:09:24 -0500478 @db.non_transactional(allow_existing=True)
Joe Gregorioec75dc12012-02-06 13:40:42 -0500479 def locked_delete(self):
480 """Delete Credential from datastore."""
481
482 if self._cache:
483 self._cache.delete(self._key_name)
484
dhermes@google.com47154822012-11-26 10:44:09 -0800485 self._delete_entity()
Joe Gregorioec75dc12012-02-06 13:40:42 -0500486
Joe Gregorio432f17e2011-05-22 23:18:00 -0400487
488class CredentialsModel(db.Model):
489 """Storage for OAuth 2.0 Credentials
490
491 Storage of the model is keyed by the user.user_id().
492 """
493 credentials = CredentialsProperty()
494
495
Joe Gregorio78787b62013-02-08 15:36:21 -0500496if ndb is not None:
497 class CredentialsNDBModel(ndb.Model):
498 """NDB Model for storage of OAuth 2.0 Credentials
dhermes@google.com47154822012-11-26 10:44:09 -0800499
Joe Gregorio78787b62013-02-08 15:36:21 -0500500 Since this model uses the same kind as CredentialsModel and has a property
501 which can serialize and deserialize Credentials correctly, it can be used
502 interchangeably with a CredentialsModel to access, insert and delete the
503 same entities. This simply provides an NDB model for interacting with the
504 same data the DB model interacts with.
dhermes@google.com47154822012-11-26 10:44:09 -0800505
Joe Gregorio78787b62013-02-08 15:36:21 -0500506 Storage of the model is keyed by the user.user_id().
507 """
508 credentials = CredentialsNDBProperty()
dhermes@google.com47154822012-11-26 10:44:09 -0800509
Joe Gregorio78787b62013-02-08 15:36:21 -0500510 @classmethod
511 def _get_kind(cls):
512 """Return the kind name for this class."""
513 return 'CredentialsModel'
dhermes@google.com47154822012-11-26 10:44:09 -0800514
515
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400516def _build_state_value(request_handler, user):
517 """Composes the value for the 'state' parameter.
518
519 Packs the current request URI and an XSRF token into an opaque string that
520 can be passed to the authentication server via the 'state' parameter.
521
522 Args:
523 request_handler: webapp.RequestHandler, The request.
524 user: google.appengine.api.users.User, The current user.
525
526 Returns:
527 The state value as a string.
528 """
529 uri = request_handler.request.url
530 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
531 action_id=str(uri))
532 return uri + ':' + token
533
534
535def _parse_state_value(state, user):
536 """Parse the value of the 'state' parameter.
537
538 Parses the value and validates the XSRF token in the state parameter.
539
540 Args:
541 state: string, The value of the state parameter.
542 user: google.appengine.api.users.User, The current user.
543
544 Raises:
545 InvalidXsrfTokenError: if the XSRF token is invalid.
546
547 Returns:
548 The redirect URI.
549 """
550 uri, token = state.rsplit(':', 1)
551 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
552 action_id=uri):
553 raise InvalidXsrfTokenError()
554
555 return uri
556
557
Joe Gregorio432f17e2011-05-22 23:18:00 -0400558class OAuth2Decorator(object):
559 """Utility for making OAuth 2.0 easier.
560
561 Instantiate and then use with oauth_required or oauth_aware
562 as decorators on webapp.RequestHandler methods.
563
564 Example:
565
566 decorator = OAuth2Decorator(
567 client_id='837...ent.com',
568 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500569 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400570
571
572 class MainHandler(webapp.RequestHandler):
573
574 @decorator.oauth_required
575 def get(self):
576 http = decorator.http()
577 # http is authorized with the user's Credentials and can be used
578 # in API calls
579
580 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400581
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400582 def set_credentials(self, credentials):
583 self._tls.credentials = credentials
584
585 def get_credentials(self):
Joe Gregorioc211bec2013-08-06 12:06:54 -0400586 """A thread local Credentials object.
587
588 Returns:
589 A client.Credentials object, or None if credentials hasn't been set in
590 this thread yet, which may happen when calling has_credentials inside
591 oauth_aware.
592 """
593 return getattr(self._tls, 'credentials', None)
594
595 credentials = property(get_credentials, set_credentials)
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400596
597 def set_flow(self, flow):
598 self._tls.flow = flow
599
600 def get_flow(self):
Joe Gregorioc211bec2013-08-06 12:06:54 -0400601 """A thread local Flow object.
602
603 Returns:
604 A credentials.Flow object, or None if the flow hasn't been set in this
605 thread yet, which happens in _create_flow() since Flows are created
606 lazily.
607 """
608 return getattr(self._tls, 'flow', None)
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400609
610 flow = property(get_flow, set_flow)
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400611
612
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400613 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400614 def __init__(self, client_id, client_secret, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800615 auth_uri=GOOGLE_AUTH_URI,
616 token_uri=GOOGLE_TOKEN_URI,
617 revoke_uri=GOOGLE_REVOKE_URI,
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100618 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400619 message=None,
620 callback_path='/oauth2callback',
Joe Gregoriocda87522013-02-22 16:22:48 -0500621 token_response_param=None,
Daniel Hermes58341a02013-04-05 09:58:16 -0700622 _storage_class=StorageByKeyName,
623 _credentials_class=CredentialsModel,
624 _credentials_property_name='credentials',
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400625 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400626
627 """Constructor for OAuth2Decorator
628
629 Args:
630 client_id: string, client identifier.
631 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500632 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400633 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400634 auth_uri: string, URI for authorization endpoint. For convenience
635 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
636 token_uri: string, URI for token endpoint. For convenience
637 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800638 revoke_uri: string, URI for revoke endpoint. For convenience
639 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100640 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400641 message: Message to display if there are problems with the OAuth 2.0
642 configuration. The message may contain HTML and will be presented on the
643 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400644 callback_path: string, The absolute path to use as the callback URI. Note
645 that this must match up with the URI given when registering the
646 application in the APIs Console.
Joe Gregoriocda87522013-02-22 16:22:48 -0500647 token_response_param: string. If provided, the full JSON response
648 to the access token request will be encoded and included in this query
649 parameter in the callback URI. This is useful with providers (e.g.
650 wordpress.com) that include extra fields that the client may want.
Daniel Hermes58341a02013-04-05 09:58:16 -0700651 _storage_class: "Protected" keyword argument not typically provided to
652 this constructor. A storage class to aid in storing a Credentials object
653 for a user in the datastore. Defaults to StorageByKeyName.
654 _credentials_class: "Protected" keyword argument not typically provided to
655 this constructor. A db or ndb Model class to hold credentials. Defaults
656 to CredentialsModel.
657 _credentials_property_name: "Protected" keyword argument not typically
658 provided to this constructor. A string indicating the name of the field
659 on the _credentials_class where a Credentials object will be stored.
660 Defaults to 'credentials'.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500661 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
662 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400663 """
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400664 self._tls = threading.local()
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400665 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400666 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400667 self._client_id = client_id
668 self._client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500669 self._scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400670 self._auth_uri = auth_uri
671 self._token_uri = token_uri
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800672 self._revoke_uri = revoke_uri
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400673 self._user_agent = user_agent
674 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400675 self._message = message
676 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400677 self._callback_path = callback_path
Joe Gregoriocda87522013-02-22 16:22:48 -0500678 self._token_response_param = token_response_param
Daniel Hermes58341a02013-04-05 09:58:16 -0700679 self._storage_class = _storage_class
680 self._credentials_class = _credentials_class
681 self._credentials_property_name = _credentials_property_name
Joe Gregoriof08a4982011-10-07 13:11:16 -0400682
683 def _display_error_message(self, request_handler):
684 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400685 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400686 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400687
688 def oauth_required(self, method):
689 """Decorator that starts the OAuth 2.0 dance.
690
691 Starts the OAuth dance for the logged in user if they haven't already
692 granted access for this application.
693
694 Args:
695 method: callable, to be decorated method of a webapp.RequestHandler
696 instance.
697 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400698
Joe Gregorio17774972012-03-01 11:11:59 -0500699 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400700 if self._in_error:
701 self._display_error_message(request_handler)
702 return
703
Joe Gregoriof427c532011-06-13 09:35:26 -0400704 user = users.get_current_user()
705 # Don't use @login_decorator as this could be used in a POST request.
706 if not user:
707 request_handler.redirect(users.create_login_url(
708 request_handler.request.uri))
709 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400710
711 self._create_flow(request_handler)
712
Joe Gregorio432f17e2011-05-22 23:18:00 -0400713 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400714 self.flow.params['state'] = _build_state_value(request_handler, user)
Daniel Hermes58341a02013-04-05 09:58:16 -0700715 self.credentials = self._storage_class(
716 self._credentials_class, None,
717 self._credentials_property_name, user=user).get()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400718
719 if not self.has_credentials():
720 return request_handler.redirect(self.authorize_url())
721 try:
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400722 resp = method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400723 except AccessTokenRefreshError:
724 return request_handler.redirect(self.authorize_url())
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400725 finally:
726 self.credentials = None
727 return resp
Joe Gregorio432f17e2011-05-22 23:18:00 -0400728
729 return check_oauth
730
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400731 def _create_flow(self, request_handler):
732 """Create the Flow object.
733
734 The Flow is calculated lazily since we don't know where this app is
735 running until it receives a request, at which point redirect_uri can be
736 calculated and then the Flow object can be constructed.
737
738 Args:
739 request_handler: webapp.RequestHandler, the request handler.
740 """
741 if self.flow is None:
742 redirect_uri = request_handler.request.relative_url(
743 self._callback_path) # Usually /oauth2callback
744 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
745 self._scope, redirect_uri=redirect_uri,
746 user_agent=self._user_agent,
747 auth_uri=self._auth_uri,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800748 token_uri=self._token_uri,
749 revoke_uri=self._revoke_uri,
750 **self._kwargs)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400751
Joe Gregorio432f17e2011-05-22 23:18:00 -0400752 def oauth_aware(self, method):
753 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
754
755 Does all the setup for the OAuth dance, but doesn't initiate it.
756 This decorator is useful if you want to create a page that knows
757 whether or not the user has granted access to this application.
758 From within a method decorated with @oauth_aware the has_credentials()
759 and authorize_url() methods can be called.
760
761 Args:
762 method: callable, to be decorated method of a webapp.RequestHandler
763 instance.
764 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400765
Joe Gregorio17774972012-03-01 11:11:59 -0500766 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400767 if self._in_error:
768 self._display_error_message(request_handler)
769 return
770
Joe Gregoriof427c532011-06-13 09:35:26 -0400771 user = users.get_current_user()
772 # Don't use @login_decorator as this could be used in a POST request.
773 if not user:
774 request_handler.redirect(users.create_login_url(
775 request_handler.request.uri))
776 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400777
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400778 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400779
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400780 self.flow.params['state'] = _build_state_value(request_handler, user)
Daniel Hermes58341a02013-04-05 09:58:16 -0700781 self.credentials = self._storage_class(
782 self._credentials_class, None,
783 self._credentials_property_name, user=user).get()
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400784 try:
785 resp = method(request_handler, *args, **kwargs)
786 finally:
787 self.credentials = None
788 return resp
Joe Gregorio432f17e2011-05-22 23:18:00 -0400789 return setup_oauth
790
Joe Gregoriob8b6fea2013-05-16 15:52:57 -0400791
Joe Gregorio432f17e2011-05-22 23:18:00 -0400792 def has_credentials(self):
793 """True if for the logged in user there are valid access Credentials.
794
795 Must only be called from with a webapp.RequestHandler subclassed method
796 that had been decorated with either @oauth_required or @oauth_aware.
797 """
798 return self.credentials is not None and not self.credentials.invalid
799
800 def authorize_url(self):
801 """Returns the URL to start the OAuth dance.
802
803 Must only be called from with a webapp.RequestHandler subclassed method
804 that had been decorated with either @oauth_required or @oauth_aware.
805 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400806 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500807 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400808
Joe Gregoriod8cc4582013-11-26 16:06:56 -0500809 def http(self, *args, **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400810 """Returns an authorized http instance.
811
812 Must only be called from within an @oauth_required decorated method, or
813 from within an @oauth_aware decorated method where has_credentials()
814 returns True.
Joe Gregoriod8cc4582013-11-26 16:06:56 -0500815
816 Args:
817 args: Positional arguments passed to httplib2.Http constructor.
818 kwargs: Positional arguments passed to httplib2.Http constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400819 """
Joe Gregoriod8cc4582013-11-26 16:06:56 -0500820 return self.credentials.authorize(httplib2.Http(*args, **kwargs))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400821
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400822 @property
823 def callback_path(self):
824 """The absolute path where the callback will occur.
825
826 Note this is the absolute path, not the absolute URI, that will be
827 calculated by the decorator at runtime. See callback_handler() for how this
828 should be used.
829
830 Returns:
831 The callback path as a string.
832 """
833 return self._callback_path
834
835
836 def callback_handler(self):
837 """RequestHandler for the OAuth 2.0 redirect callback.
838
839 Usage:
840 app = webapp.WSGIApplication([
841 ('/index', MyIndexHandler),
842 ...,
843 (decorator.callback_path, decorator.callback_handler())
844 ])
845
846 Returns:
847 A webapp.RequestHandler that handles the redirect back from the
848 server during the OAuth 2.0 dance.
849 """
850 decorator = self
851
852 class OAuth2Handler(webapp.RequestHandler):
853 """Handler for the redirect_uri of the OAuth 2.0 dance."""
854
855 @login_required
856 def get(self):
857 error = self.request.get('error')
858 if error:
859 errormsg = self.request.get('error_description', error)
860 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400861 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400862 else:
863 user = users.get_current_user()
864 decorator._create_flow(self)
865 credentials = decorator.flow.step2_exchange(self.request.params)
Daniel Hermes58341a02013-04-05 09:58:16 -0700866 decorator._storage_class(
867 decorator._credentials_class, None,
868 decorator._credentials_property_name, user=user).put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400869 redirect_uri = _parse_state_value(str(self.request.get('state')),
870 user)
Joe Gregoriocda87522013-02-22 16:22:48 -0500871
872 if decorator._token_response_param and credentials.token_response:
873 resp_json = simplejson.dumps(credentials.token_response)
Joe Gregorio10244032013-03-06 09:48:04 -0500874 redirect_uri = util._add_query_parameter(
Daniel Hermesf7b648f2013-03-06 09:38:53 -0800875 redirect_uri, decorator._token_response_param, resp_json)
Joe Gregoriocda87522013-02-22 16:22:48 -0500876
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400877 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400878
879 return OAuth2Handler
880
881 def callback_application(self):
882 """WSGI application for handling the OAuth 2.0 redirect callback.
883
884 If you need finer grained control use `callback_handler` which returns just
885 the webapp.RequestHandler.
886
887 Returns:
888 A webapp.WSGIApplication that handles the redirect back from the
889 server during the OAuth 2.0 dance.
890 """
891 return webapp.WSGIApplication([
892 (self.callback_path, self.callback_handler())
893 ])
894
Joe Gregorio432f17e2011-05-22 23:18:00 -0400895
Joe Gregoriof08a4982011-10-07 13:11:16 -0400896class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
897 """An OAuth2Decorator that builds from a clientsecrets file.
898
899 Uses a clientsecrets file as the source for all the information when
900 constructing an OAuth2Decorator.
901
902 Example:
903
904 decorator = OAuth2DecoratorFromClientSecrets(
905 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500906 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400907
908
909 class MainHandler(webapp.RequestHandler):
910
911 @decorator.oauth_required
912 def get(self):
913 http = decorator.http()
914 # http is authorized with the user's Credentials and can be used
915 # in API calls
916 """
917
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400918 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400919 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400920 """Constructor
921
922 Args:
923 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500924 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400925 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400926 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400927 clientsecrets file is missing or invalid. The message may contain HTML
928 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400929 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400930 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400931 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400932 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400933 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
934 if client_type not in [
935 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
936 raise InvalidClientSecretsError(
937 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800938 constructor_kwargs = {
939 'auth_uri': client_info['auth_uri'],
940 'token_uri': client_info['token_uri'],
941 'message': message,
942 }
943 revoke_uri = client_info.get('revoke_uri')
944 if revoke_uri is not None:
945 constructor_kwargs['revoke_uri'] = revoke_uri
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400946 super(OAuth2DecoratorFromClientSecrets, self).__init__(
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800947 client_info['client_id'], client_info['client_secret'],
948 scope, **constructor_kwargs)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400949 if message is not None:
950 self._message = message
951 else:
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800952 self._message = 'Please configure your application for OAuth 2.0.'
Joe Gregoriof08a4982011-10-07 13:11:16 -0400953
954
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400955@util.positional(2)
956def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400957 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400958 """Creates an OAuth2Decorator populated from a clientsecrets file.
959
960 Args:
961 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400962 scope: string or list of strings, scope(s) of the credentials being
963 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400964 message: string, A friendly string to display to the user if the
965 clientsecrets file is missing or invalid. The message may contain HTML and
966 will be presented on the web interface for any method that uses the
967 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400968 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400969 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400970
971 Returns: An OAuth2Decorator
972
973 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400974 return OAuth2DecoratorFromClientSecrets(filename, scope,
dhermes@google.coma9eb0bb2013-02-06 09:19:01 -0800975 message=message, cache=cache)