blob: 439a57997a3bc6b91125938d4d5dc870595fdd56 [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
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
99 def _generate_assertion(self):
100 header = {
101 'typ': 'JWT',
102 'alg': 'RS256',
103 }
104
105 now = int(time.time())
106 claims = {
107 'aud': self.audience,
108 'scope': self.scope,
109 'iat': now,
110 'exp': now + 3600,
111 'iss': self.app_name,
112 }
113
114 jwt_components = [base64.b64encode(simplejson.dumps(seg))
115 for seg in [header, claims]]
116
117 base_str = ".".join(jwt_components)
118 key_name, signature = app_identity.sign_blob(base_str)
119 jwt_components.append(base64.b64encode(signature))
120 return ".".join(jwt_components)
121
122
Joe Gregorio695fdc12011-01-16 16:46:55 -0500123class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500124 """App Engine datastore Property for Flow.
125
126 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -0500127 oauth2client.Flow"""
128
129 # Tell what the user type is.
130 data_type = Flow
131
132 # For writing to datastore.
133 def get_value_for_datastore(self, model_instance):
134 flow = super(FlowProperty,
135 self).get_value_for_datastore(model_instance)
136 return db.Blob(pickle.dumps(flow))
137
138 # For reading from datastore.
139 def make_value_from_datastore(self, value):
140 if value is None:
141 return None
142 return pickle.loads(value)
143
144 def validate(self, value):
145 if value is not None and not isinstance(value, Flow):
146 raise BadValueError('Property %s must be convertible '
147 'to a FlowThreeLegged instance (%s)' %
148 (self.name, value))
149 return super(FlowProperty, self).validate(value)
150
151 def empty(self, value):
152 return not value
153
154
155class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500156 """App Engine datastore Property for Credentials.
157
158 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -0500159 oath2client.Credentials
160 """
161
162 # Tell what the user type is.
163 data_type = Credentials
164
165 # For writing to datastore.
166 def get_value_for_datastore(self, model_instance):
167 cred = super(CredentialsProperty,
168 self).get_value_for_datastore(model_instance)
169 return db.Blob(pickle.dumps(cred))
170
171 # For reading from datastore.
172 def make_value_from_datastore(self, value):
173 if value is None:
174 return None
175 return pickle.loads(value)
176
177 def validate(self, value):
178 if value is not None and not isinstance(value, Credentials):
179 raise BadValueError('Property %s must be convertible '
180 'to an Credentials instance (%s)' %
181 (self.name, value))
182 return super(CredentialsProperty, self).validate(value)
183
184 def empty(self, value):
185 return not value
186
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 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
212 def get(self):
213 """Retrieve Credential from datastore.
214
215 Returns:
216 oauth2client.Credentials
217 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400218 if self._cache:
219 credential = self._cache.get(self._key_name)
220 if credential:
221 return pickle.loads(credential)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500222 entity = self._model.get_or_insert(self._key_name)
223 credential = getattr(entity, self._property_name)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500224 if credential and hasattr(credential, 'set_store'):
225 credential.set_store(self.put)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400226 if self._cache:
227 self._cache.set(self._key_name, pickle.dumps(credentials))
228
Joe Gregorio695fdc12011-01-16 16:46:55 -0500229 return credential
230
231 def put(self, credentials):
232 """Write a Credentials to the datastore.
233
234 Args:
235 credentials: Credentials, the credentials to store.
236 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500237 entity = self._model.get_or_insert(self._key_name)
238 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500239 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400240 if self._cache:
241 self._cache.set(self._key_name, pickle.dumps(credentials))
242
243
244class CredentialsModel(db.Model):
245 """Storage for OAuth 2.0 Credentials
246
247 Storage of the model is keyed by the user.user_id().
248 """
249 credentials = CredentialsProperty()
250
251
252class OAuth2Decorator(object):
253 """Utility for making OAuth 2.0 easier.
254
255 Instantiate and then use with oauth_required or oauth_aware
256 as decorators on webapp.RequestHandler methods.
257
258 Example:
259
260 decorator = OAuth2Decorator(
261 client_id='837...ent.com',
262 client_secret='Qh...wwI',
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400263 scope='https://www.googleapis.com/auth/buzz')
Joe Gregorio432f17e2011-05-22 23:18:00 -0400264
265
266 class MainHandler(webapp.RequestHandler):
267
268 @decorator.oauth_required
269 def get(self):
270 http = decorator.http()
271 # http is authorized with the user's Credentials and can be used
272 # in API calls
273
274 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400275
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400276 def __init__(self, client_id, client_secret, scope,
Joe Gregorio432f17e2011-05-22 23:18:00 -0400277 auth_uri='https://accounts.google.com/o/oauth2/auth',
278 token_uri='https://accounts.google.com/o/oauth2/token'):
279
280 """Constructor for OAuth2Decorator
281
282 Args:
283 client_id: string, client identifier.
284 client_secret: string client secret.
285 scope: string, scope of the credentials being requested.
Joe Gregorio432f17e2011-05-22 23:18:00 -0400286 auth_uri: string, URI for authorization endpoint. For convenience
287 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
288 token_uri: string, URI for token endpoint. For convenience
289 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
290 """
JacobMoshenkocb6d8912011-07-08 13:35:15 -0400291 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, None,
292 auth_uri, token_uri)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400293 self.credentials = None
294 self._request_handler = None
295
296 def oauth_required(self, method):
297 """Decorator that starts the OAuth 2.0 dance.
298
299 Starts the OAuth dance for the logged in user if they haven't already
300 granted access for this application.
301
302 Args:
303 method: callable, to be decorated method of a webapp.RequestHandler
304 instance.
305 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400306
Joe Gregorio432f17e2011-05-22 23:18:00 -0400307 def check_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400308 user = users.get_current_user()
309 # Don't use @login_decorator as this could be used in a POST request.
310 if not user:
311 request_handler.redirect(users.create_login_url(
312 request_handler.request.uri))
313 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400314 # Store the request URI in 'state' so we can use it later
315 self.flow.params['state'] = request_handler.request.url
316 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400317 self.credentials = StorageByKeyName(
318 CredentialsModel, user.user_id(), 'credentials').get()
319
320 if not self.has_credentials():
321 return request_handler.redirect(self.authorize_url())
322 try:
323 method(request_handler, *args)
324 except AccessTokenRefreshError:
325 return request_handler.redirect(self.authorize_url())
326
327 return check_oauth
328
329 def oauth_aware(self, method):
330 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
331
332 Does all the setup for the OAuth dance, but doesn't initiate it.
333 This decorator is useful if you want to create a page that knows
334 whether or not the user has granted access to this application.
335 From within a method decorated with @oauth_aware the has_credentials()
336 and authorize_url() methods can be called.
337
338 Args:
339 method: callable, to be decorated method of a webapp.RequestHandler
340 instance.
341 """
JacobMoshenko8e905102011-06-20 09:53:10 -0400342
Joe Gregorio432f17e2011-05-22 23:18:00 -0400343 def setup_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400344 user = users.get_current_user()
345 # Don't use @login_decorator as this could be used in a POST request.
346 if not user:
347 request_handler.redirect(users.create_login_url(
348 request_handler.request.uri))
349 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400350 self.flow.params['state'] = request_handler.request.url
351 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400352 self.credentials = StorageByKeyName(
353 CredentialsModel, user.user_id(), 'credentials').get()
354 method(request_handler, *args)
355 return setup_oauth
356
357 def has_credentials(self):
358 """True if for the logged in user there are valid access Credentials.
359
360 Must only be called from with a webapp.RequestHandler subclassed method
361 that had been decorated with either @oauth_required or @oauth_aware.
362 """
363 return self.credentials is not None and not self.credentials.invalid
364
365 def authorize_url(self):
366 """Returns the URL to start the OAuth dance.
367
368 Must only be called from with a webapp.RequestHandler subclassed method
369 that had been decorated with either @oauth_required or @oauth_aware.
370 """
371 callback = self._request_handler.request.relative_url('/oauth2callback')
372 url = self.flow.step1_get_authorize_url(callback)
373 user = users.get_current_user()
374 memcache.set(user.user_id(), pickle.dumps(self.flow),
375 namespace=OAUTH2CLIENT_NAMESPACE)
376 return url
377
378 def http(self):
379 """Returns an authorized http instance.
380
381 Must only be called from within an @oauth_required decorated method, or
382 from within an @oauth_aware decorated method where has_credentials()
383 returns True.
384 """
385 return self.credentials.authorize(httplib2.Http())
386
387
388class OAuth2Handler(webapp.RequestHandler):
389 """Handler for the redirect_uri of the OAuth 2.0 dance."""
390
391 @login_required
392 def get(self):
393 error = self.request.get('error')
394 if error:
395 errormsg = self.request.get('error_description', error)
JacobMoshenko8e905102011-06-20 09:53:10 -0400396 self.response.out.write(
397 'The authorization request failed: %s' % errormsg)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400398 else:
399 user = users.get_current_user()
JacobMoshenko8e905102011-06-20 09:53:10 -0400400 flow = pickle.loads(memcache.get(user.user_id(),
401 namespace=OAUTH2CLIENT_NAMESPACE))
Joe Gregorio432f17e2011-05-22 23:18:00 -0400402 # This code should be ammended with application specific error
403 # handling. The following cases should be considered:
404 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
405 # 2. What if the step2_exchange fails?
406 if flow:
407 credentials = flow.step2_exchange(self.request.params)
408 StorageByKeyName(
409 CredentialsModel, user.user_id(), 'credentials').put(credentials)
410 self.redirect(self.request.get('state'))
411 else:
412 # TODO Add error handling here.
413 pass
414
415
416application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
417
JacobMoshenko8e905102011-06-20 09:53:10 -0400418
Joe Gregorio432f17e2011-05-22 23:18:00 -0400419def main():
420 run_wsgi_app(application)
421
422#