blob: 6f63831be661b4927a37adfa80ea8e3916fa69d0 [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 Gregoriod84d6b82012-02-28 14:53:00 -050073 scope: string or list of strings, scope(s) of the credentials being requested.
JacobMoshenko8e905102011-06-20 09:53:10 -040074 """
Joe Gregoriod84d6b82012-02-28 14:53:00 -050075 if type(scope) is list:
76 scope = ' '.join(scope)
JacobMoshenko8e905102011-06-20 09:53:10 -040077 self.scope = scope
JacobMoshenko8e905102011-06-20 09:53:10 -040078
79 super(AppAssertionCredentials, self).__init__(
Joe Gregorio68a8cfe2012-08-03 16:17:40 -040080 'ignored' # assertion_type is ignore in this subclass.
81 )
JacobMoshenko8e905102011-06-20 09:53:10 -040082
Joe Gregorio562b7312011-09-15 09:06:38 -040083 @classmethod
84 def from_json(cls, json):
85 data = simplejson.loads(json)
Joe Gregoriod84d6b82012-02-28 14:53:00 -050086 return AppAssertionCredentials(data['scope'])
Joe Gregorio562b7312011-09-15 09:06:38 -040087
Joe Gregoriod84d6b82012-02-28 14:53:00 -050088 def _refresh(self, http_request):
89 """Refreshes the access_token.
JacobMoshenko8e905102011-06-20 09:53:10 -040090
Joe Gregoriod84d6b82012-02-28 14:53:00 -050091 Since the underlying App Engine app_identity implementation does its own
92 caching we can skip all the storage hoops and just to a refresh using the
93 API.
JacobMoshenko8e905102011-06-20 09:53:10 -040094
Joe Gregoriod84d6b82012-02-28 14:53:00 -050095 Args:
96 http_request: callable, a callable that matches the method signature of
97 httplib2.Http.request, used to make the refresh request.
JacobMoshenko8e905102011-06-20 09:53:10 -040098
Joe Gregoriod84d6b82012-02-28 14:53:00 -050099 Raises:
100 AccessTokenRefreshError: When the refresh fails.
101 """
102 try:
103 (token, _) = app_identity.get_access_token(self.scope)
104 except app_identity.Error, e:
105 raise AccessTokenRefreshError(str(e))
106 self.access_token = token
JacobMoshenko8e905102011-06-20 09:53:10 -0400107
108
Joe Gregorio695fdc12011-01-16 16:46:55 -0500109class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500110 """App Engine datastore Property for Flow.
111
112 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500113 oauth2client.Flow"""
114
115 # Tell what the user type is.
116 data_type = Flow
117
118 # For writing to datastore.
119 def get_value_for_datastore(self, model_instance):
120 flow = super(FlowProperty,
121 self).get_value_for_datastore(model_instance)
122 return db.Blob(pickle.dumps(flow))
123
124 # For reading from datastore.
125 def make_value_from_datastore(self, value):
126 if value is None:
127 return None
128 return pickle.loads(value)
129
130 def validate(self, value):
131 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400132 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500133 'to a FlowThreeLegged instance (%s)' %
134 (self.name, value))
135 return super(FlowProperty, self).validate(value)
136
137 def empty(self, value):
138 return not value
139
140
141class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500142 """App Engine datastore Property for Credentials.
143
144 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500145 oath2client.Credentials
146 """
147
148 # Tell what the user type is.
149 data_type = Credentials
150
151 # For writing to datastore.
152 def get_value_for_datastore(self, model_instance):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400153 logger.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500154 cred = super(CredentialsProperty,
155 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400156 if cred is None:
157 cred = ''
158 else:
159 cred = cred.to_json()
160 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500161
162 # For reading from datastore.
163 def make_value_from_datastore(self, value):
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400164 logger.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500165 if value is None:
166 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400167 if len(value) == 0:
168 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400169 try:
170 credentials = Credentials.new_from_json(value)
171 except ValueError:
Joe Gregorioec555842011-10-27 11:10:39 -0400172 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400173 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500174
175 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400176 value = super(CredentialsProperty, self).validate(value)
Joe Gregorioa19f3a72012-07-11 15:35:35 -0400177 logger.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500178 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400179 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400180 'to a Credentials instance (%s)' %
181 (self.name, value))
182 #if value is not None and not isinstance(value, Credentials):
183 # return None
184 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500185
186
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500187class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500188 """Store and retrieve a single credential to and from
189 the App Engine datastore.
190
191 This Storage helper presumes the Credentials
192 have been stored as a CredenialsProperty
193 on a datastore model class, and that entities
194 are stored by key_name.
195 """
196
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400197 @util.positional(4)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400198 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500199 """Constructor for Storage.
200
201 Args:
202 model: db.Model, model class
203 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400204 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400205 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500206 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500207 self._model = model
208 self._key_name = key_name
209 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400210 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500211
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400212 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500213 """Retrieve Credential from datastore.
214
215 Returns:
216 oauth2client.Credentials
217 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400218 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400219 json = self._cache.get(self._key_name)
220 if json:
221 return Credentials.new_from_json(json)
Joe Gregorio9fa077c2011-11-18 08:16:52 -0500222
223 credential = None
224 entity = self._model.get_by_key_name(self._key_name)
225 if entity is not None:
226 credential = getattr(entity, self._property_name)
227 if credential and hasattr(credential, 'set_store'):
228 credential.set_store(self)
229 if self._cache:
Joe Gregorioe84c9442012-03-12 08:45:57 -0400230 self._cache.set(self._key_name, credential.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400231
Joe Gregorio695fdc12011-01-16 16:46:55 -0500232 return credential
233
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400234 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500235 """Write a Credentials to the datastore.
236
237 Args:
238 credentials: Credentials, the credentials to store.
239 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500240 entity = self._model.get_or_insert(self._key_name)
241 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500242 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400243 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400244 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400245
Joe Gregorioec75dc12012-02-06 13:40:42 -0500246 def locked_delete(self):
247 """Delete Credential from datastore."""
248
249 if self._cache:
250 self._cache.delete(self._key_name)
251
252 entity = self._model.get_by_key_name(self._key_name)
253 if entity is not None:
254 entity.delete()
255
Joe Gregorio432f17e2011-05-22 23:18:00 -0400256
257class CredentialsModel(db.Model):
258 """Storage for OAuth 2.0 Credentials
259
260 Storage of the model is keyed by the user.user_id().
261 """
262 credentials = CredentialsProperty()
263
264
265class OAuth2Decorator(object):
266 """Utility for making OAuth 2.0 easier.
267
268 Instantiate and then use with oauth_required or oauth_aware
269 as decorators on webapp.RequestHandler methods.
270
271 Example:
272
273 decorator = OAuth2Decorator(
274 client_id='837...ent.com',
275 client_secret='Qh...wwI',
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500276 scope='https://www.googleapis.com/auth/plus')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400277
278
279 class MainHandler(webapp.RequestHandler):
280
281 @decorator.oauth_required
282 def get(self):
283 http = decorator.http()
284 # http is authorized with the user's Credentials and can be used
285 # in API calls
286
287 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400288
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400289 @util.positional(4)
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400290 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400291 auth_uri='https://accounts.google.com/o/oauth2/auth',
Joe Gregoriof08a4982011-10-07 13:11:16 -0400292 token_uri='https://accounts.google.com/o/oauth2/token',
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100293 user_agent=None,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400294 message=None,
295 callback_path='/oauth2callback',
296 **kwargs):
Joe Gregorio432f17e2011-05-22 23:18:00 -0400297
298 """Constructor for OAuth2Decorator
299
300 Args:
301 client_id: string, client identifier.
302 client_secret: string client secret.
Joe Gregoriof2f8a5a2011-10-14 15:11:29 -0400303 scope: string or list of strings, scope(s) of the credentials being
304 requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400305 auth_uri: string, URI for authorization endpoint. For convenience
306 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
307 token_uri: string, URI for token endpoint. For convenience
308 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
Johan Euphrosineacf517f2012-02-13 21:08:33 +0100309 user_agent: string, User agent of your application, default to None.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400310 message: Message to display if there are problems with the OAuth 2.0
311 configuration. The message may contain HTML and will be presented on the
312 web interface for any method that uses the decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400313 callback_path: string, The absolute path to use as the callback URI. Note
314 that this must match up with the URI given when registering the
315 application in the APIs Console.
Joe Gregorio1adde1a2012-01-06 12:30:35 -0500316 **kwargs: dict, Keyword arguments are be passed along as kwargs to the
317 OAuth2WebServerFlow constructor.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400318 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400319 self.flow = None
Joe Gregorio432f17e2011-05-22 23:18:00 -0400320 self.credentials = None
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400321 self._client_id = client_id
322 self._client_secret = client_secret
323 self._scope = scope
324 self._auth_uri = auth_uri
325 self._token_uri = token_uri
326 self._user_agent = user_agent
327 self._kwargs = kwargs
Joe Gregoriof08a4982011-10-07 13:11:16 -0400328 self._message = message
329 self._in_error = False
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400330 self._callback_path = callback_path
Joe Gregoriof08a4982011-10-07 13:11:16 -0400331
332 def _display_error_message(self, request_handler):
333 request_handler.response.out.write('<html><body>')
334 request_handler.response.out.write(self._message)
335 request_handler.response.out.write('</body></html>')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400336
337 def oauth_required(self, method):
338 """Decorator that starts the OAuth 2.0 dance.
339
340 Starts the OAuth dance for the logged in user if they haven't already
341 granted access for this application.
342
343 Args:
344 method: callable, to be decorated method of a webapp.RequestHandler
345 instance.
346 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400347
Joe Gregorio17774972012-03-01 11:11:59 -0500348 def check_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400349 if self._in_error:
350 self._display_error_message(request_handler)
351 return
352
Joe Gregoriof427c532011-06-13 09:35:26 -0400353 user = users.get_current_user()
354 # Don't use @login_decorator as this could be used in a POST request.
355 if not user:
356 request_handler.redirect(users.create_login_url(
357 request_handler.request.uri))
358 return
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400359
360 self._create_flow(request_handler)
361
Joe Gregorio432f17e2011-05-22 23:18:00 -0400362 # Store the request URI in 'state' so we can use it later
363 self.flow.params['state'] = request_handler.request.url
Joe Gregorio432f17e2011-05-22 23:18:00 -0400364 self.credentials = StorageByKeyName(
365 CredentialsModel, user.user_id(), 'credentials').get()
366
367 if not self.has_credentials():
368 return request_handler.redirect(self.authorize_url())
369 try:
Joe Gregorio17774972012-03-01 11:11:59 -0500370 method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400371 except AccessTokenRefreshError:
372 return request_handler.redirect(self.authorize_url())
373
374 return check_oauth
375
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400376 def _create_flow(self, request_handler):
377 """Create the Flow object.
378
379 The Flow is calculated lazily since we don't know where this app is
380 running until it receives a request, at which point redirect_uri can be
381 calculated and then the Flow object can be constructed.
382
383 Args:
384 request_handler: webapp.RequestHandler, the request handler.
385 """
386 if self.flow is None:
387 redirect_uri = request_handler.request.relative_url(
388 self._callback_path) # Usually /oauth2callback
389 self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
390 self._scope, redirect_uri=redirect_uri,
391 user_agent=self._user_agent,
392 auth_uri=self._auth_uri,
393 token_uri=self._token_uri, **self._kwargs)
394
395
Joe Gregorio432f17e2011-05-22 23:18:00 -0400396 def oauth_aware(self, method):
397 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
398
399 Does all the setup for the OAuth dance, but doesn't initiate it.
400 This decorator is useful if you want to create a page that knows
401 whether or not the user has granted access to this application.
402 From within a method decorated with @oauth_aware the has_credentials()
403 and authorize_url() methods can be called.
404
405 Args:
406 method: callable, to be decorated method of a webapp.RequestHandler
407 instance.
408 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400409
Joe Gregorio17774972012-03-01 11:11:59 -0500410 def setup_oauth(request_handler, *args, **kwargs):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400411 if self._in_error:
412 self._display_error_message(request_handler)
413 return
414
Joe Gregoriof427c532011-06-13 09:35:26 -0400415 user = users.get_current_user()
416 # Don't use @login_decorator as this could be used in a POST request.
417 if not user:
418 request_handler.redirect(users.create_login_url(
419 request_handler.request.uri))
420 return
Joe Gregoriof08a4982011-10-07 13:11:16 -0400421
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400422 self._create_flow(request_handler)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400423
Joe Gregorio432f17e2011-05-22 23:18:00 -0400424 self.flow.params['state'] = request_handler.request.url
Joe Gregorio432f17e2011-05-22 23:18:00 -0400425 self.credentials = StorageByKeyName(
426 CredentialsModel, user.user_id(), 'credentials').get()
Joe Gregorio17774972012-03-01 11:11:59 -0500427 method(request_handler, *args, **kwargs)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400428 return setup_oauth
429
430 def has_credentials(self):
431 """True if for the logged in user there are valid access Credentials.
432
433 Must only be called from with a webapp.RequestHandler subclassed method
434 that had been decorated with either @oauth_required or @oauth_aware.
435 """
436 return self.credentials is not None and not self.credentials.invalid
437
438 def authorize_url(self):
439 """Returns the URL to start the OAuth dance.
440
441 Must only be called from with a webapp.RequestHandler subclassed method
442 that had been decorated with either @oauth_required or @oauth_aware.
443 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400444 url = self.flow.step1_get_authorize_url()
Joe Gregorio853bcf32012-03-02 15:30:23 -0500445 return str(url)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400446
447 def http(self):
448 """Returns an authorized http instance.
449
450 Must only be called from within an @oauth_required decorated method, or
451 from within an @oauth_aware decorated method where has_credentials()
452 returns True.
453 """
454 return self.credentials.authorize(httplib2.Http())
455
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400456 @property
457 def callback_path(self):
458 """The absolute path where the callback will occur.
459
460 Note this is the absolute path, not the absolute URI, that will be
461 calculated by the decorator at runtime. See callback_handler() for how this
462 should be used.
463
464 Returns:
465 The callback path as a string.
466 """
467 return self._callback_path
468
469
470 def callback_handler(self):
471 """RequestHandler for the OAuth 2.0 redirect callback.
472
473 Usage:
474 app = webapp.WSGIApplication([
475 ('/index', MyIndexHandler),
476 ...,
477 (decorator.callback_path, decorator.callback_handler())
478 ])
479
480 Returns:
481 A webapp.RequestHandler that handles the redirect back from the
482 server during the OAuth 2.0 dance.
483 """
484 decorator = self
485
486 class OAuth2Handler(webapp.RequestHandler):
487 """Handler for the redirect_uri of the OAuth 2.0 dance."""
488
489 @login_required
490 def get(self):
491 error = self.request.get('error')
492 if error:
493 errormsg = self.request.get('error_description', error)
494 self.response.out.write(
495 'The authorization request failed: %s' % errormsg)
496 else:
497 user = users.get_current_user()
498 decorator._create_flow(self)
499 credentials = decorator.flow.step2_exchange(self.request.params)
500 StorageByKeyName(
501 CredentialsModel, user.user_id(), 'credentials').put(credentials)
502 self.redirect(str(self.request.get('state')))
503
504 return OAuth2Handler
505
506 def callback_application(self):
507 """WSGI application for handling the OAuth 2.0 redirect callback.
508
509 If you need finer grained control use `callback_handler` which returns just
510 the webapp.RequestHandler.
511
512 Returns:
513 A webapp.WSGIApplication that handles the redirect back from the
514 server during the OAuth 2.0 dance.
515 """
516 return webapp.WSGIApplication([
517 (self.callback_path, self.callback_handler())
518 ])
519
Joe Gregorio432f17e2011-05-22 23:18:00 -0400520
Joe Gregoriof08a4982011-10-07 13:11:16 -0400521class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
522 """An OAuth2Decorator that builds from a clientsecrets file.
523
524 Uses a clientsecrets file as the source for all the information when
525 constructing an OAuth2Decorator.
526
527 Example:
528
529 decorator = OAuth2DecoratorFromClientSecrets(
530 os.path.join(os.path.dirname(__file__), 'client_secrets.json')
Joe Gregorioc4fc0952011-11-09 12:21:11 -0500531 scope='https://www.googleapis.com/auth/plus')
Joe Gregoriof08a4982011-10-07 13:11:16 -0400532
533
534 class MainHandler(webapp.RequestHandler):
535
536 @decorator.oauth_required
537 def get(self):
538 http = decorator.http()
539 # http is authorized with the user's Credentials and can be used
540 # in API calls
541 """
542
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400543 @util.positional(3)
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400544 def __init__(self, filename, scope, message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400545 """Constructor
546
547 Args:
548 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400549 scope: string or list of strings, scope(s) of the credentials being
550 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400551 message: string, A friendly string to display to the user if the
552 clientsecrets file is missing or invalid. The message may contain HTML and
553 will be presented on the web interface for any method that uses the
554 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400555 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400556 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400557 """
558 try:
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400559 client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400560 if client_type not in [clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
561 raise InvalidClientSecretsError('OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
562 super(OAuth2DecoratorFromClientSecrets,
563 self).__init__(
564 client_info['client_id'],
565 client_info['client_secret'],
566 scope,
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400567 auth_uri=client_info['auth_uri'],
568 token_uri=client_info['token_uri'],
569 message=message)
Joe Gregoriof08a4982011-10-07 13:11:16 -0400570 except clientsecrets.InvalidClientSecretsError:
571 self._in_error = True
572 if message is not None:
573 self._message = message
574 else:
575 self._message = "Please configure your application for OAuth 2.0"
576
577
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400578@util.positional(2)
579def oauth2decorator_from_clientsecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400580 message=None, cache=None):
Joe Gregoriof08a4982011-10-07 13:11:16 -0400581 """Creates an OAuth2Decorator populated from a clientsecrets file.
582
583 Args:
584 filename: string, File name of client secrets.
Joe Gregorio08cdcb82012-03-14 00:09:33 -0400585 scope: string or list of strings, scope(s) of the credentials being
586 requested.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400587 message: string, A friendly string to display to the user if the
588 clientsecrets file is missing or invalid. The message may contain HTML and
589 will be presented on the web interface for any method that uses the
590 decorator.
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400591 cache: An optional cache service client that implements get() and set()
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400592 methods. See clientsecrets.loadfile() for details.
Joe Gregoriof08a4982011-10-07 13:11:16 -0400593
594 Returns: An OAuth2Decorator
595
596 """
Joe Gregorio68a8cfe2012-08-03 16:17:40 -0400597 return OAuth2DecoratorFromClientSecrets(filename, scope,
Joe Gregorioc29aaa92012-07-16 16:16:31 -0400598 message=message, cache=cache)