blob: aebbd32be00bb9597aa8bcd59c941db01811e627 [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 Gregorio695fdc12011-01-16 16:46:55 -050025import pickle
JacobMoshenko8e905102011-06-20 09:53:10 -040026import time
JacobMoshenko8e905102011-06-20 09:53:10 -040027
Joe Gregoriof08a4982011-10-07 13:11:16 -040028import clientsecrets
29
Joe Gregoriod84d6b82012-02-28 14:53:00 -050030from google.appengine.api import app_identity
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 Gregorio68a8cfe2012-08-03 16:17:40 -040036from oauth2client import util
37from oauth2client.anyjson import simplejson
38from oauth2client.client import AccessTokenRefreshError
39from oauth2client.client import AssertionCredentials
40from oauth2client.client import Credentials
41from oauth2client.client import Flow
42from oauth2client.client import OAuth2WebServerFlow
43from oauth2client.client import Storage
Joe Gregorioa19f3a72012-07-11 15:35:35 -040044
45logger = logging.getLogger(__name__)
46
Joe Gregorio432f17e2011-05-22 23:18:00 -040047OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050048
JacobMoshenko8e905102011-06-20 09:53:10 -040049
Joe Gregoriof08a4982011-10-07 13:11:16 -040050class InvalidClientSecretsError(Exception):
51 """The client_secrets.json file is malformed or missing required fields."""
52 pass
53
54
JacobMoshenko8e905102011-06-20 09:53:10 -040055class AppAssertionCredentials(AssertionCredentials):
56 """Credentials object for App Engine Assertion Grants
57
58 This object will allow an App Engine application to identify itself to Google
59 and other OAuth 2.0 servers that can verify assertions. It can be used for
60 the purpose of accessing data stored under an account assigned to the App
Joe Gregoriod84d6b82012-02-28 14:53:00 -050061 Engine application itself.
JacobMoshenko8e905102011-06-20 09:53:10 -040062
63 This credential does not require a flow to instantiate because it represents
64 a two legged flow, and therefore has all of the required information to
65 generate and refresh its own access tokens.
JacobMoshenko8e905102011-06-20 09:53:10 -040066 """
67
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040068 @util.positional(2)
Joe Gregoriod84d6b82012-02-28 14:53:00 -050069 def __init__(self, scope, **kwargs):
JacobMoshenko8e905102011-06-20 09:53:10 -040070 """Constructor for AppAssertionCredentials
71
72 Args:
Joe Gregoriofd08e432012-08-09 14:17:41 -040073 scope: string or list of strings, scope(s) of the credentials being
74 requested.
JacobMoshenko8e905102011-06-20 09:53:10 -040075 """
Joe Gregoriod84d6b82012-02-28 14:53:00 -050076 if type(scope) is list:
77 scope = ' '.join(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -040078 self.scope = scope
JacobMoshenko8e905102011-06-20 09:53:10 -040079
80 super(AppAssertionCredentials, self).__init__(
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040081 'ignored' # assertion_type is ignore in this subclass.
82 )
JacobMoshenko8e905102011-06-20 09:53:10 -040083
Joe Gregorio562b7312011-09-15 09:06:38 -040084 @classmethod
85 def from_json(cls, json):
86 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -050087 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -040088
Joe Gregoriod84d6b82012-02-28 14:53:00 -050089 def _refresh(self, http_request):
90 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -040091
Joe Gregoriod84d6b82012-02-28 14:53:00 -050092 Since the underlying App Engine app_identity implementation does its own
93 caching we can skip all the storage hoops and just to a refresh using the
94 API.
JacobMoshenko8e905102011-06-20 09:53:10 -040095
Joe Gregoriod84d6b82012-02-28 14:53:00 -050096 Args:
97 http_request: callable, a callable that matches the method signature of
98 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -040099
Joe Gregoriod84d6b82012-02-28 14:53:00 -0500100 Raises:
101 AccessTokenRefreshError: When the refresh fails.
102 """
103 try:
104 (token, _) = app_identity.get_access_token(self.scope)
105 except app_identity.Error, e:
106 raise AccessTokenRefreshError(str(e))
107 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400108
109
Joe Gregorio695fdc12011-01-16 16:46:55 -0500110class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500111 """App Engine datastore Property for Flow.
112
113 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500114 oauth2client.Flow"""
115
116 # Tell what the user type is.
117 data_type = Flow
118
119 # For writing to datastore.
120 def get_value_for_datastore(self, model_instance):
121 flow = super(FlowProperty,
122 self).get_value_for_datastore(model_instance)
123 return db.Blob(pickle.dumps(flow))
124
125 # For reading from datastore.
126 def make_value_from_datastore(self, value):
127 if value is None:
128 return None
129 return pickle.loads(value)
130
131 def validate(self, value):
132 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400133 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500134 'to a FlowThreeLegged instance (%s)' %
135 (self.name, value))
136 return super(FlowProperty, self).validate(value)
137
138 def empty(self, value):
139 return not value
140
141
142class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500143 """App Engine datastore Property for Credentials.
144
145 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500146 oath2client.Credentials
147 """
148
149 # Tell what the user type is.
150 data_type = Credentials
151
152 # For writing to datastore.
153 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400154 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500155 cred = super(CredentialsProperty,
156 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400157 if cred is None:
158 cred = ''
159 else:
160 cred = cred.to_json()
161 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500162
163 # For reading from datastore.
164 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400165 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500166 if value is None:
167 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400168 if len(value) == 0:
169 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400170 try:
171 credentials = Credentials.new_from_json(value)
172 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400173 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400174 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500175
176 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400177 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400178 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500179 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400180 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400181 'to a Credentials instance (%s)' %
182 (self.name, value))
183 #if value is not None and not isinstance(value, Credentials):
184 # return None
185 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500186
187
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500188class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500189 """Store and retrieve a single credential to and from
190 the App Engine datastore.
191
192 This Storage helper presumes the Credentials
193 have been stored as a CredenialsProperty
194 on a datastore model class, and that entities
195 are stored by key_name.
196 """
197
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400198 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400199 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500200 """Constructor for Storage.
201
202 Args:
203 model: db.Model, model class
204 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400205 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400206 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500207 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500208 self._model = model
209 self._key_name = key_name
210 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400211 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500212
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400213 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500214 """Retrieve Credential from datastore.
215
216 Returns:
217 oauth2client.Credentials
218 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400219 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400220 json = self._cache.get(self._key_name)
221 if json:
222 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500223
224 credential = None
225 entity = self._model.get_by_key_name(self._key_name)
226 if entity is not None:
227 credential = getattr(entity, self._property_name)
228 if credential and hasattr(credential, 'set_store'):
229 credential.set_store(self)
230 if self._cache:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400231 self._cache.set(self._key_name, credential.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400232
Joe Gregorio695fdc12011-01-16 16:46:55 -0500233 return credential
234
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400235 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500236 """Write a Credentials to the datastore.
237
238 Args:
239 credentials: Credentials, the credentials to store.
240 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500241 entity = self._model.get_or_insert(self._key_name)
242 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500243 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400244 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400245 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400246
Joe Gregorioec75dc12012-02-06 13:40:42 -0500247 def locked_delete(self):
248 """Delete Credential from datastore."""
249
250 if self._cache:
251 self._cache.delete(self._key_name)
252
253 entity = self._model.get_by_key_name(self._key_name)
254 if entity is not None:
255 entity.delete()
256
Joe Gregorio432f17e2011-05-22 23:18:00 -0400257
258class CredentialsModel(db.Model):
259 """Storage for OAuth 2.0 Credentials
260
261 Storage of the model is keyed by the user.user_id().
262 """
263 credentials = CredentialsProperty()
264
265
266class OAuth2Decorator(object):
267 """Utility for making OAuth 2.0 easier.
268
269 Instantiate and then use with oauth_required or oauth_aware
270 as decorators on webapp.RequestHandler methods.
271
272 Example:
273
274 decorator = OAuth2Decorator(
275 client_id='837...ent.com',
276 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500277 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400278
279
280 class MainHandler(webapp.RequestHandler):
281
282 @decorator.oauth_required
283 def get(self):
284 http = decorator.http()
285 # http is authorized with the user's Credentials and can be used
286 # in API calls
287
288 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400289
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400290 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400291 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400292 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400293 token_uri='https://accounts.google.com/o/oauth2/token',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100294 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400295 message=None,
296 callback_path='/oauth2callback',
297 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400298
299 """Constructor for OAuth2Decorator
300
301 Args:
302 client_id: string, client identifier.
303 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400304 scope: string or list of strings, scope(s) of the credentials being
305 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400306 auth_uri: string, URI for authorization endpoint. For convenience
307 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
308 token_uri: string, URI for token endpoint. For convenience
309 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100310 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400311 message: Message to display if there are problems with the OAuth 2.0
312 configuration. The message may contain HTML and will be presented on the
313 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400314 callback_path: string, The absolute path to use as the callback URI. Note
315 that this must match up with the URI given when registering the
316 application in the APIs Console.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500317 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
318 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400319 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400320 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400321 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400322 self._client_id = client_id
323 self._client_secret = client_secret
324 self._scope = scope
325 self._auth_uri = auth_uri
326 self._token_uri = token_uri
327 self._user_agent = user_agent
328 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400329 self._message = message
330 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400331 self._callback_path = callback_path
Joe Gregoriof08a4982011-10-07 13:11:16 -0400332
333 def _display_error_message(self, request_handler):
334 request_handler.response.out.write('<html><body>')
335 request_handler.response.out.write(self._message)
336 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400337
338 def oauth_required(self, method):
339 """Decorator that starts the OAuth 2.0 dance.
340
341 Starts the OAuth dance for the logged in user if they haven't already
342 granted access for this application.
343
344 Args:
345 method: callable, to be decorated method of a webapp.RequestHandler
346 instance.
347 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400348
Joe Gregorio17774972012-03-01 11:11:59 -0500349 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400350 if self._in_error:
351 self._display_error_message(request_handler)
352 return
353
Joe Gregoriof427c532011-06-13 09:35:26 -0400354 user = users.get_current_user()
355 # Don't use @login_decorator as this could be used in a POST request.
356 if not user:
357 request_handler.redirect(users.create_login_url(
358 request_handler.request.uri))
359 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400360
361 self._create_flow(request_handler)
362
Joe Gregorio432f17e2011-05-22 23:18:00 -0400363 # Store the request URI in 'state' so we can use it later
364 self.flow.params['state'] = request_handler.request.url
Joe Gregorio432f17e2011-05-22 23:18:00 -0400365 self.credentials = StorageByKeyName(
366 CredentialsModel, user.user_id(), 'credentials').get()
367
368 if not self.has_credentials():
369 return request_handler.redirect(self.authorize_url())
370 try:
Joe Gregorio17774972012-03-01 11:11:59 -0500371 method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400372 except AccessTokenRefreshError:
373 return request_handler.redirect(self.authorize_url())
374
375 return check_oauth
376
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400377 def _create_flow(self, request_handler):
378 """Create the Flow object.
379
380 The Flow is calculated lazily since we don't know where this app is
381 running until it receives a request, at which point redirect_uri can be
382 calculated and then the Flow object can be constructed.
383
384 Args:
385 request_handler: webapp.RequestHandler, the request handler.
386 """
387 if self.flow is None:
388 redirect_uri = request_handler.request.relative_url(
389 self._callback_path) # Usually /oauth2callback
390 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
391 self._scope, redirect_uri=redirect_uri,
392 user_agent=self._user_agent,
393 auth_uri=self._auth_uri,
394 token_uri=self._token_uri, **self._kwargs)
395
396
Joe Gregorio432f17e2011-05-22 23:18:00 -0400397 def oauth_aware(self, method):
398 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
399
400 Does all the setup for the OAuth dance, but doesn't initiate it.
401 This decorator is useful if you want to create a page that knows
402 whether or not the user has granted access to this application.
403 From within a method decorated with @oauth_aware the has_credentials()
404 and authorize_url() methods can be called.
405
406 Args:
407 method: callable, to be decorated method of a webapp.RequestHandler
408 instance.
409 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400410
Joe Gregorio17774972012-03-01 11:11:59 -0500411 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400412 if self._in_error:
413 self._display_error_message(request_handler)
414 return
415
Joe Gregoriof427c532011-06-13 09:35:26 -0400416 user = users.get_current_user()
417 # Don't use @login_decorator as this could be used in a POST request.
418 if not user:
419 request_handler.redirect(users.create_login_url(
420 request_handler.request.uri))
421 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400422
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400423 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400424
Joe Gregorio432f17e2011-05-22 23:18:00 -0400425 self.flow.params['state'] = request_handler.request.url
Joe Gregorio432f17e2011-05-22 23:18:00 -0400426 self.credentials = StorageByKeyName(
427 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio17774972012-03-01 11:11:59 -0500428 method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400429 return setup_oauth
430
431 def has_credentials(self):
432 """True if for the logged in user there are valid access Credentials.
433
434 Must only be called from with a webapp.RequestHandler subclassed method
435 that had been decorated with either @oauth_required or @oauth_aware.
436 """
437 return self.credentials is not None and not self.credentials.invalid
438
439 def authorize_url(self):
440 """Returns the URL to start the OAuth dance.
441
442 Must only be called from with a webapp.RequestHandler subclassed method
443 that had been decorated with either @oauth_required or @oauth_aware.
444 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400445 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500446 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400447
448 def http(self):
449 """Returns an authorized http instance.
450
451 Must only be called from within an @oauth_required decorated method, or
452 from within an @oauth_aware decorated method where has_credentials()
453 returns True.
454 """
455 return self.credentials.authorize(httplib2.Http())
456
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400457 @property
458 def callback_path(self):
459 """The absolute path where the callback will occur.
460
461 Note this is the absolute path, not the absolute URI, that will be
462 calculated by the decorator at runtime. See callback_handler() for how this
463 should be used.
464
465 Returns:
466 The callback path as a string.
467 """
468 return self._callback_path
469
470
471 def callback_handler(self):
472 """RequestHandler for the OAuth 2.0 redirect callback.
473
474 Usage:
475 app = webapp.WSGIApplication([
476 ('/index', MyIndexHandler),
477 ...,
478 (decorator.callback_path, decorator.callback_handler())
479 ])
480
481 Returns:
482 A webapp.RequestHandler that handles the redirect back from the
483 server during the OAuth 2.0 dance.
484 """
485 decorator = self
486
487 class OAuth2Handler(webapp.RequestHandler):
488 """Handler for the redirect_uri of the OAuth 2.0 dance."""
489
490 @login_required
491 def get(self):
492 error = self.request.get('error')
493 if error:
494 errormsg = self.request.get('error_description', error)
495 self.response.out.write(
496 'The authorization request failed: %s' % errormsg)
497 else:
498 user = users.get_current_user()
499 decorator._create_flow(self)
500 credentials = decorator.flow.step2_exchange(self.request.params)
501 StorageByKeyName(
502 CredentialsModel, user.user_id(), 'credentials').put(credentials)
503 self.redirect(str(self.request.get('state')))
504
505 return OAuth2Handler
506
507 def callback_application(self):
508 """WSGI application for handling the OAuth 2.0 redirect callback.
509
510 If you need finer grained control use `callback_handler` which returns just
511 the webapp.RequestHandler.
512
513 Returns:
514 A webapp.WSGIApplication that handles the redirect back from the
515 server during the OAuth 2.0 dance.
516 """
517 return webapp.WSGIApplication([
518 (self.callback_path, self.callback_handler())
519 ])
520
Joe Gregorio432f17e2011-05-22 23:18:00 -0400521
Joe Gregoriof08a4982011-10-07 13:11:16 -0400522class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
523 """An OAuth2Decorator that builds from a clientsecrets file.
524
525 Uses a clientsecrets file as the source for all the information when
526 constructing an OAuth2Decorator.
527
528 Example:
529
530 decorator = OAuth2DecoratorFromClientSecrets(
531 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500532 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400533
534
535 class MainHandler(webapp.RequestHandler):
536
537 @decorator.oauth_required
538 def get(self):
539 http = decorator.http()
540 # http is authorized with the user's Credentials and can be used
541 # in API calls
542 """
543
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400544 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400545 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400546 """Constructor
547
548 Args:
549 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400550 scope: string or list of strings, scope(s) of the credentials being
551 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400552 message: string, A friendly string to display to the user if the
553 clientsecrets file is missing or invalid. The message may contain HTML and
554 will be presented on the web interface for any method that uses the
555 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400556 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400557 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400558 """
559 try:
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400560 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400561 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
562 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
563 super(OAuth2DecoratorFromClientSecrets,
564 self).__init__(
565 client_info['client_id'],
566 client_info['client_secret'],
567 scope,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400568 auth_uri=client_info['auth_uri'],
569 token_uri=client_info['token_uri'],
570 message=message)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400571 except clientsecrets.InvalidClientSecretsError:
572 self._in_error = True
573 if message is not None:
574 self._message = message
575 else:
576 self._message = "Please configure your application for OAuth 2.0"
577
578
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400579@util.positional(2)
580def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400581 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400582 """Creates an OAuth2Decorator populated from a clientsecrets file.
583
584 Args:
585 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400586 scope: string or list of strings, scope(s) of the credentials being
587 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400588 message: string, A friendly string to display to the user if the
589 clientsecrets file is missing or invalid. The message may contain HTML and
590 will be presented on the web interface for any method that uses the
591 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400592 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400593 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400594
595 Returns: An OAuth2Decorator
596
597 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400598 return OAuth2DecoratorFromClientSecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400599 message=message, cache=cache)