blob: 71aa8f0833cb6846c7b86b34d88ddfcf64820c7c [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
24
Joe Gregorio432f17e2011-05-22 23:18:00 -040025from client import AccessTokenRefreshError
Joe Gregorio695fdc12011-01-16 16:46:55 -050026from client import Credentials
27from client import Flow
Joe Gregorio432f17e2011-05-22 23:18:00 -040028from client import OAuth2WebServerFlow
Joe Gregoriodeeb0202011-02-15 14:49:57 -050029from client import Storage
Joe Gregorio432f17e2011-05-22 23:18:00 -040030from google.appengine.api import memcache
31from google.appengine.api import users
32from google.appengine.ext import db
33from google.appengine.ext import webapp
34from google.appengine.ext.webapp.util import login_required
35from google.appengine.ext.webapp.util import run_wsgi_app
Joe Gregorio695fdc12011-01-16 16:46:55 -050036
Joe Gregorio432f17e2011-05-22 23:18:00 -040037OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
Joe Gregorio695fdc12011-01-16 16:46:55 -050038
39class FlowProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050040 """App Engine datastore Property for Flow.
41
42 Utility property that allows easy storage and retreival of an
Joe Gregorio695fdc12011-01-16 16:46:55 -050043 oauth2client.Flow"""
44
45 # Tell what the user type is.
46 data_type = Flow
47
48 # For writing to datastore.
49 def get_value_for_datastore(self, model_instance):
50 flow = super(FlowProperty,
51 self).get_value_for_datastore(model_instance)
52 return db.Blob(pickle.dumps(flow))
53
54 # For reading from datastore.
55 def make_value_from_datastore(self, value):
56 if value is None:
57 return None
58 return pickle.loads(value)
59
60 def validate(self, value):
61 if value is not None and not isinstance(value, Flow):
62 raise BadValueError('Property %s must be convertible '
63 'to a FlowThreeLegged instance (%s)' %
64 (self.name, value))
65 return super(FlowProperty, self).validate(value)
66
67 def empty(self, value):
68 return not value
69
70
71class CredentialsProperty(db.Property):
Joe Gregorio7c22ab22011-02-16 15:32:39 -050072 """App Engine datastore Property for Credentials.
73
74 Utility property that allows easy storage and retrieval of
Joe Gregorio695fdc12011-01-16 16:46:55 -050075 oath2client.Credentials
76 """
77
78 # Tell what the user type is.
79 data_type = Credentials
80
81 # For writing to datastore.
82 def get_value_for_datastore(self, model_instance):
83 cred = super(CredentialsProperty,
84 self).get_value_for_datastore(model_instance)
85 return db.Blob(pickle.dumps(cred))
86
87 # For reading from datastore.
88 def make_value_from_datastore(self, value):
89 if value is None:
90 return None
91 return pickle.loads(value)
92
93 def validate(self, value):
94 if value is not None and not isinstance(value, Credentials):
95 raise BadValueError('Property %s must be convertible '
96 'to an Credentials instance (%s)' %
97 (self.name, value))
98 return super(CredentialsProperty, self).validate(value)
99
100 def empty(self, value):
101 return not value
102
103
Joe Gregoriodeeb0202011-02-15 14:49:57 -0500104class StorageByKeyName(Storage):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500105 """Store and retrieve a single credential to and from
106 the App Engine datastore.
107
108 This Storage helper presumes the Credentials
109 have been stored as a CredenialsProperty
110 on a datastore model class, and that entities
111 are stored by key_name.
112 """
113
Joe Gregorio432f17e2011-05-22 23:18:00 -0400114 def __init__(self, model, key_name, property_name, cache=None):
Joe Gregorio695fdc12011-01-16 16:46:55 -0500115 """Constructor for Storage.
116
117 Args:
118 model: db.Model, model class
119 key_name: string, key name for the entity that has the credentials
120 property_name: string, name of the property that is an CredentialsProperty
Joe Gregorio432f17e2011-05-22 23:18:00 -0400121 cache: memcache, a write-through cache to put in front of the datastore
Joe Gregorio695fdc12011-01-16 16:46:55 -0500122 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500123 self._model = model
124 self._key_name = key_name
125 self._property_name = property_name
Joe Gregorio432f17e2011-05-22 23:18:00 -0400126 self._cache = cache
Joe Gregorio695fdc12011-01-16 16:46:55 -0500127
128 def get(self):
129 """Retrieve Credential from datastore.
130
131 Returns:
132 oauth2client.Credentials
133 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400134 if self._cache:
135 credential = self._cache.get(self._key_name)
136 if credential:
137 return pickle.loads(credential)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500138 entity = self._model.get_or_insert(self._key_name)
139 credential = getattr(entity, self._property_name)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500140 if credential and hasattr(credential, 'set_store'):
141 credential.set_store(self.put)
Joe Gregorio432f17e2011-05-22 23:18:00 -0400142 if self._cache:
143 self._cache.set(self._key_name, pickle.dumps(credentials))
144
Joe Gregorio695fdc12011-01-16 16:46:55 -0500145 return credential
146
147 def put(self, credentials):
148 """Write a Credentials to the datastore.
149
150 Args:
151 credentials: Credentials, the credentials to store.
152 """
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500153 entity = self._model.get_or_insert(self._key_name)
154 setattr(entity, self._property_name, credentials)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500155 entity.put()
Joe Gregorio432f17e2011-05-22 23:18:00 -0400156 if self._cache:
157 self._cache.set(self._key_name, pickle.dumps(credentials))
158
159
160class CredentialsModel(db.Model):
161 """Storage for OAuth 2.0 Credentials
162
163 Storage of the model is keyed by the user.user_id().
164 """
165 credentials = CredentialsProperty()
166
167
168class OAuth2Decorator(object):
169 """Utility for making OAuth 2.0 easier.
170
171 Instantiate and then use with oauth_required or oauth_aware
172 as decorators on webapp.RequestHandler methods.
173
174 Example:
175
176 decorator = OAuth2Decorator(
177 client_id='837...ent.com',
178 client_secret='Qh...wwI',
179 scope='https://www.googleapis.com/auth/buzz',
180 user_agent='my-sample-app/1.0')
181
182
183 class MainHandler(webapp.RequestHandler):
184
185 @decorator.oauth_required
186 def get(self):
187 http = decorator.http()
188 # http is authorized with the user's Credentials and can be used
189 # in API calls
190
191 """
192 def __init__(self, client_id, client_secret, scope, user_agent,
193 auth_uri='https://accounts.google.com/o/oauth2/auth',
194 token_uri='https://accounts.google.com/o/oauth2/token'):
195
196 """Constructor for OAuth2Decorator
197
198 Args:
199 client_id: string, client identifier.
200 client_secret: string client secret.
201 scope: string, scope of the credentials being requested.
202 user_agent: string, HTTP User-Agent to provide for this application.
203 auth_uri: string, URI for authorization endpoint. For convenience
204 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
205 token_uri: string, URI for token endpoint. For convenience
206 defaults to Google's endpoints but any OAuth 2.0 provider can be used.
207 """
208 self.flow = OAuth2WebServerFlow(client_id, client_secret, scope, user_agent,
209 auth_uri, token_uri)
210 self.credentials = None
211 self._request_handler = None
212
213 def oauth_required(self, method):
214 """Decorator that starts the OAuth 2.0 dance.
215
216 Starts the OAuth dance for the logged in user if they haven't already
217 granted access for this application.
218
219 Args:
220 method: callable, to be decorated method of a webapp.RequestHandler
221 instance.
222 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400223 def check_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400224 user = users.get_current_user()
225 # Don't use @login_decorator as this could be used in a POST request.
226 if not user:
227 request_handler.redirect(users.create_login_url(
228 request_handler.request.uri))
229 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400230 # Store the request URI in 'state' so we can use it later
231 self.flow.params['state'] = request_handler.request.url
232 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400233 self.credentials = StorageByKeyName(
234 CredentialsModel, user.user_id(), 'credentials').get()
235
236 if not self.has_credentials():
237 return request_handler.redirect(self.authorize_url())
238 try:
239 method(request_handler, *args)
240 except AccessTokenRefreshError:
241 return request_handler.redirect(self.authorize_url())
242
243 return check_oauth
244
245 def oauth_aware(self, method):
246 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
247
248 Does all the setup for the OAuth dance, but doesn't initiate it.
249 This decorator is useful if you want to create a page that knows
250 whether or not the user has granted access to this application.
251 From within a method decorated with @oauth_aware the has_credentials()
252 and authorize_url() methods can be called.
253
254 Args:
255 method: callable, to be decorated method of a webapp.RequestHandler
256 instance.
257 """
Joe Gregorio432f17e2011-05-22 23:18:00 -0400258 def setup_oauth(request_handler, *args):
Joe Gregoriof427c532011-06-13 09:35:26 -0400259 user = users.get_current_user()
260 # Don't use @login_decorator as this could be used in a POST request.
261 if not user:
262 request_handler.redirect(users.create_login_url(
263 request_handler.request.uri))
264 return
Joe Gregorio432f17e2011-05-22 23:18:00 -0400265 self.flow.params['state'] = request_handler.request.url
266 self._request_handler = request_handler
Joe Gregorio432f17e2011-05-22 23:18:00 -0400267 self.credentials = StorageByKeyName(
268 CredentialsModel, user.user_id(), 'credentials').get()
269 method(request_handler, *args)
270 return setup_oauth
271
272 def has_credentials(self):
273 """True if for the logged in user there are valid access Credentials.
274
275 Must only be called from with a webapp.RequestHandler subclassed method
276 that had been decorated with either @oauth_required or @oauth_aware.
277 """
278 return self.credentials is not None and not self.credentials.invalid
279
280 def authorize_url(self):
281 """Returns the URL to start the OAuth dance.
282
283 Must only be called from with a webapp.RequestHandler subclassed method
284 that had been decorated with either @oauth_required or @oauth_aware.
285 """
286 callback = self._request_handler.request.relative_url('/oauth2callback')
287 url = self.flow.step1_get_authorize_url(callback)
288 user = users.get_current_user()
289 memcache.set(user.user_id(), pickle.dumps(self.flow),
290 namespace=OAUTH2CLIENT_NAMESPACE)
291 return url
292
293 def http(self):
294 """Returns an authorized http instance.
295
296 Must only be called from within an @oauth_required decorated method, or
297 from within an @oauth_aware decorated method where has_credentials()
298 returns True.
299 """
300 return self.credentials.authorize(httplib2.Http())
301
302
303class OAuth2Handler(webapp.RequestHandler):
304 """Handler for the redirect_uri of the OAuth 2.0 dance."""
305
306 @login_required
307 def get(self):
308 error = self.request.get('error')
309 if error:
310 errormsg = self.request.get('error_description', error)
311 self.response.out.write('The authorization request failed: %s' % errormsg)
312 else:
313 user = users.get_current_user()
314 flow = pickle.loads(memcache.get(user.user_id(), namespace=OAUTH2CLIENT_NAMESPACE))
315 # This code should be ammended with application specific error
316 # handling. The following cases should be considered:
317 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
318 # 2. What if the step2_exchange fails?
319 if flow:
320 credentials = flow.step2_exchange(self.request.params)
321 StorageByKeyName(
322 CredentialsModel, user.user_id(), 'credentials').put(credentials)
323 self.redirect(self.request.get('state'))
324 else:
325 # TODO Add error handling here.
326 pass
327
328
329application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
330
331def main():
332 run_wsgi_app(application)
333
334#