blob: c747528bcefaf70b540efc76352f87c10cf3d2f9 [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 Gregorio5cf5d122012-11-16 16:36:12 -0500129 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriofd08e432012-08-09 14:17:41 -0400130 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -0400131 """
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500132 self.scope = util.scopes_to_string(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -0400133
134 super(AppAssertionCredentials, self).__init__(
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400135 'ignored' # assertion_type is ignore in this subclass.
136 )
JacobMoshenko8e905102011-06-20 09:53:10 -0400137
Joe Gregorio562b7312011-09-15 09:06:38 -0400138 @classmethod
139 def from_json(cls, json):
140 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500141 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -0400142
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500143 def _refresh(self, http_request):
144 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -0400145
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500146 Since the underlying App Engine app_identity implementation does its own
147 caching we can skip all the storage hoops and just to a refresh using the
148 API.
JacobMoshenko8e905102011-06-20 09:53:10 -0400149
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500150 Args:
151 http_request: callable, a callable that matches the method signature of
152 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -0400153
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500154 Raises:
155 AccessTokenRefreshError: When the refresh fails.
156 """
157 try:
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500158 scopes = self.scope.split()
159 (token, _) = app_identity.get_access_token(scopes)
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500160 except app_identity.Error, e:
161 raise AccessTokenRefreshError(str(e))
162 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400163
164
Joe Gregorio695fdc12011-01-16 16:46:55 -0500165class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500166 """App Engine datastore Property for Flow.
167
168 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500169 oauth2client.Flow"""
170
171 # Tell what the user type is.
172 data_type = Flow
173
174 # For writing to datastore.
175 def get_value_for_datastore(self, model_instance):
176 flow = super(FlowProperty,
177 self).get_value_for_datastore(model_instance)
178 return db.Blob(pickle.dumps(flow))
179
180 # For reading from datastore.
181 def make_value_from_datastore(self, value):
182 if value is None:
183 return None
184 return pickle.loads(value)
185
186 def validate(self, value):
187 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400188 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500189 'to a FlowThreeLegged instance (%s)' %
190 (self.name, value))
191 return super(FlowProperty, self).validate(value)
192
193 def empty(self, value):
194 return not value
195
196
197class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500198 """App Engine datastore Property for Credentials.
199
200 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500201 oath2client.Credentials
202 """
203
204 # Tell what the user type is.
205 data_type = Credentials
206
207 # For writing to datastore.
208 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400209 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500210 cred = super(CredentialsProperty,
211 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400212 if cred is None:
213 cred = ''
214 else:
215 cred = cred.to_json()
216 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500217
218 # For reading from datastore.
219 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400220 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500221 if value is None:
222 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400223 if len(value) == 0:
224 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400225 try:
226 credentials = Credentials.new_from_json(value)
227 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400228 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400229 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500230
231 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400232 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400233 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500234 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400235 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400236 'to a Credentials instance (%s)' %
237 (self.name, value))
238 #if value is not None and not isinstance(value, Credentials):
239 # return None
240 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500241
242
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500243class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500244 """Store and retrieve a single credential to and from
245 the App Engine datastore.
246
247 This Storage helper presumes the Credentials
248 have been stored as a CredenialsProperty
249 on a datastore model class, and that entities
250 are stored by key_name.
251 """
252
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400253 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400254 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500255 """Constructor for Storage.
256
257 Args:
258 model: db.Model, model class
259 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400260 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400261 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500262 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500263 self._model = model
264 self._key_name = key_name
265 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400266 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500267
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400268 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500269 """Retrieve Credential from datastore.
270
271 Returns:
272 oauth2client.Credentials
273 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400274 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400275 json = self._cache.get(self._key_name)
276 if json:
277 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500278
279 credential = None
280 entity = self._model.get_by_key_name(self._key_name)
281 if entity is not None:
282 credential = getattr(entity, self._property_name)
283 if credential and hasattr(credential, 'set_store'):
284 credential.set_store(self)
285 if self._cache:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400286 self._cache.set(self._key_name, credential.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400287
Joe Gregorio695fdc12011-01-16 16:46:55 -0500288 return credential
289
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400290 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500291 """Write a Credentials to the datastore.
292
293 Args:
294 credentials: Credentials, the credentials to store.
295 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500296 entity = self._model.get_or_insert(self._key_name)
297 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500298 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400299 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400300 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400301
Joe Gregorioec75dc12012-02-06 13:40:42 -0500302 def locked_delete(self):
303 """Delete Credential from datastore."""
304
305 if self._cache:
306 self._cache.delete(self._key_name)
307
308 entity = self._model.get_by_key_name(self._key_name)
309 if entity is not None:
310 entity.delete()
311
Joe Gregorio432f17e2011-05-22 23:18:00 -0400312
313class CredentialsModel(db.Model):
314 """Storage for OAuth 2.0 Credentials
315
316 Storage of the model is keyed by the user.user_id().
317 """
318 credentials = CredentialsProperty()
319
320
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400321def _build_state_value(request_handler, user):
322 """Composes the value for the 'state' parameter.
323
324 Packs the current request URI and an XSRF token into an opaque string that
325 can be passed to the authentication server via the 'state' parameter.
326
327 Args:
328 request_handler: webapp.RequestHandler, The request.
329 user: google.appengine.api.users.User, The current user.
330
331 Returns:
332 The state value as a string.
333 """
334 uri = request_handler.request.url
335 token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
336 action_id=str(uri))
337 return uri + ':' + token
338
339
340def _parse_state_value(state, user):
341 """Parse the value of the 'state' parameter.
342
343 Parses the value and validates the XSRF token in the state parameter.
344
345 Args:
346 state: string, The value of the state parameter.
347 user: google.appengine.api.users.User, The current user.
348
349 Raises:
350 InvalidXsrfTokenError: if the XSRF token is invalid.
351
352 Returns:
353 The redirect URI.
354 """
355 uri, token = state.rsplit(':', 1)
356 if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
357 action_id=uri):
358 raise InvalidXsrfTokenError()
359
360 return uri
361
362
Joe Gregorio432f17e2011-05-22 23:18:00 -0400363class OAuth2Decorator(object):
364 """Utility for making OAuth 2.0 easier.
365
366 Instantiate and then use with oauth_required or oauth_aware
367 as decorators on webapp.RequestHandler methods.
368
369 Example:
370
371 decorator = OAuth2Decorator(
372 client_id='837...ent.com',
373 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500374 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400375
376
377 class MainHandler(webapp.RequestHandler):
378
379 @decorator.oauth_required
380 def get(self):
381 http = decorator.http()
382 # http is authorized with the user's Credentials and can be used
383 # in API calls
384
385 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400386
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400387 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400388 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400389 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400390 token_uri='https://accounts.google.com/o/oauth2/token',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100391 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400392 message=None,
393 callback_path='/oauth2callback',
394 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400395
396 """Constructor for OAuth2Decorator
397
398 Args:
399 client_id: string, client identifier.
400 client_secret: string client secret.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500401 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400402 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400403 auth_uri: string, URI for authorization endpoint. For convenience
404 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
405 token_uri: string, URI for token endpoint. For convenience
406 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100407 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400408 message: Message to display if there are problems with the OAuth 2.0
409 configuration. The message may contain HTML and will be presented on the
410 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400411 callback_path: string, The absolute path to use as the callback URI. Note
412 that this must match up with the URI given when registering the
413 application in the APIs Console.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500414 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
415 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400416 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400417 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400418 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400419 self._client_id = client_id
420 self._client_secret = client_secret
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500421 self._scope = util.scopes_to_string(scope)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400422 self._auth_uri = auth_uri
423 self._token_uri = token_uri
424 self._user_agent = user_agent
425 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400426 self._message = message
427 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400428 self._callback_path = callback_path
Joe Gregoriof08a4982011-10-07 13:11:16 -0400429
430 def _display_error_message(self, request_handler):
431 request_handler.response.out.write('<html><body>')
Joe Gregorio77254c12012-08-27 14:13:22 -0400432 request_handler.response.out.write(_safe_html(self._message))
Joe Gregoriof08a4982011-10-07 13:11:16 -0400433 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400434
435 def oauth_required(self, method):
436 """Decorator that starts the OAuth 2.0 dance.
437
438 Starts the OAuth dance for the logged in user if they haven't already
439 granted access for this application.
440
441 Args:
442 method: callable, to be decorated method of a webapp.RequestHandler
443 instance.
444 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400445
Joe Gregorio17774972012-03-01 11:11:59 -0500446 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400447 if self._in_error:
448 self._display_error_message(request_handler)
449 return
450
Joe Gregoriof427c532011-06-13 09:35:26 -0400451 user = users.get_current_user()
452 # Don't use @login_decorator as this could be used in a POST request.
453 if not user:
454 request_handler.redirect(users.create_login_url(
455 request_handler.request.uri))
456 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400457
458 self._create_flow(request_handler)
459
Joe Gregorio432f17e2011-05-22 23:18:00 -0400460 # Store the request URI in 'state' so we can use it later
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400461 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400462 self.credentials = StorageByKeyName(
463 CredentialsModel, user.user_id(), 'credentials').get()
464
465 if not self.has_credentials():
466 return request_handler.redirect(self.authorize_url())
467 try:
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400468 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400469 except AccessTokenRefreshError:
470 return request_handler.redirect(self.authorize_url())
471
472 return check_oauth
473
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400474 def _create_flow(self, request_handler):
475 """Create the Flow object.
476
477 The Flow is calculated lazily since we don't know where this app is
478 running until it receives a request, at which point redirect_uri can be
479 calculated and then the Flow object can be constructed.
480
481 Args:
482 request_handler: webapp.RequestHandler, the request handler.
483 """
484 if self.flow is None:
485 redirect_uri = request_handler.request.relative_url(
486 self._callback_path) # Usually /oauth2callback
487 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
488 self._scope, redirect_uri=redirect_uri,
489 user_agent=self._user_agent,
490 auth_uri=self._auth_uri,
491 token_uri=self._token_uri, **self._kwargs)
492
493
Joe Gregorio432f17e2011-05-22 23:18:00 -0400494 def oauth_aware(self, method):
495 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
496
497 Does all the setup for the OAuth dance, but doesn't initiate it.
498 This decorator is useful if you want to create a page that knows
499 whether or not the user has granted access to this application.
500 From within a method decorated with @oauth_aware the has_credentials()
501 and authorize_url() methods can be called.
502
503 Args:
504 method: callable, to be decorated method of a webapp.RequestHandler
505 instance.
506 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400507
Joe Gregorio17774972012-03-01 11:11:59 -0500508 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400509 if self._in_error:
510 self._display_error_message(request_handler)
511 return
512
Joe Gregoriof427c532011-06-13 09:35:26 -0400513 user = users.get_current_user()
514 # Don't use @login_decorator as this could be used in a POST request.
515 if not user:
516 request_handler.redirect(users.create_login_url(
517 request_handler.request.uri))
518 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400519
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400520 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400521
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400522 self.flow.params['state'] = _build_state_value(request_handler, user)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400523 self.credentials = StorageByKeyName(
524 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400525 return method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400526 return setup_oauth
527
528 def has_credentials(self):
529 """True if for the logged in user there are valid access Credentials.
530
531 Must only be called from with a webapp.RequestHandler subclassed method
532 that had been decorated with either @oauth_required or @oauth_aware.
533 """
534 return self.credentials is not None and not self.credentials.invalid
535
536 def authorize_url(self):
537 """Returns the URL to start the OAuth dance.
538
539 Must only be called from with a webapp.RequestHandler subclassed method
540 that had been decorated with either @oauth_required or @oauth_aware.
541 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400542 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500543 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400544
545 def http(self):
546 """Returns an authorized http instance.
547
548 Must only be called from within an @oauth_required decorated method, or
549 from within an @oauth_aware decorated method where has_credentials()
550 returns True.
551 """
552 return self.credentials.authorize(httplib2.Http())
553
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400554 @property
555 def callback_path(self):
556 """The absolute path where the callback will occur.
557
558 Note this is the absolute path, not the absolute URI, that will be
559 calculated by the decorator at runtime. See callback_handler() for how this
560 should be used.
561
562 Returns:
563 The callback path as a string.
564 """
565 return self._callback_path
566
567
568 def callback_handler(self):
569 """RequestHandler for the OAuth 2.0 redirect callback.
570
571 Usage:
572 app = webapp.WSGIApplication([
573 ('/index', MyIndexHandler),
574 ...,
575 (decorator.callback_path, decorator.callback_handler())
576 ])
577
578 Returns:
579 A webapp.RequestHandler that handles the redirect back from the
580 server during the OAuth 2.0 dance.
581 """
582 decorator = self
583
584 class OAuth2Handler(webapp.RequestHandler):
585 """Handler for the redirect_uri of the OAuth 2.0 dance."""
586
587 @login_required
588 def get(self):
589 error = self.request.get('error')
590 if error:
591 errormsg = self.request.get('error_description', error)
592 self.response.out.write(
Joe Gregorio77254c12012-08-27 14:13:22 -0400593 'The authorization request failed: %s' % _safe_html(errormsg))
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400594 else:
595 user = users.get_current_user()
596 decorator._create_flow(self)
597 credentials = decorator.flow.step2_exchange(self.request.params)
598 StorageByKeyName(
599 CredentialsModel, user.user_id(), 'credentials').put(credentials)
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400600 redirect_uri = _parse_state_value(str(self.request.get('state')),
601 user)
602 self.redirect(redirect_uri)
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400603
604 return OAuth2Handler
605
606 def callback_application(self):
607 """WSGI application for handling the OAuth 2.0 redirect callback.
608
609 If you need finer grained control use `callback_handler` which returns just
610 the webapp.RequestHandler.
611
612 Returns:
613 A webapp.WSGIApplication that handles the redirect back from the
614 server during the OAuth 2.0 dance.
615 """
616 return webapp.WSGIApplication([
617 (self.callback_path, self.callback_handler())
618 ])
619
Joe Gregorio432f17e2011-05-22 23:18:00 -0400620
Joe Gregoriof08a4982011-10-07 13:11:16 -0400621class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
622 """An OAuth2Decorator that builds from a clientsecrets file.
623
624 Uses a clientsecrets file as the source for all the information when
625 constructing an OAuth2Decorator.
626
627 Example:
628
629 decorator = OAuth2DecoratorFromClientSecrets(
630 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500631 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400632
633
634 class MainHandler(webapp.RequestHandler):
635
636 @decorator.oauth_required
637 def get(self):
638 http = decorator.http()
639 # http is authorized with the user's Credentials and can be used
640 # in API calls
641 """
642
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400643 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400644 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400645 """Constructor
646
647 Args:
648 filename: string, File name of client secrets.
Joe Gregorio5cf5d122012-11-16 16:36:12 -0500649 scope: string or iterable of strings, scope(s) of the credentials being
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400650 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400651 message: string, A friendly string to display to the user if the
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400652 clientsecrets file is missing or invalid. The message may contain HTML
653 and will be presented on the web interface for any method that uses the
Joe Gregoriof08a4982011-10-07 13:11:16 -0400654 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400655 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400656 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400657 """
Joe Gregorio6ceea2d2012-08-24 11:57:58 -0400658 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
659 if client_type not in [
660 clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
661 raise InvalidClientSecretsError(
662 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
663 super(OAuth2DecoratorFromClientSecrets, self).__init__(
664 client_info['client_id'],
665 client_info['client_secret'],
666 scope,
667 auth_uri=client_info['auth_uri'],
668 token_uri=client_info['token_uri'],
669 message=message)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400670 if message is not None:
671 self._message = message
672 else:
673 self._message = "Please configure your application for OAuth 2.0"
674
675
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400676@util.positional(2)
677def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400678 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400679 """Creates an OAuth2Decorator populated from a clientsecrets file.
680
681 Args:
682 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400683 scope: string or list of strings, scope(s) of the credentials being
684 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400685 message: string, A friendly string to display to the user if the
686 clientsecrets file is missing or invalid. The message may contain HTML and
687 will be presented on the web interface for any method that uses the
688 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400689 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400690 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400691
692 Returns: An OAuth2Decorator
693
694 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400695 return OAuth2DecoratorFromClientSecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400696 message=message, cache=cache)