blob: e9cb17e5b4cf3c31bb9cf15af54b1180523b6bb5 [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 Gregorio432f17e2011-05-22 23:18:00 -040023import httplib2
Joe Gregorio1daa71b2011-09-15 18:12:14 -040024import logging
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040025import os
Joe Gregorio695fdc12011-01-16 16:46:55 -050026import pickle
JacobMoshenko8e905102011-06-20 09:53:10 -040027import time
JacobMoshenko8e905102011-06-20 09:53:10 -040028
Joe Gregoriod84d6b82012-02-28 14:53:00 -050029from google.appengine.api import app_identity
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040030from google.appengine.api import memcache
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040031from google.appengine.api import users
Joe Gregorio432f17e2011-05-22 23:18:00 -040032from google.appengine.ext import db
33from google.appengine.ext import webapp
34from google.appengine.ext.webapp.util import login_required
35from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040036from oauth2client import clientsecrets
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040037from oauth2client import util
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040038from oauth2client import xsrfutil
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040039from oauth2client.anyjson import simplejson
40from oauth2client.client import AccessTokenRefreshError
41from oauth2client.client import AssertionCredentials
42from oauth2client.client import Credentials
43from oauth2client.client import Flow
44from oauth2client.client import OAuth2WebServerFlow
45from oauth2client.client import Storage
Joe Gregorioa19f3a72012-07-11 15:35:35 -040046
47logger = logging.getLogger(__name__)
48
Joe Gregorio432f17e2011-05-22 23:18:00 -040049OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050050
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040051XSRF_MEMCACHE_ID = 'xsrf_secret_key'
52
JacobMoshenko8e905102011-06-20 09:53:10 -040053
Joe Gregoriof08a4982011-10-07 13:11:16 -040054class InvalidClientSecretsError(Exception):
55 """The client_secrets.json file is malformed or missing required fields."""
Joe Gregorio6ceea2d2012-08-24 11:57:58 -040056
57
58class InvalidXsrfTokenError(Exception):
59 """The XSRF token is invalid or expired."""
60
61
62class SiteXsrfSecretKey(db.Model):
63 """Storage for the sites XSRF secret key.
64
65 There will only be one instance stored of this model, the one used for the
66 site. """
67 secret = db.StringProperty()
68
69
70def _generate_new_xsrf_secret_key():
71 """Returns a random XSRF secret key.
72 """
73 return os.urandom(16).encode("hex")
74
75
76def xsrf_secret_key():
77 """Return the secret key for use for XSRF protection.
78
79 If the Site entity does not have a secret key, this method will also create
80 one and persist it.
81
82 Returns:
83 The secret key.
84 """
85 secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
86 if not secret:
87 # Load the one and only instance of SiteXsrfSecretKey.
88 model = SiteXsrfSecretKey.get_or_insert(key_name='site')
89 if not model.secret:
90 model.secret = _generate_new_xsrf_secret_key()
91 model.put()
92 secret = model.secret
93 memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
94
95 return str(secret)
Joe Gregoriof08a4982011-10-07 13:11:16 -040096
97
JacobMoshenko8e905102011-06-20 09:53:10 -040098class AppAssertionCredentials(AssertionCredentials):
99 """Credentials object for App Engine Assertion Grants
100
101 This object will allow an App Engine application to identify itself to Google
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400102 and other OAuth 2.0 servers that can verify assertions. It can be used for the
103 purpose of accessing data stored under an account assigned to the App Engine
104 application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -0400105
106 This credential does not require a flow to instantiate because it represents
107 a two legged flow, and therefore has all of the required information to
108 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -0400109 """
110
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400111 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500112 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -0400113 """Constructor for AppAssertionCredentials
114
115 Args:
Joe Gregoriofd08e432012-08-09 14:17:41 -0400116 scope: string or list of strings, scope(s) of the credentials being
117 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400118 """
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500119 if type(scope) is list:
120 scope = ' '.join(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400121 self.scope = scope
JacobMoshenko8e905102011-06-20 09:53:10 -0400122
123 super(AppAssertionCredentials, self).__init__(
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400124 'ignored' # assertion_type is ignore in this subclass.
125 )
JacobMoshenko8e905102011-06-20 09:53:10 -0400126
Joe Gregorio562b7312011-09-15 09:06:38 -0400127 @classmethod
128 def from_json(cls, json):
129 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500130 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400131
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500132 def _refresh(self, http_request):
133 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400134
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500135 Since the underlying App Engine app_identity implementation does its own
136 caching we can skip all the storage hoops and just to a refresh using the
137 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400138
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500139 Args:
140 http_request: callable, a callable that matches the method signature of
141 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400142
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500143 Raises:
144 AccessTokenRefreshError: When the refresh fails.
145 """
146 try:
147 (token, _) = app_identity.get_access_token(self.scope)
148 except app_identity.Error, e:
149 raise AccessTokenRefreshError(str(e))
150 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400151
152
Joe Gregorio695fdc12011-01-16 16:46:55 -0500153class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500154 """App Engine datastore Property for Flow.
155
156 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500157 oauth2client.Flow"""
158
159 # Tell what the user type is.
160 data_type = Flow
161
162 # For writing to datastore.
163 def get_value_for_datastore(self, model_instance):
164 flow = super(FlowProperty,
165 self).get_value_for_datastore(model_instance)
166 return db.Blob(pickle.dumps(flow))
167
168 # For reading from datastore.
169 def make_value_from_datastore(self, value):
170 if value is None:
171 return None
172 return pickle.loads(value)
173
174 def validate(self, value):
175 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400176 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500177 'to a FlowThreeLegged instance (%s)' %
178 (self.name, value))
179 return super(FlowProperty, self).validate(value)
180
181 def empty(self, value):
182 return not value
183
184
185class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500186 """App Engine datastore Property for Credentials.
187
188 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500189 oath2client.Credentials
190 """
191
192 # Tell what the user type is.
193 data_type = Credentials
194
195 # For writing to datastore.
196 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400197 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500198 cred = super(CredentialsProperty,
199 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400200 if cred is None:
201 cred = ''
202 else:
203 cred = cred.to_json()
204 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500205
206 # For reading from datastore.
207 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400208 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500209 if value is None:
210 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400211 if len(value) == 0:
212 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400213 try:
214 credentials = Credentials.new_from_json(value)
215 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400216 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400217 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500218
219 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400220 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400221 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500222 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400223 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400224 'to a Credentials instance (%s)' %
225 (self.name, value))
226 #if value is not None and not isinstance(value, Credentials):
227 # return None
228 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500229
230
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500231class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500232 """Store and retrieve a single credential to and from
233 the App Engine datastore.
234
235 This Storage helper presumes the Credentials
236 have been stored as a CredenialsProperty
237 on a datastore model class, and that entities
238 are stored by key_name.
239 """
240
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400241 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400242 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500243 """Constructor for Storage.
244
245 Args:
246 model: db.Model, model class
247 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400248 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400249 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500250 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500251 self._model = model
252 self._key_name = key_name
253 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400254 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500255
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400256 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500257 """Retrieve Credential from datastore.
258
259 Returns:
260 oauth2client.Credentials
261 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400262 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400263 json = self._cache.get(self._key_name)
264 if json:
265 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500266
267 credential = None
268 entity = self._model.get_by_key_name(self._key_name)
269 if entity is not None:
270 credential = getattr(entity, self._property_name)
271 if credential and hasattr(credential, 'set_store'):
272 credential.set_store(self)
273 if self._cache:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400274 self._cache.set(self._key_name, credential.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400275
Joe Gregorio695fdc12011-01-16 16:46:55 -0500276 return credential
277
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400278 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500279 """Write a Credentials to the datastore.
280
281 Args:
282 credentials: Credentials, the credentials to store.
283 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500284 entity = self._model.get_or_insert(self._key_name)
285 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500286 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400287 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400288 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400289
Joe Gregorioec75dc12012-02-06 13:40:42 -0500290 def locked_delete(self):
291 """Delete Credential from datastore."""
292
293 if self._cache:
294 self._cache.delete(self._key_name)
295
296 entity = self._model.get_by_key_name(self._key_name)
297 if entity is not None:
298 entity.delete()
299
Joe Gregorio432f17e2011-05-22 23:18:00 -0400300
301class CredentialsModel(db.Model):
302 """Storage for OAuth 2.0 Credentials
303
304 Storage of the model is keyed by the user.user_id().
305 """
306 credentials = CredentialsProperty()
307
308
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400309def _build_state_value(request_handler, user):
310 """Composes the value for the 'state' parameter.
311
312 Packs the current request URI and an XSRF token into an opaque string that
313 can be passed to the authentication server via the 'state' parameter.
314
315 Args:
316 request_handler: webapp.RequestHandler, The request.
317 user: google.appengine.api.users.User, The current user.
318
319 Returns:
320 The state value as a string.
321 """
322 uri = request_handler.request.url
323 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
324 action_id=str(uri))
325 return uri + ':' + token
326
327
328def _parse_state_value(state, user):
329 """Parse the value of the 'state' parameter.
330
331 Parses the value and validates the XSRF token in the state parameter.
332
333 Args:
334 state: string, The value of the state parameter.
335 user: google.appengine.api.users.User, The current user.
336
337 Raises:
338 InvalidXsrfTokenError: if the XSRF token is invalid.
339
340 Returns:
341 The redirect URI.
342 """
343 uri, token = state.rsplit(':', 1)
344 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
345 action_id=uri):
346 raise InvalidXsrfTokenError()
347
348 return uri
349
350
Joe Gregorio432f17e2011-05-22 23:18:00 -0400351class OAuth2Decorator(object):
352 """Utility for making OAuth 2.0 easier.
353
354 Instantiate and then use with oauth_required or oauth_aware
355 as decorators on webapp.RequestHandler methods.
356
357 Example:
358
359 decorator = OAuth2Decorator(
360 client_id='837...ent.com',
361 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500362 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400363
364
365 class MainHandler(webapp.RequestHandler):
366
367 @decorator.oauth_required
368 def get(self):
369 http = decorator.http()
370 # http is authorized with the user's Credentials and can be used
371 # in API calls
372
373 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400374
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400375 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400376 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400377 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400378 token_uri='https://accounts.google.com/o/oauth2/token',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100379 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400380 message=None,
381 callback_path='/oauth2callback',
382 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400383
384 """Constructor for OAuth2Decorator
385
386 Args:
387 client_id: string, client identifier.
388 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400389 scope: string or list of strings, scope(s) of the credentials being
390 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400391 auth_uri: string, URI for authorization endpoint. For convenience
392 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
393 token_uri: string, URI for token endpoint. For convenience
394 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100395 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400396 message: Message to display if there are problems with the OAuth 2.0
397 configuration. The message may contain HTML and will be presented on the
398 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400399 callback_path: string, The absolute path to use as the callback URI. Note
400 that this must match up with the URI given when registering the
401 application in the APIs Console.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500402 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
403 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400404 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400405 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400406 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400407 self._client_id = client_id
408 self._client_secret = client_secret
409 self._scope = scope
410 self._auth_uri = auth_uri
411 self._token_uri = token_uri
412 self._user_agent = user_agent
413 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400414 self._message = message
415 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400416 self._callback_path = callback_path
Joe Gregoriof08a4982011-10-07 13:11:16 -0400417
418 def _display_error_message(self, request_handler):
419 request_handler.response.out.write('<html><body>')
420 request_handler.response.out.write(self._message)
421 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400422
423 def oauth_required(self, method):
424 """Decorator that starts the OAuth 2.0 dance.
425
426 Starts the OAuth dance for the logged in user if they haven't already
427 granted access for this application.
428
429 Args:
430 method: callable, to be decorated method of a webapp.RequestHandler
431 instance.
432 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400433
Joe Gregorio17774972012-03-01 11:11:59 -0500434 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400435 if self._in_error:
436 self._display_error_message(request_handler)
437 return
438
Joe Gregoriof427c532011-06-13 09:35:26 -0400439 user = users.get_current_user()
440 # Don't use @login_decorator as this could be used in a POST request.
441 if not user:
442 request_handler.redirect(users.create_login_url(
443 request_handler.request.uri))
444 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400445
446 self._create_flow(request_handler)
447
Joe Gregorio432f17e2011-05-22 23:18:00 -0400448 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400449 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400450 self.credentials = StorageByKeyName(
451 CredentialsModel, user.user_id(), 'credentials').get()
452
453 if not self.has_credentials():
454 return request_handler.redirect(self.authorize_url())
455 try:
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400456 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400457 except AccessTokenRefreshError:
458 return request_handler.redirect(self.authorize_url())
459
460 return check_oauth
461
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400462 def _create_flow(self, request_handler):
463 """Create the Flow object.
464
465 The Flow is calculated lazily since we don't know where this app is
466 running until it receives a request, at which point redirect_uri can be
467 calculated and then the Flow object can be constructed.
468
469 Args:
470 request_handler: webapp.RequestHandler, the request handler.
471 """
472 if self.flow is None:
473 redirect_uri = request_handler.request.relative_url(
474 self._callback_path) # Usually /oauth2callback
475 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
476 self._scope, redirect_uri=redirect_uri,
477 user_agent=self._user_agent,
478 auth_uri=self._auth_uri,
479 token_uri=self._token_uri, **self._kwargs)
480
481
Joe Gregorio432f17e2011-05-22 23:18:00 -0400482 def oauth_aware(self, method):
483 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
484
485 Does all the setup for the OAuth dance, but doesn't initiate it.
486 This decorator is useful if you want to create a page that knows
487 whether or not the user has granted access to this application.
488 From within a method decorated with @oauth_aware the has_credentials()
489 and authorize_url() methods can be called.
490
491 Args:
492 method: callable, to be decorated method of a webapp.RequestHandler
493 instance.
494 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400495
Joe Gregorio17774972012-03-01 11:11:59 -0500496 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400497 if self._in_error:
498 self._display_error_message(request_handler)
499 return
500
Joe Gregoriof427c532011-06-13 09:35:26 -0400501 user = users.get_current_user()
502 # Don't use @login_decorator as this could be used in a POST request.
503 if not user:
504 request_handler.redirect(users.create_login_url(
505 request_handler.request.uri))
506 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400507
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400508 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400509
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400510 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400511 self.credentials = StorageByKeyName(
512 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400513 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400514 return setup_oauth
515
516 def has_credentials(self):
517 """True if for the logged in user there are valid access Credentials.
518
519 Must only be called from with a webapp.RequestHandler subclassed method
520 that had been decorated with either @oauth_required or @oauth_aware.
521 """
522 return self.credentials is not None and not self.credentials.invalid
523
524 def authorize_url(self):
525 """Returns the URL to start the OAuth dance.
526
527 Must only be called from with a webapp.RequestHandler subclassed method
528 that had been decorated with either @oauth_required or @oauth_aware.
529 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400530 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500531 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400532
533 def http(self):
534 """Returns an authorized http instance.
535
536 Must only be called from within an @oauth_required decorated method, or
537 from within an @oauth_aware decorated method where has_credentials()
538 returns True.
539 """
540 return self.credentials.authorize(httplib2.Http())
541
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400542 @property
543 def callback_path(self):
544 """The absolute path where the callback will occur.
545
546 Note this is the absolute path, not the absolute URI, that will be
547 calculated by the decorator at runtime. See callback_handler() for how this
548 should be used.
549
550 Returns:
551 The callback path as a string.
552 """
553 return self._callback_path
554
555
556 def callback_handler(self):
557 """RequestHandler for the OAuth 2.0 redirect callback.
558
559 Usage:
560 app = webapp.WSGIApplication([
561 ('/index', MyIndexHandler),
562 ...,
563 (decorator.callback_path, decorator.callback_handler())
564 ])
565
566 Returns:
567 A webapp.RequestHandler that handles the redirect back from the
568 server during the OAuth 2.0 dance.
569 """
570 decorator = self
571
572 class OAuth2Handler(webapp.RequestHandler):
573 """Handler for the redirect_uri of the OAuth 2.0 dance."""
574
575 @login_required
576 def get(self):
577 error = self.request.get('error')
578 if error:
579 errormsg = self.request.get('error_description', error)
580 self.response.out.write(
581 'The authorization request failed: %s' % errormsg)
582 else:
583 user = users.get_current_user()
584 decorator._create_flow(self)
585 credentials = decorator.flow.step2_exchange(self.request.params)
586 StorageByKeyName(
587 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400588 redirect_uri = _parse_state_value(str(self.request.get('state')),
589 user)
590 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400591
592 return OAuth2Handler
593
594 def callback_application(self):
595 """WSGI application for handling the OAuth 2.0 redirect callback.
596
597 If you need finer grained control use `callback_handler` which returns just
598 the webapp.RequestHandler.
599
600 Returns:
601 A webapp.WSGIApplication that handles the redirect back from the
602 server during the OAuth 2.0 dance.
603 """
604 return webapp.WSGIApplication([
605 (self.callback_path, self.callback_handler())
606 ])
607
Joe Gregorio432f17e2011-05-22 23:18:00 -0400608
Joe Gregoriof08a4982011-10-07 13:11:16 -0400609class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
610 """An OAuth2Decorator that builds from a clientsecrets file.
611
612 Uses a clientsecrets file as the source for all the information when
613 constructing an OAuth2Decorator.
614
615 Example:
616
617 decorator = OAuth2DecoratorFromClientSecrets(
618 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500619 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400620
621
622 class MainHandler(webapp.RequestHandler):
623
624 @decorator.oauth_required
625 def get(self):
626 http = decorator.http()
627 # http is authorized with the user's Credentials and can be used
628 # in API calls
629 """
630
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400631 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400632 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400633 """Constructor
634
635 Args:
636 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400637 scope: string or list of strings, scope(s) of the credentials being
638 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400639 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400640 clientsecrets file is missing or invalid. The message may contain HTML
641 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400642 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400643 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400644 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400645 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400646 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
647 if client_type not in [
648 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
649 raise InvalidClientSecretsError(
650 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
651 super(OAuth2DecoratorFromClientSecrets, self).__init__(
652 client_info['client_id'],
653 client_info['client_secret'],
654 scope,
655 auth_uri=client_info['auth_uri'],
656 token_uri=client_info['token_uri'],
657 message=message)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400658 if message is not None:
659 self._message = message
660 else:
661 self._message = "Please configure your application for OAuth 2.0"
662
663
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400664@util.positional(2)
665def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400666 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400667 """Creates an OAuth2Decorator populated from a clientsecrets file.
668
669 Args:
670 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400671 scope: string or list of strings, scope(s) of the credentials being
672 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400673 message: string, A friendly string to display to the user if the
674 clientsecrets file is missing or invalid. The message may contain HTML and
675 will be presented on the web interface for any method that uses the
676 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400677 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400678 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400679
680 Returns: An OAuth2Decorator
681
682 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400683 return OAuth2DecoratorFromClientSecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400684 message=message, cache=cache)