blob: 2811069b1d8a58e4c403cb617f9685d0b17840ee [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 Gregorio432f17e2011-05-22 23:18:00 -040022import httplib2
Joe Gregorio695fdc12011-01-16 16:46:55 -050023import pickle
JacobMoshenko8e905102011-06-20 09:53:10 -040024import time
25import base64
JacobMoshenko8e905102011-06-20 09:53:10 -040026
27try: # pragma: no cover
28 import simplejson
29except ImportError: # pragma: no cover
30 try:
31 # Try to import from django, should work on App Engine
32 from django.utils import simplejson
33 except ImportError:
34 # Should work for Python2.6 and higher.
35 import json as simplejson
Joe Gregorio695fdc12011-01-16 16:46:55 -050036
Joe Gregorio432f17e2011-05-22 23:18:00 -040037from client import AccessTokenRefreshError
JacobMoshenko8e905102011-06-20 09:53:10 -040038from client import AssertionCredentials
Joe Gregorio695fdc12011-01-16 16:46:55 -050039from client import Credentials
40from client import Flow
Joe Gregorio432f17e2011-05-22 23:18:00 -040041from client import OAuth2WebServerFlow
Joe Gregoriodeeb0202011-02-15 14:49:57 -050042from client import Storage
Joe Gregorio432f17e2011-05-22 23:18:00 -040043from google.appengine.api import memcache
44from google.appengine.api import users
JacobMoshenko8e905102011-06-20 09:53:10 -040045from google.appengine.api.app_identity import app_identity
Joe Gregorio432f17e2011-05-22 23:18:00 -040046from google.appengine.ext import db
47from google.appengine.ext import webapp
48from google.appengine.ext.webapp.util import login_required
49from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregorio695fdc12011-01-16 16:46:55 -050050
Joe Gregorio432f17e2011-05-22 23:18:00 -040051OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050052
JacobMoshenko8e905102011-06-20 09:53:10 -040053
54class AppAssertionCredentials(AssertionCredentials):
55 """Credentials object for App Engine Assertion Grants
56
57 This object will allow an App Engine application to identify itself to Google
58 and other OAuth 2.0 servers that can verify assertions. It can be used for
59 the purpose of accessing data stored under an account assigned to the App
60 Engine application itself. The algorithm used for generating the assertion is
61 the Signed JSON Web Token (JWT) algorithm. Additional details can be found at
62 the following link:
63
64 http://self-issued.info/docs/draft-jones-json-web-token.html
65
66 This credential does not require a flow to instantiate because it represents
67 a two legged flow, and therefore has all of the required information to
68 generate and refresh its own access tokens.
69
70 AssertionFlowCredentials objects may be safely pickled and unpickled.
71 """
72
JacobMoshenkocb6d8912011-07-08 13:35:15 -040073 def __init__(self, scope,
JacobMoshenko8e905102011-06-20 09:53:10 -040074 audience='https://accounts.google.com/o/oauth2/token',
75 assertion_type='http://oauth.net/grant_type/jwt/1.0/bearer',
76 token_uri='https://accounts.google.com/o/oauth2/token', **kwargs):
77 """Constructor for AppAssertionCredentials
78
79 Args:
80 scope: string, scope of the credentials being requested.
JacobMoshenko8e905102011-06-20 09:53:10 -040081 audience: string, The audience, or verifier of the assertion. For
82 convenience defaults to Google's audience.
83 assertion_type: string, Type name that will identify the format of the
84 assertion string. For convience, defaults to the JSON Web Token (JWT)
85 assertion type string.
86 token_uri: string, URI for token endpoint. For convenience
87 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
88 """
89 self.scope = scope
90 self.audience = audience
91 self.app_name = app_identity.get_service_account_name()
92
93 super(AppAssertionCredentials, self).__init__(
94 assertion_type,
JacobMoshenkocb6d8912011-07-08 13:35:15 -040095 None,
JacobMoshenko8e905102011-06-20 09:53:10 -040096 token_uri)
97
Joe Gregorio562b7312011-09-15 09:06:38 -040098 @classmethod
99 def from_json(cls, json):
100 data = simplejson.loads(json)
101 retval = AccessTokenCredentials(
102 data['scope'],
103 data['audience'],
104 data['assertion_type'],
105 data['token_uri'])
106 return retval
107
JacobMoshenko8e905102011-06-20 09:53:10 -0400108 def _generate_assertion(self):
109 header = {
110 'typ': 'JWT',
111 'alg': 'RS256',
112 }
113
114 now = int(time.time())
115 claims = {
116 'aud': self.audience,
117 'scope': self.scope,
118 'iat': now,
119 'exp': now + 3600,
120 'iss': self.app_name,
121 }
122
123 jwt_components = [base64.b64encode(simplejson.dumps(seg))
124 for seg in [header, claims]]
125
126 base_str = ".".join(jwt_components)
127 key_name, signature = app_identity.sign_blob(base_str)
128 jwt_components.append(base64.b64encode(signature))
129 return ".".join(jwt_components)
130
131
Joe Gregorio695fdc12011-01-16 16:46:55 -0500132class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500133 """App Engine datastore Property for Flow.
134
135 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500136 oauth2client.Flow"""
137
138 # Tell what the user type is.
139 data_type = Flow
140
141 # For writing to datastore.
142 def get_value_for_datastore(self, model_instance):
143 flow = super(FlowProperty,
144 self).get_value_for_datastore(model_instance)
145 return db.Blob(pickle.dumps(flow))
146
147 # For reading from datastore.
148 def make_value_from_datastore(self, value):
149 if value is None:
150 return None
151 return pickle.loads(value)
152
153 def validate(self, value):
154 if value is not None and not isinstance(value, Flow):
155 raise BadValueError('Property %s must be convertible '
156 'to a FlowThreeLegged instance (%s)' %
157 (self.name, value))
158 return super(FlowProperty, self).validate(value)
159
160 def empty(self, value):
161 return not value
162
163
164class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500165 """App Engine datastore Property for Credentials.
166
167 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500168 oath2client.Credentials
169 """
170
171 # Tell what the user type is.
172 data_type = Credentials
173
174 # For writing to datastore.
175 def get_value_for_datastore(self, model_instance):
176 cred = super(CredentialsProperty,
177 self).get_value_for_datastore(model_instance)
Joe Gregorio562b7312011-09-15 09:06:38 -0400178 if cred is None:
179 cred = ''
180 else:
181 cred = cred.to_json()
182 return db.Blob(cred)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500183
184 # For reading from datastore.
185 def make_value_from_datastore(self, value):
186 if value is None:
187 return None
Joe Gregorio562b7312011-09-15 09:06:38 -0400188 if len(value) == 0:
189 return None
190 credentials = None
191 try:
192 credentials = Credentials.new_from_json(value)
193 except ValueError:
194 credentials = pickle.loads(value)
195 return credentials
Joe Gregorio695fdc12011-01-16 16:46:55 -0500196
197 def validate(self, value):
198 if value is not None and not isinstance(value, Credentials):
Joe Gregorio562b7312011-09-15 09:06:38 -0400199 raise db.BadValueError('Property %s must be convertible '
Joe Gregorio695fdc12011-01-16 16:46:55 -0500200 'to an Credentials instance (%s)' %
201 (self.name, value))
202 return super(CredentialsProperty, self).validate(value)
203
204 def empty(self, value):
205 return not value
206
207
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500208class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500209 """Store and retrieve a single credential to and from
210 the App Engine datastore.
211
212 This Storage helper presumes the Credentials
213 have been stored as a CredenialsProperty
214 on a datastore model class, and that entities
215 are stored by key_name.
216 """
217
Joe Gregorio432f17e2011-05-22 23:18:00 -0400218 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500219 """Constructor for Storage.
220
221 Args:
222 model: db.Model, model class
223 key_name: string, key name for the entity that has the credentials
JacobMoshenko8e905102011-06-20 09:53:10 -0400224 property_name: string, name of the property that is a CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400225 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500226 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500227 self._model = model
228 self._key_name = key_name
229 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400230 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500231
232 def get(self):
233 """Retrieve Credential from datastore.
234
235 Returns:
236 oauth2client.Credentials
237 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400238 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400239 json = self._cache.get(self._key_name)
240 if json:
241 return Credentials.new_from_json(json)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500242 entity = self._model.get_or_insert(self._key_name)
243 credential = getattr(entity, self._property_name)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500244 if credential and hasattr(credential, 'set_store'):
Joe Gregorio9da2ad82011-09-11 14:04:44 -0400245 credential.set_store(self)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400246 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400247 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400248
Joe Gregorio695fdc12011-01-16 16:46:55 -0500249 return credential
250
251 def put(self, credentials):
252 """Write a Credentials to the datastore.
253
254 Args:
255 credentials: Credentials, the credentials to store.
256 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500257 entity = self._model.get_or_insert(self._key_name)
258 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500259 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400260 if self._cache:
Joe Gregorio562b7312011-09-15 09:06:38 -0400261 self._cache.set(self._key_name, credentials.to_json())
Joe Gregorio432f17e2011-05-22 23:18:00 -0400262
263
264class CredentialsModel(db.Model):
265 """Storage for OAuth 2.0 Credentials
266
267 Storage of the model is keyed by the user.user_id().
268 """
269 credentials = CredentialsProperty()
270
271
272class OAuth2Decorator(object):
273 """Utility for making OAuth 2.0 easier.
274
275 Instantiate and then use with oauth_required or oauth_aware
276 as decorators on webapp.RequestHandler methods.
277
278 Example:
279
280 decorator = OAuth2Decorator(
281 client_id='837...ent.com',
282 client_secret='Qh...wwI',
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400283 scope='https://www.googleapis.com/auth/buzz')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400284
285
286 class MainHandler(webapp.RequestHandler):
287
288 @decorator.oauth_required
289 def get(self):
290 http = decorator.http()
291 # http is authorized with the user's Credentials and can be used
292 # in API calls
293
294 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400295
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400296 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400297 auth_uri='https://accounts.google.com/o/oauth2/auth',
298 token_uri='https://accounts.google.com/o/oauth2/token'):
299
300 """Constructor for OAuth2Decorator
301
302 Args:
303 client_id: string, client identifier.
304 client_secret: string client secret.
305 scope: string, scope of the credentials being 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.
310 """
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400311 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, None,
312 auth_uri, token_uri)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400313 self.credentials = None
314 self._request_handler = None
315
316 def oauth_required(self, method):
317 """Decorator that starts the OAuth 2.0 dance.
318
319 Starts the OAuth dance for the logged in user if they haven't already
320 granted access for this application.
321
322 Args:
323 method: callable, to be decorated method of a webapp.RequestHandler
324 instance.
325 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400326
Joe Gregorio432f17e2011-05-22 23:18:00 -0400327 def check_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400328 user = users.get_current_user()
329 # Don't use @login_decorator as this could be used in a POST request.
330 if not user:
331 request_handler.redirect(users.create_login_url(
332 request_handler.request.uri))
333 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400334 # Store the request URI in 'state' so we can use it later
335 self.flow.params['state'] = request_handler.request.url
336 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400337 self.credentials = StorageByKeyName(
338 CredentialsModel, user.user_id(), 'credentials').get()
339
340 if not self.has_credentials():
341 return request_handler.redirect(self.authorize_url())
342 try:
343 method(request_handler, *args)
344 except AccessTokenRefreshError:
345 return request_handler.redirect(self.authorize_url())
346
347 return check_oauth
348
349 def oauth_aware(self, method):
350 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
351
352 Does all the setup for the OAuth dance, but doesn't initiate it.
353 This decorator is useful if you want to create a page that knows
354 whether or not the user has granted access to this application.
355 From within a method decorated with @oauth_aware the has_credentials()
356 and authorize_url() methods can be called.
357
358 Args:
359 method: callable, to be decorated method of a webapp.RequestHandler
360 instance.
361 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400362
Joe Gregorio432f17e2011-05-22 23:18:00 -0400363 def setup_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400364 user = users.get_current_user()
365 # Don't use @login_decorator as this could be used in a POST request.
366 if not user:
367 request_handler.redirect(users.create_login_url(
368 request_handler.request.uri))
369 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400370 self.flow.params['state'] = request_handler.request.url
371 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400372 self.credentials = StorageByKeyName(
373 CredentialsModel, user.user_id(), 'credentials').get()
374 method(request_handler, *args)
375 return setup_oauth
376
377 def has_credentials(self):
378 """True if for the logged in user there are valid access Credentials.
379
380 Must only be called from with a webapp.RequestHandler subclassed method
381 that had been decorated with either @oauth_required or @oauth_aware.
382 """
383 return self.credentials is not None and not self.credentials.invalid
384
385 def authorize_url(self):
386 """Returns the URL to start the OAuth dance.
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 callback = self._request_handler.request.relative_url('/oauth2callback')
392 url = self.flow.step1_get_authorize_url(callback)
393 user = users.get_current_user()
394 memcache.set(user.user_id(), pickle.dumps(self.flow),
395 namespace=OAUTH2CLIENT_NAMESPACE)
396 return url
397
398 def http(self):
399 """Returns an authorized http instance.
400
401 Must only be called from within an @oauth_required decorated method, or
402 from within an @oauth_aware decorated method where has_credentials()
403 returns True.
404 """
405 return self.credentials.authorize(httplib2.Http())
406
407
408class OAuth2Handler(webapp.RequestHandler):
409 """Handler for the redirect_uri of the OAuth 2.0 dance."""
410
411 @login_required
412 def get(self):
413 error = self.request.get('error')
414 if error:
415 errormsg = self.request.get('error_description', error)
JacobMoshenko8e905102011-06-20 09:53:10 -0400416 self.response.out.write(
417 'The authorization request failed: %s' % errormsg)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400418 else:
419 user = users.get_current_user()
JacobMoshenko8e905102011-06-20 09:53:10 -0400420 flow = pickle.loads(memcache.get(user.user_id(),
421 namespace=OAUTH2CLIENT_NAMESPACE))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400422 # This code should be ammended with application specific error
423 # handling. The following cases should be considered:
424 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
425 # 2. What if the step2_exchange fails?
426 if flow:
427 credentials = flow.step2_exchange(self.request.params)
428 StorageByKeyName(
429 CredentialsModel, user.user_id(), 'credentials').put(credentials)
430 self.redirect(self.request.get('state'))
431 else:
432 # TODO Add error handling here.
433 pass
434
435
436application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
437
JacobMoshenko8e905102011-06-20 09:53:10 -0400438
Joe Gregorio432f17e2011-05-22 23:18:00 -0400439def main():
440 run_wsgi_app(application)
441
442#