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