blob: 533d53fd502b60905e4019a1c8c888dfadc76250 [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
28try: # pragma: no cover
29 import simplejson
30except ImportError: # pragma: no cover
31 try:
32 # Try to import from django, should work on App Engine
33 from django.utils import simplejson
34 except ImportError:
35 # Should work for Python2.6 and higher.
36 import json as simplejson
Joe Gregorio695fdc12011-01-16 16:46:55 -050037
Joe Gregorio432f17e2011-05-22 23:18:00 -040038from client import AccessTokenRefreshError
JacobMoshenko8e905102011-06-20 09:53:10 -040039from client import AssertionCredentials
Joe Gregorio695fdc12011-01-16 16:46:55 -050040from client import Credentials
41from client import Flow
Joe Gregorio432f17e2011-05-22 23:18:00 -040042from client import OAuth2WebServerFlow
Joe Gregoriodeeb0202011-02-15 14:49:57 -050043from client import Storage
Joe Gregorio432f17e2011-05-22 23:18:00 -040044from google.appengine.api import memcache
45from google.appengine.api import users
JacobMoshenko8e905102011-06-20 09:53:10 -040046from google.appengine.api.app_identity import app_identity
Joe Gregorio432f17e2011-05-22 23:18:00 -040047from google.appengine.ext import db
48from google.appengine.ext import webapp
49from google.appengine.ext.webapp.util import login_required
50from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregorio695fdc12011-01-16 16:46:55 -050051
Joe Gregorio432f17e2011-05-22 23:18:00 -040052OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050053
JacobMoshenko8e905102011-06-20 09:53:10 -040054
55class 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
61 Engine application itself. The algorithm used for generating the assertion is
62 the Signed JSON Web Token (JWT) algorithm. Additional details can be found at
63 the following link:
64
65 http://self-issued.info/docs/draft-jones-json-web-token.html
66
67 This credential does not require a flow to instantiate because it represents
68 a two legged flow, and therefore has all of the required information to
69 generate and refresh its own access tokens.
70
71 AssertionFlowCredentials objects may be safely pickled and unpickled.
72 """
73
JacobMoshenkocb6d8912011-07-08 13:35:15 -040074 def __init__(self, scope,
JacobMoshenko8e905102011-06-20 09:53:10 -040075 audience='https://accounts.google.com/o/oauth2/token',
76 assertion_type='http://oauth.net/grant_type/jwt/1.0/bearer',
77 token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
78 """Constructor for AppAssertionCredentials
79
80 Args:
81 scope: string, scope of the credentials being requested.
JacobMoshenko8e905102011-06-20 09:53:10 -040082 audience: string, The audience, or verifier of the assertion. For
83 convenience defaults to Google's audience.
84 assertion_type: string, Type name that will identify the format of the
85 assertion string. For convience, defaults to the JSON Web Token (JWT)
86 assertion type string.
87 token_uri: string, URI for token endpoint. For convenience
88 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
89 """
90 self.scope = scope
91 self.audience = audience
92 self.app_name = app_identity.get_service_account_name()
93
94 super(AppAssertionCredentials, self).__init__(
95 assertion_type,
JacobMoshenkocb6d8912011-07-08 13:35:15 -040096 None,
JacobMoshenko8e905102011-06-20 09:53:10 -040097 token_uri)
98
Joe Gregorio562b7312011-09-15 09:06:38 -040099 @classmethod
100 def from_json(cls, json):
101 data = simplejson.loads(json)
102 retval = AccessTokenCredentials(
103 data['scope'],
104 data['audience'],
105 data['assertion_type'],
106 data['token_uri'])
107 return retval
108
JacobMoshenko8e905102011-06-20 09:53:10 -0400109 def _generate_assertion(self):
110 header = {
111 'typ': 'JWT',
112 'alg': 'RS256',
113 }
114
115 now = int(time.time())
116 claims = {
117 'aud': self.audience,
118 'scope': self.scope,
119 'iat': now,
120 'exp': now + 3600,
121 'iss': self.app_name,
122 }
123
124 jwt_components = [base64.b64encode(simplejson.dumps(seg))
125 for seg in [header, claims]]
126
127 base_str = ".".join(jwt_components)
128 key_name, signature = app_identity.sign_blob(base_str)
129 jwt_components.append(base64.b64encode(signature))
130 return ".".join(jwt_components)
131
132
Joe Gregorio695fdc12011-01-16 16:46:55 -0500133class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500134 """App Engine datastore Property for Flow.
135
136 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500137 oauth2client.Flow"""
138
139 # Tell what the user type is.
140 data_type = Flow
141
142 # For writing to datastore.
143 def get_value_for_datastore(self, model_instance):
144 flow = super(FlowProperty,
145 self).get_value_for_datastore(model_instance)
146 return db.Blob(pickle.dumps(flow))
147
148 # For reading from datastore.
149 def make_value_from_datastore(self, value):
150 if value is None:
151 return None
152 return pickle.loads(value)
153
154 def validate(self, value):
155 if value is not None and not isinstance(value, Flow):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400156 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500157 'to a FlowThreeLegged instance (%s)' %
158 (self.name, value))
159 return super(FlowProperty, self).validate(value)
160
161 def empty(self, value):
162 return not value
163
164
165class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500166 """App Engine datastore Property for Credentials.
167
168 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500169 oath2client.Credentials
170 """
171
172 # Tell what the user type is.
173 data_type = Credentials
174
175 # For writing to datastore.
176 def get_value_for_datastore(self, model_instance):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400177 logging.info("get: Got type " + str(type(model_instance)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500178 cred = super(CredentialsProperty,
179 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400180 if cred is None:
181 cred = ''
182 else:
183 cred = cred.to_json()
184 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500185
186 # For reading from datastore.
187 def make_value_from_datastore(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400188 logging.info("make: Got a " + value)
189 logging.info("make: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500190 if value is None:
191 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400192 if len(value) == 0:
193 return None
194 credentials = None
195 try:
196 credentials = Credentials.new_from_json(value)
197 except ValueError:
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400198 try:
199 credentials = pickle.loads(value)
200 except ValueError:
201 credentials = None
Joe Gregorio562b7312011-09-15 09:06:38 -0400202 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500203
204 def validate(self, value):
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400205 value = super(CredentialsProperty, self).validate(value)
206 logging.info("validate: Got type " + str(type(value)))
Joe Gregorio695fdc12011-01-16 16:46:55 -0500207 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400208 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio1daa71b2011-09-15 18:12:14 -0400209 'to a Credentials instance (%s)' %
210 (self.name, value))
211 #if value is not None and not isinstance(value, Credentials):
212 # return None
213 return value
Joe Gregorio695fdc12011-01-16 16:46:55 -0500214
215
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500216class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500217 """Store and retrieve a single credential to and from
218 the App Engine datastore.
219
220 This Storage helper presumes the Credentials
221 have been stored as a CredenialsProperty
222 on a datastore model class, and that entities
223 are stored by key_name.
224 """
225
Joe Gregorio432f17e2011-05-22 23:18:00 -0400226 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500227 """Constructor for Storage.
228
229 Args:
230 model: db.Model, model class
231 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400232 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400233 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500234 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500235 self._model = model
236 self._key_name = key_name
237 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400238 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500239
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400240 def locked_get(self):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500241 """Retrieve Credential from datastore.
242
243 Returns:
244 oauth2client.Credentials
245 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400246 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400247 json = self._cache.get(self._key_name)
248 if json:
249 return Credentials.new_from_json(json)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500250 entity = self._model.get_or_insert(self._key_name)
251 credential = getattr(entity, self._property_name)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500252 if credential and hasattr(credential, 'set_store'):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400253 credential.set_store(self)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400254 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400255 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400256
Joe Gregorio695fdc12011-01-16 16:46:55 -0500257 return credential
258
Joe Gregoriod2ee4d82011-09-15 14:32:45 -0400259 def locked_put(self, credentials):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500260 """Write a Credentials to the datastore.
261
262 Args:
263 credentials: Credentials, the credentials to store.
264 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500265 entity = self._model.get_or_insert(self._key_name)
266 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500267 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400268 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400269 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400270
271
272class CredentialsModel(db.Model):
273 """Storage for OAuth 2.0 Credentials
274
275 Storage of the model is keyed by the user.user_id().
276 """
277 credentials = CredentialsProperty()
278
279
280class OAuth2Decorator(object):
281 """Utility for making OAuth 2.0 easier.
282
283 Instantiate and then use with oauth_required or oauth_aware
284 as decorators on webapp.RequestHandler methods.
285
286 Example:
287
288 decorator = OAuth2Decorator(
289 client_id='837...ent.com',
290 client_secret='Qh...wwI',
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400291 scope='https://www.googleapis.com/auth/buzz')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400292
293
294 class MainHandler(webapp.RequestHandler):
295
296 @decorator.oauth_required
297 def get(self):
298 http = decorator.http()
299 # http is authorized with the user's Credentials and can be used
300 # in API calls
301
302 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400303
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400304 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400305 auth_uri='https://accounts.google.com/o/oauth2/auth',
306 token_uri='https://accounts.google.com/o/oauth2/token'):
307
308 """Constructor for OAuth2Decorator
309
310 Args:
311 client_id: string, client identifier.
312 client_secret: string client secret.
313 scope: string, scope of the credentials being requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400314 auth_uri: string, URI for authorization endpoint. For convenience
315 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
316 token_uri: string, URI for token endpoint. For convenience
317 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
318 """
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400319 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, None,
320 auth_uri, token_uri)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400321 self.credentials = None
322 self._request_handler = None
323
324 def oauth_required(self, method):
325 """Decorator that starts the OAuth 2.0 dance.
326
327 Starts the OAuth dance for the logged in user if they haven't already
328 granted access for this application.
329
330 Args:
331 method: callable, to be decorated method of a webapp.RequestHandler
332 instance.
333 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400334
Joe Gregorio432f17e2011-05-22 23:18:00 -0400335 def check_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400336 user = users.get_current_user()
337 # Don't use @login_decorator as this could be used in a POST request.
338 if not user:
339 request_handler.redirect(users.create_login_url(
340 request_handler.request.uri))
341 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400342 # Store the request URI in 'state' so we can use it later
343 self.flow.params['state'] = request_handler.request.url
344 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400345 self.credentials = StorageByKeyName(
346 CredentialsModel, user.user_id(), 'credentials').get()
347
348 if not self.has_credentials():
349 return request_handler.redirect(self.authorize_url())
350 try:
351 method(request_handler, *args)
352 except AccessTokenRefreshError:
353 return request_handler.redirect(self.authorize_url())
354
355 return check_oauth
356
357 def oauth_aware(self, method):
358 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
359
360 Does all the setup for the OAuth dance, but doesn't initiate it.
361 This decorator is useful if you want to create a page that knows
362 whether or not the user has granted access to this application.
363 From within a method decorated with @oauth_aware the has_credentials()
364 and authorize_url() methods can be called.
365
366 Args:
367 method: callable, to be decorated method of a webapp.RequestHandler
368 instance.
369 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400370
Joe Gregorio432f17e2011-05-22 23:18:00 -0400371 def setup_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400372 user = users.get_current_user()
373 # Don't use @login_decorator as this could be used in a POST request.
374 if not user:
375 request_handler.redirect(users.create_login_url(
376 request_handler.request.uri))
377 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400378 self.flow.params['state'] = request_handler.request.url
379 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400380 self.credentials = StorageByKeyName(
381 CredentialsModel, user.user_id(), 'credentials').get()
382 method(request_handler, *args)
383 return setup_oauth
384
385 def has_credentials(self):
386 """True if for the logged in user there are valid access Credentials.
387
388 Must only be called from with a webapp.RequestHandler subclassed method
389 that had been decorated with either @oauth_required or @oauth_aware.
390 """
391 return self.credentials is not None and not self.credentials.invalid
392
393 def authorize_url(self):
394 """Returns the URL to start the OAuth dance.
395
396 Must only be called from with a webapp.RequestHandler subclassed method
397 that had been decorated with either @oauth_required or @oauth_aware.
398 """
399 callback = self._request_handler.request.relative_url('/oauth2callback')
400 url = self.flow.step1_get_authorize_url(callback)
401 user = users.get_current_user()
402 memcache.set(user.user_id(), pickle.dumps(self.flow),
403 namespace=OAUTH2CLIENT_NAMESPACE)
404 return url
405
406 def http(self):
407 """Returns an authorized http instance.
408
409 Must only be called from within an @oauth_required decorated method, or
410 from within an @oauth_aware decorated method where has_credentials()
411 returns True.
412 """
413 return self.credentials.authorize(httplib2.Http())
414
415
416class OAuth2Handler(webapp.RequestHandler):
417 """Handler for the redirect_uri of the OAuth 2.0 dance."""
418
419 @login_required
420 def get(self):
421 error = self.request.get('error')
422 if error:
423 errormsg = self.request.get('error_description', error)
JacobMoshenko8e905102011-06-20 09:53:10 -0400424 self.response.out.write(
425 'The authorization request failed: %s' % errormsg)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400426 else:
427 user = users.get_current_user()
JacobMoshenko8e905102011-06-20 09:53:10 -0400428 flow = pickle.loads(memcache.get(user.user_id(),
429 namespace=OAUTH2CLIENT_NAMESPACE))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400430 # This code should be ammended with application specific error
431 # handling. The following cases should be considered:
432 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
433 # 2. What if the step2_exchange fails?
434 if flow:
435 credentials = flow.step2_exchange(self.request.params)
436 StorageByKeyName(
437 CredentialsModel, user.user_id(), 'credentials').put(credentials)
438 self.redirect(self.request.get('state'))
439 else:
440 # TODO Add error handling here.
441 pass
442
443
444application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
445
JacobMoshenko8e905102011-06-20 09:53:10 -0400446
Joe Gregorio432f17e2011-05-22 23:18:00 -0400447def main():
448 run_wsgi_app(application)
449
450#