blob: 5439a354b46f9db72e3282968da1a7d4a7709f30 [file] [log] [blame]
Joe Gregorio695fdc12011-01-16 16:46:55 -05001# Copyright (C) 2010 Google Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Utilities for Google App Engine
16
Joe Gregorio7c22ab22011-02-16 15:32:39 -050017Utilities for making it easier to use OAuth 2.0 on Google App Engine.
Joe Gregorio695fdc12011-01-16 16:46:55 -050018"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorio1daa71b2011-09-15 18:12:14 -040022import base64
Joe Gregorio77254c12012-08-27 14:13:22 -040023import cgi
Joe Gregorio432f17e2011-05-22 23:18:00 -040024import httplib2
Joe Gregorio1daa71b2011-09-15 18:12:14 -040025import logging
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040026import os
Joe Gregorio695fdc12011-01-16 16:46:55 -050027import pickle
JacobMoshenko8e905102011-06-20 09:53:10 -040028import time
JacobMoshenko8e905102011-06-20 09:53:10 -040029
Joe Gregoriod84d6b82012-02-28 14:53:00 -050030from google.appengine.api import app_identity
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040031from google.appengine.api import memcache
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040032from google.appengine.api import users
Joe Gregorio432f17e2011-05-22 23:18:00 -040033from google.appengine.ext import db
34from google.appengine.ext import webapp
35from google.appengine.ext.webapp.util import login_required
36from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040037from oauth2client import clientsecrets
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040038from oauth2client import util
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040039from oauth2client import xsrfutil
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040040from oauth2client.anyjson import simplejson
41from oauth2client.client import AccessTokenRefreshError
42from oauth2client.client import AssertionCredentials
43from oauth2client.client import Credentials
44from oauth2client.client import Flow
45from oauth2client.client import OAuth2WebServerFlow
46from oauth2client.client import Storage
Joe Gregorioa19f3a72012-07-11 15:35:35 -040047
48logger = logging.getLogger(__name__)
49
Joe Gregorio432f17e2011-05-22 23:18:00 -040050OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050051
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040052XSRF_MEMCACHE_ID = 'xsrf_secret_key'
53
JacobMoshenko8e905102011-06-20 09:53:10 -040054
Joe Gregorio77254c12012-08-27 14:13:22 -040055def _safe_html(s):
56 """Escape text to make it safe to display.
57
58 Args:
59 s: string, The text to escape.
60
61 Returns:
62 The escaped text as a string.
63 """
64 return cgi.escape(s, quote=1).replace("'", ''')
65
66
Joe Gregoriof08a4982011-10-07 13:11:16 -040067class InvalidClientSecretsError(Exception):
68 """The client_secrets.json file is malformed or missing required fields."""
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040069
70
71class InvalidXsrfTokenError(Exception):
72 """The XSRF token is invalid or expired."""
73
74
75class SiteXsrfSecretKey(db.Model):
76 """Storage for the sites XSRF secret key.
77
78 There will only be one instance stored of this model, the one used for the
79 site. """
80 secret = db.StringProperty()
81
82
83def _generate_new_xsrf_secret_key():
84 """Returns a random XSRF secret key.
85 """
86 return os.urandom(16).encode("hex")
87
88
89def xsrf_secret_key():
90 """Return the secret key for use for XSRF protection.
91
92 If the Site entity does not have a secret key, this method will also create
93 one and persist it.
94
95 Returns:
96 The secret key.
97 """
98 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
99 if not secret:
100 # Load the one and only instance of SiteXsrfSecretKey.
101 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
102 if not model.secret:
103 model.secret = _generate_new_xsrf_secret_key()
104 model.put()
105 secret = model.secret
106 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
107
108 return str(secret)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400109
110
JacobMoshenko8e905102011-06-20 09:53:10 -0400111class AppAssertionCredentials(AssertionCredentials):
112 """Credentials object for App Engine Assertion Grants
113
114 This object will allow an App Engine application to identify itself to Google
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400115 and other OAuth 2.0 servers that can verify assertions. It can be used for the
116 purpose of accessing data stored under an account assigned to the App Engine
117 application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -0400118
119 This credential does not require a flow to instantiate because it represents
120 a two legged flow, and therefore has all of the required information to
121 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -0400122 """
123
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400124 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500125 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -0400126 """Constructor for AppAssertionCredentials
127
128 Args:
Joe Gregoriofd08e432012-08-09 14:17:41 -0400129 scope: string or list of strings, scope(s) of the credentials being
130 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400131 """
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500132 if type(scope) is list:
133 scope = ' '.join(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400134 self.scope = scope
JacobMoshenko8e905102011-06-20 09:53:10 -0400135
136 super(AppAssertionCredentials, self).__init__(
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400137 'ignored' # assertion_type is ignore in this subclass.
138 )
JacobMoshenko8e905102011-06-20 09:53:10 -0400139
Joe Gregorio562b7312011-09-15 09:06:38 -0400140 @classmethod
141 def from_json(cls, json):
142 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500143 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400144
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500145 def _refresh(self, http_request):
146 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400147
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500148 Since the underlying App Engine app_identity implementation does its own
149 caching we can skip all the storage hoops and just to a refresh using the
150 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400151
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500152 Args:
153 http_request: callable, a callable that matches the method signature of
154 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400155
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500156 Raises:
157 AccessTokenRefreshError: When the refresh fails.
158 """
159 try:
160 (token, _) = app_identity.get_access_token(self.scope)
161 except app_identity.Error, e:
162 raise AccessTokenRefreshError(str(e))
163 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400164
165
Joe Gregorio695fdc12011-01-16 16:46:55 -0500166class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500167 """App Engine datastore Property for Flow.
168
169 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500170 oauth2client.Flow"""
171
172 # Tell what the user type is.
173 data_type = Flow
174
175 # For writing to datastore.
176 def get_value_for_datastore(self, model_instance):
177 flow = super(FlowProperty,
178 self).get_value_for_datastore(model_instance)
179 return db.Blob(pickle.dumps(flow))
180
181 # For reading from datastore.
182 def make_value_from_datastore(self, value):
183 if value is None:
184 return None
185 return pickle.loads(value)
186
187 def validate(self, value):
188 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400189 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500190 'to a FlowThreeLegged instance (%s)' %
191 (self.name, value))
192 return super(FlowProperty, self).validate(value)
193
194 def empty(self, value):
195 return not value
196
197
198class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500199 """App Engine datastore Property for Credentials.
200
201 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500202 oath2client.Credentials
203 """
204
205 # Tell what the user type is.
206 data_type = Credentials
207
208 # For writing to datastore.
209 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400210 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500211 cred = super(CredentialsProperty,
212 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400213 if cred is None:
214 cred = ''
215 else:
216 cred = cred.to_json()
217 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500218
219 # For reading from datastore.
220 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400221 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500222 if value is None:
223 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400224 if len(value) == 0:
225 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400226 try:
227 credentials = Credentials.new_from_json(value)
228 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400229 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400230 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500231
232 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400233 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400234 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500235 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400236 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400237 'to a Credentials instance (%s)' %
238 (self.name, value))
239 #if value is not None and not isinstance(value, Credentials):
240 # return None
241 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500242
243
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500244class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500245 """Store and retrieve a single credential to and from
246 the App Engine datastore.
247
248 This Storage helper presumes the Credentials
249 have been stored as a CredenialsProperty
250 on a datastore model class, and that entities
251 are stored by key_name.
252 """
253
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400254 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400255 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500256 """Constructor for Storage.
257
258 Args:
259 model: db.Model, model class
260 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400261 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400262 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500263 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500264 self._model = model
265 self._key_name = key_name
266 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400267 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500268
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400269 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500270 """Retrieve Credential from datastore.
271
272 Returns:
273 oauth2client.Credentials
274 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400275 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400276 json = self._cache.get(self._key_name)
277 if json:
278 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500279
280 credential = None
281 entity = self._model.get_by_key_name(self._key_name)
282 if entity is not None:
283 credential = getattr(entity, self._property_name)
284 if credential and hasattr(credential, 'set_store'):
285 credential.set_store(self)
286 if self._cache:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400287 self._cache.set(self._key_name, credential.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400288
Joe Gregorio695fdc12011-01-16 16:46:55 -0500289 return credential
290
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400291 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500292 """Write a Credentials to the datastore.
293
294 Args:
295 credentials: Credentials, the credentials to store.
296 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500297 entity = self._model.get_or_insert(self._key_name)
298 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500299 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400300 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400301 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400302
Joe Gregorioec75dc12012-02-06 13:40:42 -0500303 def locked_delete(self):
304 """Delete Credential from datastore."""
305
306 if self._cache:
307 self._cache.delete(self._key_name)
308
309 entity = self._model.get_by_key_name(self._key_name)
310 if entity is not None:
311 entity.delete()
312
Joe Gregorio432f17e2011-05-22 23:18:00 -0400313
314class CredentialsModel(db.Model):
315 """Storage for OAuth 2.0 Credentials
316
317 Storage of the model is keyed by the user.user_id().
318 """
319 credentials = CredentialsProperty()
320
321
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400322def _build_state_value(request_handler, user):
323 """Composes the value for the 'state' parameter.
324
325 Packs the current request URI and an XSRF token into an opaque string that
326 can be passed to the authentication server via the 'state' parameter.
327
328 Args:
329 request_handler: webapp.RequestHandler, The request.
330 user: google.appengine.api.users.User, The current user.
331
332 Returns:
333 The state value as a string.
334 """
335 uri = request_handler.request.url
336 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
337 action_id=str(uri))
338 return uri + ':' + token
339
340
341def _parse_state_value(state, user):
342 """Parse the value of the 'state' parameter.
343
344 Parses the value and validates the XSRF token in the state parameter.
345
346 Args:
347 state: string, The value of the state parameter.
348 user: google.appengine.api.users.User, The current user.
349
350 Raises:
351 InvalidXsrfTokenError: if the XSRF token is invalid.
352
353 Returns:
354 The redirect URI.
355 """
356 uri, token = state.rsplit(':', 1)
357 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
358 action_id=uri):
359 raise InvalidXsrfTokenError()
360
361 return uri
362
363
Joe Gregorio432f17e2011-05-22 23:18:00 -0400364class OAuth2Decorator(object):
365 """Utility for making OAuth 2.0 easier.
366
367 Instantiate and then use with oauth_required or oauth_aware
368 as decorators on webapp.RequestHandler methods.
369
370 Example:
371
372 decorator = OAuth2Decorator(
373 client_id='837...ent.com',
374 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500375 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400376
377
378 class MainHandler(webapp.RequestHandler):
379
380 @decorator.oauth_required
381 def get(self):
382 http = decorator.http()
383 # http is authorized with the user's Credentials and can be used
384 # in API calls
385
386 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400387
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400388 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400389 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400390 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400391 token_uri='https://accounts.google.com/o/oauth2/token',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100392 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400393 message=None,
394 callback_path='/oauth2callback',
395 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400396
397 """Constructor for OAuth2Decorator
398
399 Args:
400 client_id: string, client identifier.
401 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400402 scope: string or list of strings, scope(s) of the credentials being
403 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400404 auth_uri: string, URI for authorization endpoint. For convenience
405 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
406 token_uri: string, URI for token endpoint. For convenience
407 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100408 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400409 message: Message to display if there are problems with the OAuth 2.0
410 configuration. The message may contain HTML and will be presented on the
411 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400412 callback_path: string, The absolute path to use as the callback URI. Note
413 that this must match up with the URI given when registering the
414 application in the APIs Console.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500415 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
416 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400417 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400418 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400419 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400420 self._client_id = client_id
421 self._client_secret = client_secret
422 self._scope = scope
423 self._auth_uri = auth_uri
424 self._token_uri = token_uri
425 self._user_agent = user_agent
426 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400427 self._message = message
428 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400429 self._callback_path = callback_path
Joe Gregoriof08a4982011-10-07 13:11:16 -0400430
431 def _display_error_message(self, request_handler):
432 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400433 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400434 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400435
436 def oauth_required(self, method):
437 """Decorator that starts the OAuth 2.0 dance.
438
439 Starts the OAuth dance for the logged in user if they haven't already
440 granted access for this application.
441
442 Args:
443 method: callable, to be decorated method of a webapp.RequestHandler
444 instance.
445 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400446
Joe Gregorio17774972012-03-01 11:11:59 -0500447 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400448 if self._in_error:
449 self._display_error_message(request_handler)
450 return
451
Joe Gregoriof427c532011-06-13 09:35:26 -0400452 user = users.get_current_user()
453 # Don't use @login_decorator as this could be used in a POST request.
454 if not user:
455 request_handler.redirect(users.create_login_url(
456 request_handler.request.uri))
457 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400458
459 self._create_flow(request_handler)
460
Joe Gregorio432f17e2011-05-22 23:18:00 -0400461 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400462 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400463 self.credentials = StorageByKeyName(
464 CredentialsModel, user.user_id(), 'credentials').get()
465
466 if not self.has_credentials():
467 return request_handler.redirect(self.authorize_url())
468 try:
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400469 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400470 except AccessTokenRefreshError:
471 return request_handler.redirect(self.authorize_url())
472
473 return check_oauth
474
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400475 def _create_flow(self, request_handler):
476 """Create the Flow object.
477
478 The Flow is calculated lazily since we don't know where this app is
479 running until it receives a request, at which point redirect_uri can be
480 calculated and then the Flow object can be constructed.
481
482 Args:
483 request_handler: webapp.RequestHandler, the request handler.
484 """
485 if self.flow is None:
486 redirect_uri = request_handler.request.relative_url(
487 self._callback_path) # Usually /oauth2callback
488 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
489 self._scope, redirect_uri=redirect_uri,
490 user_agent=self._user_agent,
491 auth_uri=self._auth_uri,
492 token_uri=self._token_uri, **self._kwargs)
493
494
Joe Gregorio432f17e2011-05-22 23:18:00 -0400495 def oauth_aware(self, method):
496 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
497
498 Does all the setup for the OAuth dance, but doesn't initiate it.
499 This decorator is useful if you want to create a page that knows
500 whether or not the user has granted access to this application.
501 From within a method decorated with @oauth_aware the has_credentials()
502 and authorize_url() methods can be called.
503
504 Args:
505 method: callable, to be decorated method of a webapp.RequestHandler
506 instance.
507 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400508
Joe Gregorio17774972012-03-01 11:11:59 -0500509 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400510 if self._in_error:
511 self._display_error_message(request_handler)
512 return
513
Joe Gregoriof427c532011-06-13 09:35:26 -0400514 user = users.get_current_user()
515 # Don't use @login_decorator as this could be used in a POST request.
516 if not user:
517 request_handler.redirect(users.create_login_url(
518 request_handler.request.uri))
519 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400520
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400521 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400522
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400523 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400524 self.credentials = StorageByKeyName(
525 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400526 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400527 return setup_oauth
528
529 def has_credentials(self):
530 """True if for the logged in user there are valid access Credentials.
531
532 Must only be called from with a webapp.RequestHandler subclassed method
533 that had been decorated with either @oauth_required or @oauth_aware.
534 """
535 return self.credentials is not None and not self.credentials.invalid
536
537 def authorize_url(self):
538 """Returns the URL to start the OAuth dance.
539
540 Must only be called from with a webapp.RequestHandler subclassed method
541 that had been decorated with either @oauth_required or @oauth_aware.
542 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400543 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500544 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400545
546 def http(self):
547 """Returns an authorized http instance.
548
549 Must only be called from within an @oauth_required decorated method, or
550 from within an @oauth_aware decorated method where has_credentials()
551 returns True.
552 """
553 return self.credentials.authorize(httplib2.Http())
554
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400555 @property
556 def callback_path(self):
557 """The absolute path where the callback will occur.
558
559 Note this is the absolute path, not the absolute URI, that will be
560 calculated by the decorator at runtime. See callback_handler() for how this
561 should be used.
562
563 Returns:
564 The callback path as a string.
565 """
566 return self._callback_path
567
568
569 def callback_handler(self):
570 """RequestHandler for the OAuth 2.0 redirect callback.
571
572 Usage:
573 app = webapp.WSGIApplication([
574 ('/index', MyIndexHandler),
575 ...,
576 (decorator.callback_path, decorator.callback_handler())
577 ])
578
579 Returns:
580 A webapp.RequestHandler that handles the redirect back from the
581 server during the OAuth 2.0 dance.
582 """
583 decorator = self
584
585 class OAuth2Handler(webapp.RequestHandler):
586 """Handler for the redirect_uri of the OAuth 2.0 dance."""
587
588 @login_required
589 def get(self):
590 error = self.request.get('error')
591 if error:
592 errormsg = self.request.get('error_description', error)
593 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400594 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400595 else:
596 user = users.get_current_user()
597 decorator._create_flow(self)
598 credentials = decorator.flow.step2_exchange(self.request.params)
599 StorageByKeyName(
600 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400601 redirect_uri = _parse_state_value(str(self.request.get('state')),
602 user)
603 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400604
605 return OAuth2Handler
606
607 def callback_application(self):
608 """WSGI application for handling the OAuth 2.0 redirect callback.
609
610 If you need finer grained control use `callback_handler` which returns just
611 the webapp.RequestHandler.
612
613 Returns:
614 A webapp.WSGIApplication that handles the redirect back from the
615 server during the OAuth 2.0 dance.
616 """
617 return webapp.WSGIApplication([
618 (self.callback_path, self.callback_handler())
619 ])
620
Joe Gregorio432f17e2011-05-22 23:18:00 -0400621
Joe Gregoriof08a4982011-10-07 13:11:16 -0400622class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
623 """An OAuth2Decorator that builds from a clientsecrets file.
624
625 Uses a clientsecrets file as the source for all the information when
626 constructing an OAuth2Decorator.
627
628 Example:
629
630 decorator = OAuth2DecoratorFromClientSecrets(
631 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500632 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400633
634
635 class MainHandler(webapp.RequestHandler):
636
637 @decorator.oauth_required
638 def get(self):
639 http = decorator.http()
640 # http is authorized with the user's Credentials and can be used
641 # in API calls
642 """
643
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400644 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400645 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400646 """Constructor
647
648 Args:
649 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400650 scope: string or list of strings, scope(s) of the credentials being
651 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400652 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400653 clientsecrets file is missing or invalid. The message may contain HTML
654 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400655 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400656 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400657 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400658 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400659 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
660 if client_type not in [
661 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
662 raise InvalidClientSecretsError(
663 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
664 super(OAuth2DecoratorFromClientSecrets, self).__init__(
665 client_info['client_id'],
666 client_info['client_secret'],
667 scope,
668 auth_uri=client_info['auth_uri'],
669 token_uri=client_info['token_uri'],
670 message=message)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400671 if message is not None:
672 self._message = message
673 else:
674 self._message = "Please configure your application for OAuth 2.0"
675
676
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400677@util.positional(2)
678def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400679 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400680 """Creates an OAuth2Decorator populated from a clientsecrets file.
681
682 Args:
683 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400684 scope: string or list of strings, scope(s) of the credentials being
685 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400686 message: string, A friendly string to display to the user if the
687 clientsecrets file is missing or invalid. The message may contain HTML and
688 will be presented on the web interface for any method that uses the
689 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400690 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400691 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400692
693 Returns: An OAuth2Decorator
694
695 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400696 return OAuth2DecoratorFromClientSecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400697 message=message, cache=cache)