blob: 560038720518ce95ee6be22f7618f9e405bf014d [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 """
223 @login_required
224 def check_oauth(request_handler, *args):
225 # Store the request URI in 'state' so we can use it later
226 self.flow.params['state'] = request_handler.request.url
227 self._request_handler = request_handler
228 user = users.get_current_user()
229 self.credentials = StorageByKeyName(
230 CredentialsModel, user.user_id(), 'credentials').get()
231
232 if not self.has_credentials():
233 return request_handler.redirect(self.authorize_url())
234 try:
235 method(request_handler, *args)
236 except AccessTokenRefreshError:
237 return request_handler.redirect(self.authorize_url())
238
239 return check_oauth
240
241 def oauth_aware(self, method):
242 """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
243
244 Does all the setup for the OAuth dance, but doesn't initiate it.
245 This decorator is useful if you want to create a page that knows
246 whether or not the user has granted access to this application.
247 From within a method decorated with @oauth_aware the has_credentials()
248 and authorize_url() methods can be called.
249
250 Args:
251 method: callable, to be decorated method of a webapp.RequestHandler
252 instance.
253 """
254 @login_required
255 def setup_oauth(request_handler, *args):
256 self.flow.params['state'] = request_handler.request.url
257 self._request_handler = request_handler
258 user = users.get_current_user()
259 self.credentials = StorageByKeyName(
260 CredentialsModel, user.user_id(), 'credentials').get()
261 method(request_handler, *args)
262 return setup_oauth
263
264 def has_credentials(self):
265 """True if for the logged in user there are valid access Credentials.
266
267 Must only be called from with a webapp.RequestHandler subclassed method
268 that had been decorated with either @oauth_required or @oauth_aware.
269 """
270 return self.credentials is not None and not self.credentials.invalid
271
272 def authorize_url(self):
273 """Returns the URL to start the OAuth dance.
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 callback = self._request_handler.request.relative_url('/oauth2callback')
279 url = self.flow.step1_get_authorize_url(callback)
280 user = users.get_current_user()
281 memcache.set(user.user_id(), pickle.dumps(self.flow),
282 namespace=OAUTH2CLIENT_NAMESPACE)
283 return url
284
285 def http(self):
286 """Returns an authorized http instance.
287
288 Must only be called from within an @oauth_required decorated method, or
289 from within an @oauth_aware decorated method where has_credentials()
290 returns True.
291 """
292 return self.credentials.authorize(httplib2.Http())
293
294
295class OAuth2Handler(webapp.RequestHandler):
296 """Handler for the redirect_uri of the OAuth 2.0 dance."""
297
298 @login_required
299 def get(self):
300 error = self.request.get('error')
301 if error:
302 errormsg = self.request.get('error_description', error)
303 self.response.out.write('The authorization request failed: %s' % errormsg)
304 else:
305 user = users.get_current_user()
306 flow = pickle.loads(memcache.get(user.user_id(), namespace=OAUTH2CLIENT_NAMESPACE))
307 # This code should be ammended with application specific error
308 # handling. The following cases should be considered:
309 # 1. What if the flow doesn't exist in memcache? Or is corrupt?
310 # 2. What if the step2_exchange fails?
311 if flow:
312 credentials = flow.step2_exchange(self.request.params)
313 StorageByKeyName(
314 CredentialsModel, user.user_id(), 'credentials').put(credentials)
315 self.redirect(self.request.get('state'))
316 else:
317 # TODO Add error handling here.
318 pass
319
320
321application = webapp.WSGIApplication([('/oauth2callback', OAuth2Handler)])
322
323def main():
324 run_wsgi_app(application)
325
326#