Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2)

Side by Side Diff: appengine/chromium_build_logs/third_party/oauth2client/appengine.py

Issue 1260293009: make version of ts_mon compatible with appengine (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: clean up code Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 # Copyright (C) 2010 Google Inc. 1 # Copyright 2014 Google Inc. All rights reserved.
2 # 2 #
3 # Licensed under the Apache License, Version 2.0 (the "License"); 3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with 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 5 # You may obtain a copy of the License at
6 # 6 #
7 # http://www.apache.org/licenses/LICENSE-2.0 7 # http://www.apache.org/licenses/LICENSE-2.0
8 # 8 #
9 # Unless required by applicable law or agreed to in writing, software 9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS, 10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and 12 # See the License for the specific language governing permissions and
13 # limitations under the License. 13 # limitations under the License.
14 14
15 """Utilities for Google App Engine 15 """Utilities for Google App Engine
16 16
17 Utilities for making it easier to use OAuth 2.0 on Google App Engine. 17 Utilities for making it easier to use OAuth 2.0 on Google App Engine.
18 """ 18 """
19 19
20 __author__ = 'jcgregorio@google.com (Joe Gregorio)' 20 __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 21
22 import base64 22 import cgi
23 import json
24 import logging
25 import os
26 import pickle
27 import threading
28
23 import httplib2 29 import httplib2
24 import logging 30 import webapp2 as webapp
25 import pickle
26 import time
27 31
28 import clientsecrets 32 from google.appengine.api import app_identity
29
30 from anyjson import simplejson
31 from client import AccessTokenRefreshError
32 from client import AssertionCredentials
33 from client import Credentials
34 from client import Flow
35 from client import OAuth2WebServerFlow
36 from client import Storage
37 from google.appengine.api import memcache 33 from google.appengine.api import memcache
38 from google.appengine.api import users 34 from google.appengine.api import users
39 from google.appengine.api.app_identity import app_identity
40 from google.appengine.ext import db 35 from google.appengine.ext import db
41 from google.appengine.ext import webapp
42 from google.appengine.ext.webapp.util import login_required 36 from google.appengine.ext.webapp.util import login_required
43 from google.appengine.ext.webapp.util import run_wsgi_app 37 from google.appengine.ext.webapp.util import run_wsgi_app
38 from oauth2client import GOOGLE_AUTH_URI
39 from oauth2client import GOOGLE_REVOKE_URI
40 from oauth2client import GOOGLE_TOKEN_URI
41 from oauth2client import clientsecrets
42 from oauth2client import util
43 from oauth2client import xsrfutil
44 from oauth2client.client import AccessTokenRefreshError
45 from oauth2client.client import AssertionCredentials
46 from oauth2client.client import Credentials
47 from oauth2client.client import Flow
48 from oauth2client.client import OAuth2WebServerFlow
49 from oauth2client.client import Storage
50
51 # TODO(dhermes): Resolve import issue.
52 # This is a temporary fix for a Google internal issue.
53 try:
54 from google.appengine.ext import ndb
55 except ImportError:
56 ndb = None
57
58
59 logger = logging.getLogger(__name__)
44 60
45 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns' 61 OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
46 62
63 XSRF_MEMCACHE_ID = 'xsrf_secret_key'
64
65
66 def _safe_html(s):
67 """Escape text to make it safe to display.
68
69 Args:
70 s: string, The text to escape.
71
72 Returns:
73 The escaped text as a string.
74 """
75 return cgi.escape(s, quote=1).replace("'", ''')
76
47 77
48 class InvalidClientSecretsError(Exception): 78 class InvalidClientSecretsError(Exception):
49 """The client_secrets.json file is malformed or missing required fields.""" 79 """The client_secrets.json file is malformed or missing required fields."""
50 pass 80
81
82 class InvalidXsrfTokenError(Exception):
83 """The XSRF token is invalid or expired."""
84
85
86 class SiteXsrfSecretKey(db.Model):
87 """Storage for the sites XSRF secret key.
88
89 There will only be one instance stored of this model, the one used for the
90 site.
91 """
92 secret = db.StringProperty()
93
94 if ndb is not None:
95 class SiteXsrfSecretKeyNDB(ndb.Model):
96 """NDB Model for storage for the sites XSRF secret key.
97
98 Since this model uses the same kind as SiteXsrfSecretKey, it can be used
99 interchangeably. This simply provides an NDB model for interacting with the
100 same data the DB model interacts with.
101
102 There should only be one instance stored of this model, the one used for the
103 site.
104 """
105 secret = ndb.StringProperty()
106
107 @classmethod
108 def _get_kind(cls):
109 """Return the kind name for this class."""
110 return 'SiteXsrfSecretKey'
111
112
113 def _generate_new_xsrf_secret_key():
114 """Returns a random XSRF secret key.
115 """
116 return os.urandom(16).encode("hex")
117
118
119 def xsrf_secret_key():
120 """Return the secret key for use for XSRF protection.
121
122 If the Site entity does not have a secret key, this method will also create
123 one and persist it.
124
125 Returns:
126 The secret key.
127 """
128 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
129 if not secret:
130 # Load the one and only instance of SiteXsrfSecretKey.
131 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
132 if not model.secret:
133 model.secret = _generate_new_xsrf_secret_key()
134 model.put()
135 secret = model.secret
136 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
137
138 return str(secret)
51 139
52 140
53 class AppAssertionCredentials(AssertionCredentials): 141 class AppAssertionCredentials(AssertionCredentials):
54 """Credentials object for App Engine Assertion Grants 142 """Credentials object for App Engine Assertion Grants
55 143
56 This object will allow an App Engine application to identify itself to Google 144 This object will allow an App Engine application to identify itself to Google
57 and other OAuth 2.0 servers that can verify assertions. It can be used for 145 and other OAuth 2.0 servers that can verify assertions. It can be used for the
58 the purpose of accessing data stored under an account assigned to the App 146 purpose of accessing data stored under an account assigned to the App Engine
59 Engine application itself. The algorithm used for generating the assertion is 147 application itself.
60 the Signed JSON Web Token (JWT) algorithm. Additional details can be found at
61 the following link:
62
63 http://self-issued.info/docs/draft-jones-json-web-token.html
64 148
65 This credential does not require a flow to instantiate because it represents 149 This credential does not require a flow to instantiate because it represents
66 a two legged flow, and therefore has all of the required information to 150 a two legged flow, and therefore has all of the required information to
67 generate and refresh its own access tokens. 151 generate and refresh its own access tokens.
68
69 """ 152 """
70 153
71 def __init__(self, scope, 154 @util.positional(2)
72 audience='https://accounts.google.com/o/oauth2/token', 155 def __init__(self, scope, **kwargs):
73 assertion_type='http://oauth.net/grant_type/jwt/1.0/bearer',
74 token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
75 """Constructor for AppAssertionCredentials 156 """Constructor for AppAssertionCredentials
76 157
77 Args: 158 Args:
78 scope: string, scope of the credentials being requested. 159 scope: string or iterable of strings, scope(s) of the credentials being
79 audience: string, The audience, or verifier of the assertion. For 160 requested.
80 convenience defaults to Google's audience. 161 **kwargs: optional keyword args, including:
81 assertion_type: string, Type name that will identify the format of the 162 service_account_id: service account id of the application. If None or
82 assertion string. For convience, defaults to the JSON Web Token (JWT) 163 unspecified, the default service account for the app is used.
83 assertion type string.
84 token_uri: string, URI for token endpoint. For convenience
85 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
86 """ 164 """
87 self.scope = scope 165 self.scope = util.scopes_to_string(scope)
88 self.audience = audience 166 self._kwargs = kwargs
89 self.app_name = app_identity.get_service_account_name() 167 self.service_account_id = kwargs.get('service_account_id', None)
90 168
91 super(AppAssertionCredentials, self).__init__( 169 # Assertion type is no longer used, but still in the parent class signature.
92 assertion_type, 170 super(AppAssertionCredentials, self).__init__(None)
93 None,
94 token_uri)
95 171
96 @classmethod 172 @classmethod
97 def from_json(cls, json): 173 def from_json(cls, json_data):
98 data = simplejson.loads(json) 174 data = json.loads(json_data)
99 retval = AccessTokenCredentials( 175 return AppAssertionCredentials(data['scope'])
100 data['scope'],
101 data['audience'],
102 data['assertion_type'],
103 data['token_uri'])
104 return retval
105 176
106 def _generate_assertion(self): 177 def _refresh(self, http_request):
107 header = { 178 """Refreshes the access_token.
108 'typ': 'JWT',
109 'alg': 'RS256',
110 }
111 179
112 now = int(time.time()) 180 Since the underlying App Engine app_identity implementation does its own
113 claims = { 181 caching we can skip all the storage hoops and just to a refresh using the
114 'aud': self.audience, 182 API.
115 'scope': self.scope,
116 'iat': now,
117 'exp': now + 3600,
118 'iss': self.app_name,
119 }
120 183
121 jwt_components = [base64.b64encode(simplejson.dumps(seg)) 184 Args:
122 for seg in [header, claims]] 185 http_request: callable, a callable that matches the method signature of
186 httplib2.Http.request, used to make the refresh request.
123 187
124 base_str = ".".join(jwt_components) 188 Raises:
125 key_name, signature = app_identity.sign_blob(base_str) 189 AccessTokenRefreshError: When the refresh fails.
126 jwt_components.append(base64.b64encode(signature)) 190 """
127 return ".".join(jwt_components) 191 try:
192 scopes = self.scope.split()
193 (token, _) = app_identity.get_access_token(
194 scopes, service_account_id=self.service_account_id)
195 except app_identity.Error as e:
196 raise AccessTokenRefreshError(str(e))
197 self.access_token = token
198
199 @property
200 def serialization_data(self):
201 raise NotImplementedError('Cannot serialize credentials for AppEngine.')
202
203 def create_scoped_required(self):
204 return not self.scope
205
206 def create_scoped(self, scopes):
207 return AppAssertionCredentials(scopes, **self._kwargs)
128 208
129 209
130 class FlowProperty(db.Property): 210 class FlowProperty(db.Property):
131 """App Engine datastore Property for Flow. 211 """App Engine datastore Property for Flow.
132 212
133 Utility property that allows easy storage and retreival of an 213 Utility property that allows easy storage and retrieval of an
134 oauth2client.Flow""" 214 oauth2client.Flow"""
135 215
136 # Tell what the user type is. 216 # Tell what the user type is.
137 data_type = Flow 217 data_type = Flow
138 218
139 # For writing to datastore. 219 # For writing to datastore.
140 def get_value_for_datastore(self, model_instance): 220 def get_value_for_datastore(self, model_instance):
141 flow = super(FlowProperty, 221 flow = super(FlowProperty,
142 self).get_value_for_datastore(model_instance) 222 self).get_value_for_datastore(model_instance)
143 return db.Blob(pickle.dumps(flow)) 223 return db.Blob(pickle.dumps(flow))
144 224
145 # For reading from datastore. 225 # For reading from datastore.
146 def make_value_from_datastore(self, value): 226 def make_value_from_datastore(self, value):
147 if value is None: 227 if value is None:
148 return None 228 return None
149 return pickle.loads(value) 229 return pickle.loads(value)
150 230
151 def validate(self, value): 231 def validate(self, value):
152 if value is not None and not isinstance(value, Flow): 232 if value is not None and not isinstance(value, Flow):
153 raise db.BadValueError('Property %s must be convertible ' 233 raise db.BadValueError('Property %s must be convertible '
154 'to a FlowThreeLegged instance (%s)' % 234 'to a FlowThreeLegged instance (%s)' %
155 (self.name, value)) 235 (self.name, value))
156 return super(FlowProperty, self).validate(value) 236 return super(FlowProperty, self).validate(value)
157 237
158 def empty(self, value): 238 def empty(self, value):
159 return not value 239 return not value
160 240
161 241
242 if ndb is not None:
243 class FlowNDBProperty(ndb.PickleProperty):
244 """App Engine NDB datastore Property for Flow.
245
246 Serves the same purpose as the DB FlowProperty, but for NDB models. Since
247 PickleProperty inherits from BlobProperty, the underlying representation of
248 the data in the datastore will be the same as in the DB case.
249
250 Utility property that allows easy storage and retrieval of an
251 oauth2client.Flow
252 """
253
254 def _validate(self, value):
255 """Validates a value as a proper Flow object.
256
257 Args:
258 value: A value to be set on the property.
259
260 Raises:
261 TypeError if the value is not an instance of Flow.
262 """
263 logger.info('validate: Got type %s', type(value))
264 if value is not None and not isinstance(value, Flow):
265 raise TypeError('Property %s must be convertible to a flow '
266 'instance; received: %s.' % (self._name, value))
267
268
162 class CredentialsProperty(db.Property): 269 class CredentialsProperty(db.Property):
163 """App Engine datastore Property for Credentials. 270 """App Engine datastore Property for Credentials.
164 271
165 Utility property that allows easy storage and retrieval of 272 Utility property that allows easy storage and retrieval of
166 oath2client.Credentials 273 oath2client.Credentials
167 """ 274 """
168 275
169 # Tell what the user type is. 276 # Tell what the user type is.
170 data_type = Credentials 277 data_type = Credentials
171 278
172 # For writing to datastore. 279 # For writing to datastore.
173 def get_value_for_datastore(self, model_instance): 280 def get_value_for_datastore(self, model_instance):
174 logging.info("get: Got type " + str(type(model_instance))) 281 logger.info("get: Got type " + str(type(model_instance)))
175 cred = super(CredentialsProperty, 282 cred = super(CredentialsProperty,
176 self).get_value_for_datastore(model_instance) 283 self).get_value_for_datastore(model_instance)
177 if cred is None: 284 if cred is None:
178 cred = '' 285 cred = ''
179 else: 286 else:
180 cred = cred.to_json() 287 cred = cred.to_json()
181 return db.Blob(cred) 288 return db.Blob(cred)
182 289
183 # For reading from datastore. 290 # For reading from datastore.
184 def make_value_from_datastore(self, value): 291 def make_value_from_datastore(self, value):
185 logging.info("make: Got type " + str(type(value))) 292 logger.info("make: Got type " + str(type(value)))
186 if value is None: 293 if value is None:
187 return None 294 return None
188 if len(value) == 0: 295 if len(value) == 0:
189 return None 296 return None
190 try: 297 try:
191 credentials = Credentials.new_from_json(value) 298 credentials = Credentials.new_from_json(value)
192 except ValueError: 299 except ValueError:
193 credentials = None 300 credentials = None
194 return credentials 301 return credentials
195 302
196 def validate(self, value): 303 def validate(self, value):
197 value = super(CredentialsProperty, self).validate(value) 304 value = super(CredentialsProperty, self).validate(value)
198 logging.info("validate: Got type " + str(type(value))) 305 logger.info("validate: Got type " + str(type(value)))
199 if value is not None and not isinstance(value, Credentials): 306 if value is not None and not isinstance(value, Credentials):
200 raise db.BadValueError('Property %s must be convertible ' 307 raise db.BadValueError('Property %s must be convertible '
201 'to a Credentials instance (%s)' % 308 'to a Credentials instance (%s)' %
202 (self.name, value)) 309 (self.name, value))
203 #if value is not None and not isinstance(value, Credentials): 310 #if value is not None and not isinstance(value, Credentials):
204 # return None 311 # return None
205 return value 312 return value
206 313
207 314
315 if ndb is not None:
316 # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
317 # and subclass mechanics to use new_from_dict, to_dict,
318 # from_dict, etc.
319 class CredentialsNDBProperty(ndb.BlobProperty):
320 """App Engine NDB datastore Property for Credentials.
321
322 Serves the same purpose as the DB CredentialsProperty, but for NDB models.
323 Since CredentialsProperty stores data as a blob and this inherits from
324 BlobProperty, the data in the datastore will be the same as in the DB case.
325
326 Utility property that allows easy storage and retrieval of Credentials and
327 subclasses.
328 """
329 def _validate(self, value):
330 """Validates a value as a proper credentials object.
331
332 Args:
333 value: A value to be set on the property.
334
335 Raises:
336 TypeError if the value is not an instance of Credentials.
337 """
338 logger.info('validate: Got type %s', type(value))
339 if value is not None and not isinstance(value, Credentials):
340 raise TypeError('Property %s must be convertible to a credentials '
341 'instance; received: %s.' % (self._name, value))
342
343 def _to_base_type(self, value):
344 """Converts our validated value to a JSON serialized string.
345
346 Args:
347 value: A value to be set in the datastore.
348
349 Returns:
350 A JSON serialized version of the credential, else '' if value is None.
351 """
352 if value is None:
353 return ''
354 else:
355 return value.to_json()
356
357 def _from_base_type(self, value):
358 """Converts our stored JSON string back to the desired type.
359
360 Args:
361 value: A value from the datastore to be converted to the desired type.
362
363 Returns:
364 A deserialized Credentials (or subclass) object, else None if the
365 value can't be parsed.
366 """
367 if not value:
368 return None
369 try:
370 # Uses the from_json method of the implied class of value
371 credentials = Credentials.new_from_json(value)
372 except ValueError:
373 credentials = None
374 return credentials
375
376
208 class StorageByKeyName(Storage): 377 class StorageByKeyName(Storage):
209 """Store and retrieve a single credential to and from 378 """Store and retrieve a credential to and from the App Engine datastore.
210 the App Engine datastore.
211 379
212 This Storage helper presumes the Credentials 380 This Storage helper presumes the Credentials have been stored as a
213 have been stored as a CredenialsProperty 381 CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
214 on a datastore model class, and that entities 382 that entities are stored by key_name.
215 are stored by key_name.
216 """ 383 """
217 384
218 def __init__(self, model, key_name, property_name, cache=None): 385 @util.positional(4)
386 def __init__(self, model, key_name, property_name, cache=None, user=None):
219 """Constructor for Storage. 387 """Constructor for Storage.
220 388
221 Args: 389 Args:
222 model: db.Model, model class 390 model: db.Model or ndb.Model, model class
223 key_name: string, key name for the entity that has the credentials 391 key_name: string, key name for the entity that has the credentials
224 property_name: string, name of the property that is a CredentialsProperty 392 property_name: string, name of the property that is a CredentialsProperty
225 cache: memcache, a write-through cache to put in front of the datastore 393 or CredentialsNDBProperty.
394 cache: memcache, a write-through cache to put in front of the datastore.
395 If the model you are using is an NDB model, using a cache will be
396 redundant since the model uses an instance cache and memcache for you.
397 user: users.User object, optional. Can be used to grab user ID as a
398 key_name if no key name is specified.
226 """ 399 """
400 if key_name is None:
401 if user is None:
402 raise ValueError('StorageByKeyName called with no key name or user.')
403 key_name = user.user_id()
404
227 self._model = model 405 self._model = model
228 self._key_name = key_name 406 self._key_name = key_name
229 self._property_name = property_name 407 self._property_name = property_name
230 self._cache = cache 408 self._cache = cache
231 409
410 def _is_ndb(self):
411 """Determine whether the model of the instance is an NDB model.
412
413 Returns:
414 Boolean indicating whether or not the model is an NDB or DB model.
415 """
416 # issubclass will fail if one of the arguments is not a class, only need
417 # worry about new-style classes since ndb and db models are new-style
418 if isinstance(self._model, type):
419 if ndb is not None and issubclass(self._model, ndb.Model):
420 return True
421 elif issubclass(self._model, db.Model):
422 return False
423
424 raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
425
426 def _get_entity(self):
427 """Retrieve entity from datastore.
428
429 Uses a different model method for db or ndb models.
430
431 Returns:
432 Instance of the model corresponding to the current storage object
433 and stored using the key name of the storage object.
434 """
435 if self._is_ndb():
436 return self._model.get_by_id(self._key_name)
437 else:
438 return self._model.get_by_key_name(self._key_name)
439
440 def _delete_entity(self):
441 """Delete entity from datastore.
442
443 Attempts to delete using the key_name stored on the object, whether or not
444 the given key is in the datastore.
445 """
446 if self._is_ndb():
447 ndb.Key(self._model, self._key_name).delete()
448 else:
449 entity_key = db.Key.from_path(self._model.kind(), self._key_name)
450 db.delete(entity_key)
451
452 @db.non_transactional(allow_existing=True)
232 def locked_get(self): 453 def locked_get(self):
233 """Retrieve Credential from datastore. 454 """Retrieve Credential from datastore.
234 455
235 Returns: 456 Returns:
236 oauth2client.Credentials 457 oauth2client.Credentials
237 """ 458 """
459 credentials = None
238 if self._cache: 460 if self._cache:
239 json = self._cache.get(self._key_name) 461 json = self._cache.get(self._key_name)
240 if json: 462 if json:
241 return Credentials.new_from_json(json) 463 credentials = Credentials.new_from_json(json)
242 464 if credentials is None:
243 credential = None 465 entity = self._get_entity()
244 entity = self._model.get_by_key_name(self._key_name) 466 if entity is not None:
245 if entity is not None: 467 credentials = getattr(entity, self._property_name)
246 credential = getattr(entity, self._property_name)
247 if credential and hasattr(credential, 'set_store'):
248 credential.set_store(self)
249 if self._cache: 468 if self._cache:
250 self._cache.set(self._key_name, credentials.to_json()) 469 self._cache.set(self._key_name, credentials.to_json())
251 470
252 return credential 471 if credentials and hasattr(credentials, 'set_store'):
472 credentials.set_store(self)
473 return credentials
253 474
475 @db.non_transactional(allow_existing=True)
254 def locked_put(self, credentials): 476 def locked_put(self, credentials):
255 """Write a Credentials to the datastore. 477 """Write a Credentials to the datastore.
256 478
257 Args: 479 Args:
258 credentials: Credentials, the credentials to store. 480 credentials: Credentials, the credentials to store.
259 """ 481 """
260 entity = self._model.get_or_insert(self._key_name) 482 entity = self._model.get_or_insert(self._key_name)
261 setattr(entity, self._property_name, credentials) 483 setattr(entity, self._property_name, credentials)
262 entity.put() 484 entity.put()
263 if self._cache: 485 if self._cache:
264 self._cache.set(self._key_name, credentials.to_json()) 486 self._cache.set(self._key_name, credentials.to_json())
265 487
488 @db.non_transactional(allow_existing=True)
489 def locked_delete(self):
490 """Delete Credential from datastore."""
491
492 if self._cache:
493 self._cache.delete(self._key_name)
494
495 self._delete_entity()
496
266 497
267 class CredentialsModel(db.Model): 498 class CredentialsModel(db.Model):
268 """Storage for OAuth 2.0 Credentials 499 """Storage for OAuth 2.0 Credentials
269 500
270 Storage of the model is keyed by the user.user_id(). 501 Storage of the model is keyed by the user.user_id().
271 """ 502 """
272 credentials = CredentialsProperty() 503 credentials = CredentialsProperty()
273 504
274 505
506 if ndb is not None:
507 class CredentialsNDBModel(ndb.Model):
508 """NDB Model for storage of OAuth 2.0 Credentials
509
510 Since this model uses the same kind as CredentialsModel and has a property
511 which can serialize and deserialize Credentials correctly, it can be used
512 interchangeably with a CredentialsModel to access, insert and delete the
513 same entities. This simply provides an NDB model for interacting with the
514 same data the DB model interacts with.
515
516 Storage of the model is keyed by the user.user_id().
517 """
518 credentials = CredentialsNDBProperty()
519
520 @classmethod
521 def _get_kind(cls):
522 """Return the kind name for this class."""
523 return 'CredentialsModel'
524
525
526 def _build_state_value(request_handler, user):
527 """Composes the value for the 'state' parameter.
528
529 Packs the current request URI and an XSRF token into an opaque string that
530 can be passed to the authentication server via the 'state' parameter.
531
532 Args:
533 request_handler: webapp.RequestHandler, The request.
534 user: google.appengine.api.users.User, The current user.
535
536 Returns:
537 The state value as a string.
538 """
539 uri = request_handler.request.url
540 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
541 action_id=str(uri))
542 return uri + ':' + token
543
544
545 def _parse_state_value(state, user):
546 """Parse the value of the 'state' parameter.
547
548 Parses the value and validates the XSRF token in the state parameter.
549
550 Args:
551 state: string, The value of the state parameter.
552 user: google.appengine.api.users.User, The current user.
553
554 Raises:
555 InvalidXsrfTokenError: if the XSRF token is invalid.
556
557 Returns:
558 The redirect URI.
559 """
560 uri, token = state.rsplit(':', 1)
561 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
562 action_id=uri):
563 raise InvalidXsrfTokenError()
564
565 return uri
566
567
275 class OAuth2Decorator(object): 568 class OAuth2Decorator(object):
276 """Utility for making OAuth 2.0 easier. 569 """Utility for making OAuth 2.0 easier.
277 570
278 Instantiate and then use with oauth_required or oauth_aware 571 Instantiate and then use with oauth_required or oauth_aware
279 as decorators on webapp.RequestHandler methods. 572 as decorators on webapp.RequestHandler methods.
280 573
281 Example: 574 ::
282 575
283 decorator = OAuth2Decorator( 576 decorator = OAuth2Decorator(
284 client_id='837...ent.com', 577 client_id='837...ent.com',
285 client_secret='Qh...wwI', 578 client_secret='Qh...wwI',
286 scope='https://www.googleapis.com/auth/plus') 579 scope='https://www.googleapis.com/auth/plus')
287 580
288
289 class MainHandler(webapp.RequestHandler): 581 class MainHandler(webapp.RequestHandler):
290
291 @decorator.oauth_required 582 @decorator.oauth_required
292 def get(self): 583 def get(self):
293 http = decorator.http() 584 http = decorator.http()
294 # http is authorized with the user's Credentials and can be used 585 # http is authorized with the user's Credentials and can be used
295 # in API calls 586 # in API calls
296 587
297 """ 588 """
298 589
590 def set_credentials(self, credentials):
591 self._tls.credentials = credentials
592
593 def get_credentials(self):
594 """A thread local Credentials object.
595
596 Returns:
597 A client.Credentials object, or None if credentials hasn't been set in
598 this thread yet, which may happen when calling has_credentials inside
599 oauth_aware.
600 """
601 return getattr(self._tls, 'credentials', None)
602
603 credentials = property(get_credentials, set_credentials)
604
605 def set_flow(self, flow):
606 self._tls.flow = flow
607
608 def get_flow(self):
609 """A thread local Flow object.
610
611 Returns:
612 A credentials.Flow object, or None if the flow hasn't been set in this
613 thread yet, which happens in _create_flow() since Flows are created
614 lazily.
615 """
616 return getattr(self._tls, 'flow', None)
617
618 flow = property(get_flow, set_flow)
619
620
621 @util.positional(4)
299 def __init__(self, client_id, client_secret, scope, 622 def __init__(self, client_id, client_secret, scope,
300 auth_uri='https://accounts.google.com/o/oauth2/auth', 623 auth_uri=GOOGLE_AUTH_URI,
301 token_uri='https://accounts.google.com/o/oauth2/token', 624 token_uri=GOOGLE_TOKEN_URI,
302 message=None, **kwargs): 625 revoke_uri=GOOGLE_REVOKE_URI,
626 user_agent=None,
627 message=None,
628 callback_path='/oauth2callback',
629 token_response_param=None,
630 _storage_class=StorageByKeyName,
631 _credentials_class=CredentialsModel,
632 _credentials_property_name='credentials',
633 **kwargs):
303 634
304 """Constructor for OAuth2Decorator 635 """Constructor for OAuth2Decorator
305 636
306 Args: 637 Args:
307 client_id: string, client identifier. 638 client_id: string, client identifier.
308 client_secret: string client secret. 639 client_secret: string client secret.
309 scope: string or list of strings, scope(s) of the credentials being 640 scope: string or iterable of strings, scope(s) of the credentials being
310 requested. 641 requested.
311 auth_uri: string, URI for authorization endpoint. For convenience 642 auth_uri: string, URI for authorization endpoint. For convenience
312 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 643 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
313 token_uri: string, URI for token endpoint. For convenience 644 token_uri: string, URI for token endpoint. For convenience
314 defaults to Google's endpoints but any OAuth 2.0 provider can be used. 645 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
646 revoke_uri: string, URI for revoke endpoint. For convenience
647 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
648 user_agent: string, User agent of your application, default to None.
315 message: Message to display if there are problems with the OAuth 2.0 649 message: Message to display if there are problems with the OAuth 2.0
316 configuration. The message may contain HTML and will be presented on the 650 configuration. The message may contain HTML and will be presented on the
317 web interface for any method that uses the decorator. 651 web interface for any method that uses the decorator.
318 **kwargs: dict, Keyword arguments are be passed along as kwargs to the 652 callback_path: string, The absolute path to use as the callback URI. Note
319 OAuth2WebServerFlow constructor. 653 that this must match up with the URI given when registering the
654 application in the APIs Console.
655 token_response_param: string. If provided, the full JSON response
656 to the access token request will be encoded and included in this query
657 parameter in the callback URI. This is useful with providers (e.g.
658 wordpress.com) that include extra fields that the client may want.
659 _storage_class: "Protected" keyword argument not typically provided to
660 this constructor. A storage class to aid in storing a Credentials object
661 for a user in the datastore. Defaults to StorageByKeyName.
662 _credentials_class: "Protected" keyword argument not typically provided to
663 this constructor. A db or ndb Model class to hold credentials. Defaults
664 to CredentialsModel.
665 _credentials_property_name: "Protected" keyword argument not typically
666 provided to this constructor. A string indicating the name of the field
667 on the _credentials_class where a Credentials object will be stored.
668 Defaults to 'credentials'.
669 **kwargs: dict, Keyword arguments are passed along as kwargs to
670 the OAuth2WebServerFlow constructor.
671
320 """ 672 """
321 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, None, 673 self._tls = threading.local()
322 auth_uri, token_uri, **kwargs) 674 self.flow = None
323 self.credentials = None 675 self.credentials = None
324 self._request_handler = None 676 self._client_id = client_id
677 self._client_secret = client_secret
678 self._scope = util.scopes_to_string(scope)
679 self._auth_uri = auth_uri
680 self._token_uri = token_uri
681 self._revoke_uri = revoke_uri
682 self._user_agent = user_agent
683 self._kwargs = kwargs
325 self._message = message 684 self._message = message
326 self._in_error = False 685 self._in_error = False
686 self._callback_path = callback_path
687 self._token_response_param = token_response_param
688 self._storage_class = _storage_class
689 self._credentials_class = _credentials_class
690 self._credentials_property_name = _credentials_property_name
327 691
328 def _display_error_message(self, request_handler): 692 def _display_error_message(self, request_handler):
329 request_handler.response.out.write('<html><body>') 693 request_handler.response.out.write('<html><body>')
330 request_handler.response.out.write(self._message) 694 request_handler.response.out.write(_safe_html(self._message))
331 request_handler.response.out.write('</body></html>') 695 request_handler.response.out.write('</body></html>')
332 696
333 def oauth_required(self, method): 697 def oauth_required(self, method):
334 """Decorator that starts the OAuth 2.0 dance. 698 """Decorator that starts the OAuth 2.0 dance.
335 699
336 Starts the OAuth dance for the logged in user if they haven't already 700 Starts the OAuth dance for the logged in user if they haven't already
337 granted access for this application. 701 granted access for this application.
338 702
339 Args: 703 Args:
340 method: callable, to be decorated method of a webapp.RequestHandler 704 method: callable, to be decorated method of a webapp.RequestHandler
341 instance. 705 instance.
342 """ 706 """
343 707
344 def check_oauth(request_handler, *args): 708 def check_oauth(request_handler, *args, **kwargs):
345 if self._in_error: 709 if self._in_error:
346 self._display_error_message(request_handler) 710 self._display_error_message(request_handler)
347 return 711 return
348 712
349 user = users.get_current_user() 713 user = users.get_current_user()
350 # Don't use @login_decorator as this could be used in a POST request. 714 # Don't use @login_decorator as this could be used in a POST request.
351 if not user: 715 if not user:
352 request_handler.redirect(users.create_login_url( 716 request_handler.redirect(users.create_login_url(
353 request_handler.request.uri)) 717 request_handler.request.uri))
354 return 718 return
719
720 self._create_flow(request_handler)
721
355 # Store the request URI in 'state' so we can use it later 722 # Store the request URI in 'state' so we can use it later
356 self.flow.params['state'] = request_handler.request.url 723 self.flow.params['state'] = _build_state_value(request_handler, user)
357 self._request_handler = request_handler 724 self.credentials = self._storage_class(
358 self.credentials = StorageByKeyName( 725 self._credentials_class, None,
359 CredentialsModel, user.user_id(), 'credentials').get() 726 self._credentials_property_name, user=user).get()
360 727
361 if not self.has_credentials(): 728 if not self.has_credentials():
362 return request_handler.redirect(self.authorize_url()) 729 return request_handler.redirect(self.authorize_url())
363 try: 730 try:
364 method(request_handler, *args) 731 resp = method(request_handler, *args, **kwargs)
365 except AccessTokenRefreshError: 732 except AccessTokenRefreshError:
366 return request_handler.redirect(self.authorize_url()) 733 return request_handler.redirect(self.authorize_url())
734 finally:
735 self.credentials = None
736 return resp
367 737
368 return check_oauth 738 return check_oauth
369 739
740 def _create_flow(self, request_handler):
741 """Create the Flow object.
742
743 The Flow is calculated lazily since we don't know where this app is
744 running until it receives a request, at which point redirect_uri can be
745 calculated and then the Flow object can be constructed.
746
747 Args:
748 request_handler: webapp.RequestHandler, the request handler.
749 """
750 if self.flow is None:
751 redirect_uri = request_handler.request.relative_url(
752 self._callback_path) # Usually /oauth2callback
753 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
754 self._scope, redirect_uri=redirect_uri,
755 user_agent=self._user_agent,
756 auth_uri=self._auth_uri,
757 token_uri=self._token_uri,
758 revoke_uri=self._revoke_uri,
759 **self._kwargs)
760
370 def oauth_aware(self, method): 761 def oauth_aware(self, method):
371 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it. 762 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
372 763
373 Does all the setup for the OAuth dance, but doesn't initiate it. 764 Does all the setup for the OAuth dance, but doesn't initiate it.
374 This decorator is useful if you want to create a page that knows 765 This decorator is useful if you want to create a page that knows
375 whether or not the user has granted access to this application. 766 whether or not the user has granted access to this application.
376 From within a method decorated with @oauth_aware the has_credentials() 767 From within a method decorated with @oauth_aware the has_credentials()
377 and authorize_url() methods can be called. 768 and authorize_url() methods can be called.
378 769
379 Args: 770 Args:
380 method: callable, to be decorated method of a webapp.RequestHandler 771 method: callable, to be decorated method of a webapp.RequestHandler
381 instance. 772 instance.
382 """ 773 """
383 774
384 def setup_oauth(request_handler, *args): 775 def setup_oauth(request_handler, *args, **kwargs):
385 if self._in_error: 776 if self._in_error:
386 self._display_error_message(request_handler) 777 self._display_error_message(request_handler)
387 return 778 return
388 779
389 user = users.get_current_user() 780 user = users.get_current_user()
390 # Don't use @login_decorator as this could be used in a POST request. 781 # Don't use @login_decorator as this could be used in a POST request.
391 if not user: 782 if not user:
392 request_handler.redirect(users.create_login_url( 783 request_handler.redirect(users.create_login_url(
393 request_handler.request.uri)) 784 request_handler.request.uri))
394 return 785 return
395 786
787 self._create_flow(request_handler)
396 788
397 self.flow.params['state'] = request_handler.request.url 789 self.flow.params['state'] = _build_state_value(request_handler, user)
398 self._request_handler = request_handler 790 self.credentials = self._storage_class(
399 self.credentials = StorageByKeyName( 791 self._credentials_class, None,
400 CredentialsModel, user.user_id(), 'credentials').get() 792 self._credentials_property_name, user=user).get()
401 method(request_handler, *args) 793 try:
794 resp = method(request_handler, *args, **kwargs)
795 finally:
796 self.credentials = None
797 return resp
402 return setup_oauth 798 return setup_oauth
403 799
800
404 def has_credentials(self): 801 def has_credentials(self):
405 """True if for the logged in user there are valid access Credentials. 802 """True if for the logged in user there are valid access Credentials.
406 803
407 Must only be called from with a webapp.RequestHandler subclassed method 804 Must only be called from with a webapp.RequestHandler subclassed method
408 that had been decorated with either @oauth_required or @oauth_aware. 805 that had been decorated with either @oauth_required or @oauth_aware.
409 """ 806 """
410 return self.credentials is not None and not self.credentials.invalid 807 return self.credentials is not None and not self.credentials.invalid
411 808
412 def authorize_url(self): 809 def authorize_url(self):
413 """Returns the URL to start the OAuth dance. 810 """Returns the URL to start the OAuth dance.
414 811
415 Must only be called from with a webapp.RequestHandler subclassed method 812 Must only be called from with a webapp.RequestHandler subclassed method
416 that had been decorated with either @oauth_required or @oauth_aware. 813 that had been decorated with either @oauth_required or @oauth_aware.
417 """ 814 """
418 callback = self._request_handler.request.relative_url('/oauth2callback') 815 url = self.flow.step1_get_authorize_url()
419 url = self.flow.step1_get_authorize_url(callback) 816 return str(url)
420 user = users.get_current_user()
421 memcache.set(user.user_id(), pickle.dumps(self.flow),
422 namespace=OAUTH2CLIENT_NAMESPACE)
423 return url
424 817
425 def http(self): 818 def http(self, *args, **kwargs):
426 """Returns an authorized http instance. 819 """Returns an authorized http instance.
427 820
428 Must only be called from within an @oauth_required decorated method, or 821 Must only be called from within an @oauth_required decorated method, or
429 from within an @oauth_aware decorated method where has_credentials() 822 from within an @oauth_aware decorated method where has_credentials()
430 returns True. 823 returns True.
824
825 Args:
826 *args: Positional arguments passed to httplib2.Http constructor.
827 **kwargs: Positional arguments passed to httplib2.Http constructor.
431 """ 828 """
432 return self.credentials.authorize(httplib2.Http()) 829 return self.credentials.authorize(httplib2.Http(*args, **kwargs))
830
831 @property
832 def callback_path(self):
833 """The absolute path where the callback will occur.
834
835 Note this is the absolute path, not the absolute URI, that will be
836 calculated by the decorator at runtime. See callback_handler() for how this
837 should be used.
838
839 Returns:
840 The callback path as a string.
841 """
842 return self._callback_path
843
844
845 def callback_handler(self):
846 """RequestHandler for the OAuth 2.0 redirect callback.
847
848 Usage::
849
850 app = webapp.WSGIApplication([
851 ('/index', MyIndexHandler),
852 ...,
853 (decorator.callback_path, decorator.callback_handler())
854 ])
855
856 Returns:
857 A webapp.RequestHandler that handles the redirect back from the
858 server during the OAuth 2.0 dance.
859 """
860 decorator = self
861
862 class OAuth2Handler(webapp.RequestHandler):
863 """Handler for the redirect_uri of the OAuth 2.0 dance."""
864
865 @login_required
866 def get(self):
867 error = self.request.get('error')
868 if error:
869 errormsg = self.request.get('error_description', error)
870 self.response.out.write(
871 'The authorization request failed: %s' % _safe_html(errormsg))
872 else:
873 user = users.get_current_user()
874 decorator._create_flow(self)
875 credentials = decorator.flow.step2_exchange(self.request.params)
876 decorator._storage_class(
877 decorator._credentials_class, None,
878 decorator._credentials_property_name, user=user).put(credentials)
879 redirect_uri = _parse_state_value(str(self.request.get('state')),
880 user)
881
882 if decorator._token_response_param and credentials.token_response:
883 resp_json = json.dumps(credentials.token_response)
884 redirect_uri = util._add_query_parameter(
885 redirect_uri, decorator._token_response_param, resp_json)
886
887 self.redirect(redirect_uri)
888
889 return OAuth2Handler
890
891 def callback_application(self):
892 """WSGI application for handling the OAuth 2.0 redirect callback.
893
894 If you need finer grained control use `callback_handler` which returns just
895 the webapp.RequestHandler.
896
897 Returns:
898 A webapp.WSGIApplication that handles the redirect back from the
899 server during the OAuth 2.0 dance.
900 """
901 return webapp.WSGIApplication([
902 (self.callback_path, self.callback_handler())
903 ])
433 904
434 905
435 class OAuth2DecoratorFromClientSecrets(OAuth2Decorator): 906 class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
436 """An OAuth2Decorator that builds from a clientsecrets file. 907 """An OAuth2Decorator that builds from a clientsecrets file.
437 908
438 Uses a clientsecrets file as the source for all the information when 909 Uses a clientsecrets file as the source for all the information when
439 constructing an OAuth2Decorator. 910 constructing an OAuth2Decorator.
440 911
441 Example: 912 ::
442 913
443 decorator = OAuth2DecoratorFromClientSecrets( 914 decorator = OAuth2DecoratorFromClientSecrets(
444 os.path.join(os.path.dirname(__file__), 'client_secrets.json') 915 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
445 scope='https://www.googleapis.com/auth/plus') 916 scope='https://www.googleapis.com/auth/plus')
446 917
447
448 class MainHandler(webapp.RequestHandler): 918 class MainHandler(webapp.RequestHandler):
449
450 @decorator.oauth_required 919 @decorator.oauth_required
451 def get(self): 920 def get(self):
452 http = decorator.http() 921 http = decorator.http()
453 # http is authorized with the user's Credentials and can be used 922 # http is authorized with the user's Credentials and can be used
454 # in API calls 923 # in API calls
924
455 """ 925 """
456 926
457 def __init__(self, filename, scope, message=None): 927 @util.positional(3)
928 def __init__(self, filename, scope, message=None, cache=None, **kwargs):
458 """Constructor 929 """Constructor
459 930
460 Args: 931 Args:
461 filename: string, File name of client secrets. 932 filename: string, File name of client secrets.
462 scope: string, Space separated list of scopes. 933 scope: string or iterable of strings, scope(s) of the credentials being
934 requested.
463 message: string, A friendly string to display to the user if the 935 message: string, A friendly string to display to the user if the
464 clientsecrets file is missing or invalid. The message may contain HTML a nd 936 clientsecrets file is missing or invalid. The message may contain HTML
465 will be presented on the web interface for any method that uses the 937 and will be presented on the web interface for any method that uses the
466 decorator. 938 decorator.
939 cache: An optional cache service client that implements get() and set()
940 methods. See clientsecrets.loadfile() for details.
941 **kwargs: dict, Keyword arguments are passed along as kwargs to
942 the OAuth2WebServerFlow constructor.
467 """ 943 """
468 try: 944 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
469 client_type, client_info = clientsecrets.loadfile(filename) 945 if client_type not in [
470 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLE D]: 946 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
471 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this O Auth 2.0 flow.') 947 raise InvalidClientSecretsError(
472 super(OAuth2DecoratorFromClientSecrets, 948 "OAuth2Decorator doesn't support this OAuth 2.0 flow.")
473 self).__init__( 949 constructor_kwargs = dict(kwargs)
474 client_info['client_id'], 950 constructor_kwargs.update({
475 client_info['client_secret'], 951 'auth_uri': client_info['auth_uri'],
476 scope, 952 'token_uri': client_info['token_uri'],
477 client_info['auth_uri'], 953 'message': message,
478 client_info['token_uri'], 954 })
479 message) 955 revoke_uri = client_info.get('revoke_uri')
480 except clientsecrets.InvalidClientSecretsError: 956 if revoke_uri is not None:
481 self._in_error = True 957 constructor_kwargs['revoke_uri'] = revoke_uri
958 super(OAuth2DecoratorFromClientSecrets, self).__init__(
959 client_info['client_id'], client_info['client_secret'],
960 scope, **constructor_kwargs)
482 if message is not None: 961 if message is not None:
483 self._message = message 962 self._message = message
484 else: 963 else:
485 self._message = "Please configure your application for OAuth 2.0" 964 self._message = 'Please configure your application for OAuth 2.0.'
486 965
487 966
488 def oauth2decorator_from_clientsecrets(filename, scope, message=None): 967 @util.positional(2)
968 def oauth2decorator_from_clientsecrets(filename, scope,
969 message=None, cache=None):
489 """Creates an OAuth2Decorator populated from a clientsecrets file. 970 """Creates an OAuth2Decorator populated from a clientsecrets file.
490 971
491 Args: 972 Args:
492 filename: string, File name of client secrets. 973 filename: string, File name of client secrets.
493 scope: string, Space separated list of scopes. 974 scope: string or list of strings, scope(s) of the credentials being
975 requested.
494 message: string, A friendly string to display to the user if the 976 message: string, A friendly string to display to the user if the
495 clientsecrets file is missing or invalid. The message may contain HTML and 977 clientsecrets file is missing or invalid. The message may contain HTML and
496 will be presented on the web interface for any method that uses the 978 will be presented on the web interface for any method that uses the
497 decorator. 979 decorator.
980 cache: An optional cache service client that implements get() and set()
981 methods. See clientsecrets.loadfile() for details.
498 982
499 Returns: An OAuth2Decorator 983 Returns: An OAuth2Decorator
500 984
501 """ 985 """
502 return OAuth2DecoratorFromClientSecrets(filename, scope, message) 986 return OAuth2DecoratorFromClientSecrets(filename, scope,
503 987 message=message, cache=cache)
504
505 class OAuth2Handler(webapp.RequestHandler):
506 """Handler for the redirect_uri of the OAuth 2.0 dance."""
507
508 @login_required
509 def get(self):
510 error = self.request.get('error')
511 if error:
512 errormsg = self.request.get('error_description', error)
513 self.response.out.write(
514 'The authorization request failed: %s' % errormsg)
515 else:
516 user = users.get_current_user()
517 flow = pickle.loads(memcache.get(user.user_id(),
518 namespace=OAUTH2CLIENT_NAMESPACE))
519 # This code should be ammended with application specific error
520 # handling. The following cases should be considered:
521 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
522 # 2. What if the step2_exchange fails?
523 if flow:
524 credentials = flow.step2_exchange(self.request.params)
525 StorageByKeyName(
526 CredentialsModel, user.user_id(), 'credentials').put(credentials)
527 self.redirect(str(self.request.get('state')))
528 else:
529 # TODO Add error handling here.
530 pass
531
532
533 application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
534
535
536 def main():
537 run_wsgi_app(application)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698