blob: 430d068ee9a1e618b86a30bc3e2cea2d32d94388 [file] [log] [blame]
Joe Gregorio20a5aa92011-04-01 17:44:25 -04001# 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.
Joe Gregorio67d77772010-09-01 16:45:45 -040014
15"""Utilities for OAuth.
16
17Utilities for making it easier to work with OAuth.
18"""
19
20__author__ = 'jcgregorio@google.com (Joe Gregorio)'
21
Joe Gregorioa0a52e42011-02-17 17:13:26 -050022
Joe Gregorio67d77772010-09-01 16:45:45 -040023import copy
Joe Gregorio845a5452010-09-08 13:50:34 -040024import httplib2
Joe Gregorio695fdc12011-01-16 16:46:55 -050025import logging
Joe Gregorio67d77772010-09-01 16:45:45 -040026import oauth2 as oauth
Joe Gregorio845a5452010-09-08 13:50:34 -040027import urllib
Joe Gregorio695fdc12011-01-16 16:46:55 -050028import urlparse
Joe Gregorio695fdc12011-01-16 16:46:55 -050029from anyjson import simplejson
Joe Gregorio67d77772010-09-01 16:45:45 -040030
31try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050032 from urlparse import parse_qsl
Joe Gregorio67d77772010-09-01 16:45:45 -040033except ImportError:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050034 from cgi import parse_qsl
Joe Gregorio845a5452010-09-08 13:50:34 -040035
36
Tom Miller05cd4f52010-10-06 11:09:12 -070037class Error(Exception):
38 """Base error for this module."""
39 pass
40
41
42class RequestError(Error):
43 """Error occurred during request."""
44 pass
45
46
47class MissingParameter(Error):
Joe Gregorio67d77772010-09-01 16:45:45 -040048 pass
49
Joe Gregorio845a5452010-09-08 13:50:34 -040050
Joe Gregorioa0a52e42011-02-17 17:13:26 -050051class CredentialsInvalidError(Error):
52 pass
53
54
Joe Gregorio845a5452010-09-08 13:50:34 -040055def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040056 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040057
58
Joe Gregorio67d77772010-09-01 16:45:45 -040059def _oauth_uri(name, discovery, params):
ade@google.coma9907a22010-12-11 02:44:37 +000060 """Look up the OAuth URI from the discovery
Joe Gregorio845a5452010-09-08 13:50:34 -040061 document and add query parameters based on
62 params.
63
64 name - The name of the OAuth URI to lookup, one
65 of 'request', 'access', or 'authorize'.
66 discovery - Portion of discovery document the describes
67 the OAuth endpoints.
68 params - Dictionary that is used to form the query parameters
69 for the specified URI.
70 """
Joe Gregorio67d77772010-09-01 16:45:45 -040071 if name not in ['request', 'access', 'authorize']:
72 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040073 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040074 query = {}
75 for key in keys:
76 if key in params:
77 query[key] = params[key]
78 return discovery[name]['url'] + '?' + urllib.urlencode(query)
79
Joe Gregorio845a5452010-09-08 13:50:34 -040080
81class Credentials(object):
82 """Base class for all Credentials objects.
83
84 Subclasses must define an authorize() method
85 that applies the credentials to an HTTP transport.
86 """
87
88 def authorize(self, http):
89 """Take an httplib2.Http instance (or equivalent) and
90 authorizes it for the set of credentials, usually by
91 replacing http.request() with a method that adds in
92 the appropriate headers and then delegates to the original
93 Http.request() method.
94 """
95 _abstract()
96
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050097
Joe Gregorio695fdc12011-01-16 16:46:55 -050098class Flow(object):
99 """Base class for all Flow objects."""
100 pass
Joe Gregorio845a5452010-09-08 13:50:34 -0400101
Joe Gregorio7a6df3a2011-01-31 21:55:21 -0500102
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500103class Storage(object):
104 """Base class for all Storage objects.
105
106 Store and retrieve a single credential.
107 """
108
109 def get(self):
110 """Retrieve credential.
111
112 Returns:
113 apiclient.oauth.Credentials
114 """
115 _abstract()
116
117 def put(self, credentials):
118 """Write a credential.
119
120 Args:
121 credentials: Credentials, the credentials to store.
122 """
123 _abstract()
124
125
Joe Gregorio845a5452010-09-08 13:50:34 -0400126class OAuthCredentials(Credentials):
127 """Credentials object for OAuth 1.0a
128 """
129
130 def __init__(self, consumer, token, user_agent):
131 """
132 consumer - An instance of oauth.Consumer.
133 token - An instance of oauth.Token constructed with
134 the access token and secret.
135 user_agent - The HTTP User-Agent to provide for this application.
136 """
137 self.consumer = consumer
138 self.token = token
139 self.user_agent = user_agent
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500140 self.store = None
141
142 # True if the credentials have been revoked
143 self._invalid = False
144
145 @property
146 def invalid(self):
147 """True if the credentials are invalid, such as being revoked."""
148 return getattr(self, "_invalid", False)
149
150 def set_store(self, store):
151 """Set the storage for the credential.
152
153 Args:
154 store: callable, a callable that when passed a Credential
155 will store the credential back to where it came from.
156 This is needed to store the latest access_token if it
157 has been revoked.
158 """
159 self.store = store
160
161 def __getstate__(self):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500162 """Trim the state down to something that can be pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500163 d = copy.copy(self.__dict__)
164 del d['store']
165 return d
166
167 def __setstate__(self, state):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500168 """Reconstitute the state of the object from being pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500169 self.__dict__.update(state)
170 self.store = None
Joe Gregorio845a5452010-09-08 13:50:34 -0400171
172 def authorize(self, http):
173 """
174 Args:
175 http - An instance of httplib2.Http
176 or something that acts like it.
177
178 Returns:
179 A modified instance of http that was passed in.
180
181 Example:
182
183 h = httplib2.Http()
184 h = credentials.authorize(h)
185
186 You can't create a new OAuth
187 subclass of httplib2.Authenication because
188 it never gets passed the absolute URI, which is
189 needed for signing. So instead we have to overload
190 'request' with a closure that adds in the
191 Authorization header and then calls the original version
192 of 'request()'.
193 """
194 request_orig = http.request
195 signer = oauth.SignatureMethod_HMAC_SHA1()
196
197 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400198 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400199 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
200 connection_type=None):
201 """Modify the request headers to add the appropriate
202 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500203 response_code = 302
204 http.follow_redirects = False
205 while response_code in [301, 302]:
206 req = oauth.Request.from_consumer_and_token(
207 self.consumer, self.token, http_method=method, http_url=uri)
208 req.sign_request(signer, self.consumer, self.token)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500209 if headers is None:
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500210 headers = {}
211 headers.update(req.to_header())
212 if 'user-agent' in headers:
213 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
214 else:
215 headers['user-agent'] = self.user_agent
216 resp, content = request_orig(uri, method, body, headers,
217 redirections, connection_type)
218 response_code = resp.status
219 if response_code in [301, 302]:
220 uri = resp['location']
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500221
222 # Update the stored credential if it becomes invalid.
223 if response_code == 401:
224 logging.info('Access token no longer valid: %s' % content)
225 self._invalid = True
226 if self.store is not None:
227 self.store(self)
228 raise CredentialsInvalidError("Credentials are no longer valid.")
229
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500230 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400231
232 http.request = new_request
233 return http
234
235
Joe Gregorio695fdc12011-01-16 16:46:55 -0500236class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400237 """Does the Three Legged Dance for OAuth 1.0a.
238 """
239
Joe Gregorio67d77772010-09-01 16:45:45 -0400240 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
241 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400242 """
243 discovery - Section of the API discovery document that describes
244 the OAuth endpoints.
245 consumer_key - OAuth consumer key
246 consumer_secret - OAuth consumer secret
247 user_agent - The HTTP User-Agent that identifies the application.
248 **kwargs - The keyword arguments are all optional and required
249 parameters for the OAuth calls.
250 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400251 self.discovery = discovery
252 self.consumer_key = consumer_key
253 self.consumer_secret = consumer_secret
254 self.user_agent = user_agent
255 self.params = kwargs
256 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400257 required = {}
258 for uriinfo in discovery.itervalues():
259 for name, value in uriinfo['parameters'].iteritems():
260 if value['required'] and not name.startswith('oauth_'):
261 required[name] = 1
262 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400263 if key not in self.params:
264 raise MissingParameter('Required parameter %s not supplied' % key)
265
Joe Gregorio845a5452010-09-08 13:50:34 -0400266 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400267 """Returns a URI to redirect to the provider.
268
Joe Gregorio845a5452010-09-08 13:50:34 -0400269 oauth_callback - Either the string 'oob' for a non-web-based application,
270 or a URI that handles the callback from the authorization
271 server.
272
273 If oauth_callback is 'oob' then pass in the
274 generated verification code to step2_exchange,
275 otherwise pass in the query parameters received
276 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400277 """
278 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
279 client = oauth.Client(consumer)
280
281 headers = {
282 'user-agent': self.user_agent,
283 'content-type': 'application/x-www-form-urlencoded'
284 }
285 body = urllib.urlencode({'oauth_callback': oauth_callback})
286 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400287
Joe Gregorio67d77772010-09-01 16:45:45 -0400288 resp, content = client.request(uri, 'POST', headers=headers,
289 body=body)
290 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500291 logging.error('Failed to retrieve temporary authorization: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700292 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400293
294 self.request_token = dict(parse_qsl(content))
295
296 auth_params = copy.copy(self.params)
297 auth_params['oauth_token'] = self.request_token['oauth_token']
298
Joe Gregorio845a5452010-09-08 13:50:34 -0400299 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400300
Joe Gregorio845a5452010-09-08 13:50:34 -0400301 def step2_exchange(self, verifier):
302 """Exhanges an authorized request token
303 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400304
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500305 Args:
306 verifier: string, dict - either the verifier token, or a dictionary
Joe Gregorio845a5452010-09-08 13:50:34 -0400307 of the query parameters to the callback, which contains
308 the oauth_verifier.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500309 Returns:
310 The Credentials object.
Joe Gregorio845a5452010-09-08 13:50:34 -0400311 """
312
313 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
314 verifier = verifier['oauth_verifier']
315
316 token = oauth.Token(
317 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400318 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400319 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400320 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
321 client = oauth.Client(consumer, token)
322
323 headers = {
324 'user-agent': self.user_agent,
325 'content-type': 'application/x-www-form-urlencoded'
326 }
327
328 uri = _oauth_uri('access', self.discovery, self.params)
329 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400330 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500331 logging.error('Failed to retrieve access token: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700332 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400333
Joe Gregorio845a5452010-09-08 13:50:34 -0400334 oauth_params = dict(parse_qsl(content))
335 token = oauth.Token(
336 oauth_params['oauth_token'],
337 oauth_params['oauth_token_secret'])
338
339 return OAuthCredentials(consumer, token, self.user_agent)