blob: a9766c7f57291791eb70c7f8904c87f4eeb31ded [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
26import logging
27
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
74 def __init__(self, scope, user_agent,
75 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.
82 user_agent: string, The HTTP User-Agent to provide for this application.
83 audience: string, The audience, or verifier of the assertion. For
84 convenience defaults to Google's audience.
85 assertion_type: string, Type name that will identify the format of the
86 assertion string. For convience, defaults to the JSON Web Token (JWT)
87 assertion type string.
88 token_uri: string, URI for token endpoint. For convenience
89 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
90 """
91 self.scope = scope
92 self.audience = audience
93 self.app_name = app_identity.get_service_account_name()
94
95 super(AppAssertionCredentials, self).__init__(
96 assertion_type,
97 user_agent,
98 token_uri)
99
100 def _generate_assertion(self):
101 header = {
102 'typ': 'JWT',
103 'alg': 'RS256',
104 }
105
106 now = int(time.time())
107 claims = {
108 'aud': self.audience,
109 'scope': self.scope,
110 'iat': now,
111 'exp': now + 3600,
112 'iss': self.app_name,
113 }
114
115 jwt_components = [base64.b64encode(simplejson.dumps(seg))
116 for seg in [header, claims]]
117
118 base_str = ".".join(jwt_components)
119 key_name, signature = app_identity.sign_blob(base_str)
120 jwt_components.append(base64.b64encode(signature))
121 return ".".join(jwt_components)
122
123
Joe Gregorio695fdc12011-01-16 16:46:55 -0500124class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500125 """App Engine datastore Property for Flow.
126
127 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500128 oauth2client.Flow"""
129
130 # Tell what the user type is.
131 data_type = Flow
132
133 # For writing to datastore.
134 def get_value_for_datastore(self, model_instance):
135 flow = super(FlowProperty,
136 self).get_value_for_datastore(model_instance)
137 return db.Blob(pickle.dumps(flow))
138
139 # For reading from datastore.
140 def make_value_from_datastore(self, value):
141 if value is None:
142 return None
143 return pickle.loads(value)
144
145 def validate(self, value):
146 if value is not None and not isinstance(value, Flow):
147 raise BadValueError('Property %s must be convertible '
148 'to a FlowThreeLegged instance (%s)' %
149 (self.name, value))
150 return super(FlowProperty, self).validate(value)
151
152 def empty(self, value):
153 return not value
154
155
156class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500157 """App Engine datastore Property for Credentials.
158
159 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500160 oath2client.Credentials
161 """
162
163 # Tell what the user type is.
164 data_type = Credentials
165
166 # For writing to datastore.
167 def get_value_for_datastore(self, model_instance):
168 cred = super(CredentialsProperty,
169 self).get_value_for_datastore(model_instance)
170 return db.Blob(pickle.dumps(cred))
171
172 # For reading from datastore.
173 def make_value_from_datastore(self, value):
174 if value is None:
175 return None
176 return pickle.loads(value)
177
178 def validate(self, value):
179 if value is not None and not isinstance(value, Credentials):
180 raise BadValueError('Property %s must be convertible '
181 'to an Credentials instance (%s)' %
182 (self.name, value))
183 return super(CredentialsProperty, self).validate(value)
184
185 def empty(self, value):
186 return not value
187
188
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500189class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500190 """Store and retrieve a single credential to and from
191 the App Engine datastore.
192
193 This Storage helper presumes the Credentials
194 have been stored as a CredenialsProperty
195 on a datastore model class, and that entities
196 are stored by key_name.
197 """
198
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
213 def get(self):
214 """Retrieve Credential from datastore.
215
216 Returns:
217 oauth2client.Credentials
218 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400219 if self._cache:
220 credential = self._cache.get(self._key_name)
221 if credential:
222 return pickle.loads(credential)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500223 entity = self._model.get_or_insert(self._key_name)
224 credential = getattr(entity, self._property_name)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500225 if credential and hasattr(credential, 'set_store'):
226 credential.set_store(self.put)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400227 if self._cache:
228 self._cache.set(self._key_name, pickle.dumps(credentials))
229
Joe Gregorio695fdc12011-01-16 16:46:55 -0500230 return credential
231
232 def put(self, credentials):
233 """Write a Credentials to the datastore.
234
235 Args:
236 credentials: Credentials, the credentials to store.
237 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500238 entity = self._model.get_or_insert(self._key_name)
239 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500240 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400241 if self._cache:
242 self._cache.set(self._key_name, pickle.dumps(credentials))
243
244
245class CredentialsModel(db.Model):
246 """Storage for OAuth 2.0 Credentials
247
248 Storage of the model is keyed by the user.user_id().
249 """
250 credentials = CredentialsProperty()
251
252
253class OAuth2Decorator(object):
254 """Utility for making OAuth 2.0 easier.
255
256 Instantiate and then use with oauth_required or oauth_aware
257 as decorators on webapp.RequestHandler methods.
258
259 Example:
260
261 decorator = OAuth2Decorator(
262 client_id='837...ent.com',
263 client_secret='Qh...wwI',
264 scope='https://www.googleapis.com/auth/buzz',
265 user_agent='my-sample-app/1.0')
266
267
268 class MainHandler(webapp.RequestHandler):
269
270 @decorator.oauth_required
271 def get(self):
272 http = decorator.http()
273 # http is authorized with the user's Credentials and can be used
274 # in API calls
275
276 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400277
Joe Gregorio432f17e2011-05-22 23:18:00 -0400278 def __init__(self, client_id, client_secret, scope, user_agent,
279 auth_uri='https://accounts.google.com/o/oauth2/auth',
280 token_uri='https://accounts.google.com/o/oauth2/token'):
281
282 """Constructor for OAuth2Decorator
283
284 Args:
285 client_id: string, client identifier.
286 client_secret: string client secret.
287 scope: string, scope of the credentials being requested.
288 user_agent: string, HTTP User-Agent to provide for this application.
289 auth_uri: string, URI for authorization endpoint. For convenience
290 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
291 token_uri: string, URI for token endpoint. For convenience
292 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
293 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400294 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope,
295 user_agent, auth_uri, token_uri)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400296 self.credentials = None
297 self._request_handler = None
298
299 def oauth_required(self, method):
300 """Decorator that starts the OAuth 2.0 dance.
301
302 Starts the OAuth dance for the logged in user if they haven't already
303 granted access for this application.
304
305 Args:
306 method: callable, to be decorated method of a webapp.RequestHandler
307 instance.
308 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400309
Joe Gregorio432f17e2011-05-22 23:18:00 -0400310 def check_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400311 user = users.get_current_user()
312 # Don't use @login_decorator as this could be used in a POST request.
313 if not user:
314 request_handler.redirect(users.create_login_url(
315 request_handler.request.uri))
316 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400317 # Store the request URI in 'state' so we can use it later
318 self.flow.params['state'] = request_handler.request.url
319 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400320 self.credentials = StorageByKeyName(
321 CredentialsModel, user.user_id(), 'credentials').get()
322
323 if not self.has_credentials():
324 return request_handler.redirect(self.authorize_url())
325 try:
326 method(request_handler, *args)
327 except AccessTokenRefreshError:
328 return request_handler.redirect(self.authorize_url())
329
330 return check_oauth
331
332 def oauth_aware(self, method):
333 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
334
335 Does all the setup for the OAuth dance, but doesn't initiate it.
336 This decorator is useful if you want to create a page that knows
337 whether or not the user has granted access to this application.
338 From within a method decorated with @oauth_aware the has_credentials()
339 and authorize_url() methods can be called.
340
341 Args:
342 method: callable, to be decorated method of a webapp.RequestHandler
343 instance.
344 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400345
Joe Gregorio432f17e2011-05-22 23:18:00 -0400346 def setup_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400347 user = users.get_current_user()
348 # Don't use @login_decorator as this could be used in a POST request.
349 if not user:
350 request_handler.redirect(users.create_login_url(
351 request_handler.request.uri))
352 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400353 self.flow.params['state'] = request_handler.request.url
354 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400355 self.credentials = StorageByKeyName(
356 CredentialsModel, user.user_id(), 'credentials').get()
357 method(request_handler, *args)
358 return setup_oauth
359
360 def has_credentials(self):
361 """True if for the logged in user there are valid access Credentials.
362
363 Must only be called from with a webapp.RequestHandler subclassed method
364 that had been decorated with either @oauth_required or @oauth_aware.
365 """
366 return self.credentials is not None and not self.credentials.invalid
367
368 def authorize_url(self):
369 """Returns the URL to start the OAuth dance.
370
371 Must only be called from with a webapp.RequestHandler subclassed method
372 that had been decorated with either @oauth_required or @oauth_aware.
373 """
374 callback = self._request_handler.request.relative_url('/oauth2callback')
375 url = self.flow.step1_get_authorize_url(callback)
376 user = users.get_current_user()
377 memcache.set(user.user_id(), pickle.dumps(self.flow),
378 namespace=OAUTH2CLIENT_NAMESPACE)
379 return url
380
381 def http(self):
382 """Returns an authorized http instance.
383
384 Must only be called from within an @oauth_required decorated method, or
385 from within an @oauth_aware decorated method where has_credentials()
386 returns True.
387 """
388 return self.credentials.authorize(httplib2.Http())
389
390
391class OAuth2Handler(webapp.RequestHandler):
392 """Handler for the redirect_uri of the OAuth 2.0 dance."""
393
394 @login_required
395 def get(self):
396 error = self.request.get('error')
397 if error:
398 errormsg = self.request.get('error_description', error)
JacobMoshenko8e905102011-06-20 09:53:10 -0400399 self.response.out.write(
400 'The authorization request failed: %s' % errormsg)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400401 else:
402 user = users.get_current_user()
JacobMoshenko8e905102011-06-20 09:53:10 -0400403 flow = pickle.loads(memcache.get(user.user_id(),
404 namespace=OAUTH2CLIENT_NAMESPACE))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400405 # This code should be ammended with application specific error
406 # handling. The following cases should be considered:
407 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
408 # 2. What if the step2_exchange fails?
409 if flow:
410 credentials = flow.step2_exchange(self.request.params)
411 StorageByKeyName(
412 CredentialsModel, user.user_id(), 'credentials').put(credentials)
413 self.redirect(self.request.get('state'))
414 else:
415 # TODO Add error handling here.
416 pass
417
418
419application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
420
JacobMoshenko8e905102011-06-20 09:53:10 -0400421
Joe Gregorio432f17e2011-05-22 23:18:00 -0400422def main():
423 run_wsgi_app(application)
424
425#