blob: aacdaef9296f0530644c3b3a0540c6397e397e0d [file] [log] [blame]
Joe Gregorio67d77772010-09-01 16:45:45 -04001# Copyright 2010 Google Inc. All Rights Reserved.
2
3"""Utilities for OAuth.
4
5Utilities for making it easier to work with OAuth.
6"""
7
8__author__ = 'jcgregorio@google.com (Joe Gregorio)'
9
Joe Gregorioa0a52e42011-02-17 17:13:26 -050010
Joe Gregorio67d77772010-09-01 16:45:45 -040011import copy
Joe Gregorio845a5452010-09-08 13:50:34 -040012import httplib2
Joe Gregorio695fdc12011-01-16 16:46:55 -050013import logging
Joe Gregorio67d77772010-09-01 16:45:45 -040014import oauth2 as oauth
Joe Gregorio845a5452010-09-08 13:50:34 -040015import urllib
Joe Gregorio695fdc12011-01-16 16:46:55 -050016import urlparse
Joe Gregorio695fdc12011-01-16 16:46:55 -050017from anyjson import simplejson
Joe Gregorio67d77772010-09-01 16:45:45 -040018
19try:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050020 from urlparse import parse_qsl
Joe Gregorio67d77772010-09-01 16:45:45 -040021except ImportError:
Joe Gregorio7c22ab22011-02-16 15:32:39 -050022 from cgi import parse_qsl
Joe Gregorio845a5452010-09-08 13:50:34 -040023
24
Tom Miller05cd4f52010-10-06 11:09:12 -070025class Error(Exception):
26 """Base error for this module."""
27 pass
28
29
30class RequestError(Error):
31 """Error occurred during request."""
32 pass
33
34
35class MissingParameter(Error):
Joe Gregorio67d77772010-09-01 16:45:45 -040036 pass
37
Joe Gregorio845a5452010-09-08 13:50:34 -040038
Joe Gregorioa0a52e42011-02-17 17:13:26 -050039class CredentialsInvalidError(Error):
40 pass
41
42
Joe Gregorio845a5452010-09-08 13:50:34 -040043def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040044 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040045
46
Joe Gregorio67d77772010-09-01 16:45:45 -040047def _oauth_uri(name, discovery, params):
ade@google.coma9907a22010-12-11 02:44:37 +000048 """Look up the OAuth URI from the discovery
Joe Gregorio845a5452010-09-08 13:50:34 -040049 document and add query parameters based on
50 params.
51
52 name - The name of the OAuth URI to lookup, one
53 of 'request', 'access', or 'authorize'.
54 discovery - Portion of discovery document the describes
55 the OAuth endpoints.
56 params - Dictionary that is used to form the query parameters
57 for the specified URI.
58 """
Joe Gregorio67d77772010-09-01 16:45:45 -040059 if name not in ['request', 'access', 'authorize']:
60 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040061 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040062 query = {}
63 for key in keys:
64 if key in params:
65 query[key] = params[key]
66 return discovery[name]['url'] + '?' + urllib.urlencode(query)
67
Joe Gregorio845a5452010-09-08 13:50:34 -040068
69class Credentials(object):
70 """Base class for all Credentials objects.
71
72 Subclasses must define an authorize() method
73 that applies the credentials to an HTTP transport.
74 """
75
76 def authorize(self, http):
77 """Take an httplib2.Http instance (or equivalent) and
78 authorizes it for the set of credentials, usually by
79 replacing http.request() with a method that adds in
80 the appropriate headers and then delegates to the original
81 Http.request() method.
82 """
83 _abstract()
84
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050085
Joe Gregorio695fdc12011-01-16 16:46:55 -050086class Flow(object):
87 """Base class for all Flow objects."""
88 pass
Joe Gregorio845a5452010-09-08 13:50:34 -040089
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050090
Joe Gregorioa0a52e42011-02-17 17:13:26 -050091class Storage(object):
92 """Base class for all Storage objects.
93
94 Store and retrieve a single credential.
95 """
96
97 def get(self):
98 """Retrieve credential.
99
100 Returns:
101 apiclient.oauth.Credentials
102 """
103 _abstract()
104
105 def put(self, credentials):
106 """Write a credential.
107
108 Args:
109 credentials: Credentials, the credentials to store.
110 """
111 _abstract()
112
113
Joe Gregorio845a5452010-09-08 13:50:34 -0400114class OAuthCredentials(Credentials):
115 """Credentials object for OAuth 1.0a
116 """
117
118 def __init__(self, consumer, token, user_agent):
119 """
120 consumer - An instance of oauth.Consumer.
121 token - An instance of oauth.Token constructed with
122 the access token and secret.
123 user_agent - The HTTP User-Agent to provide for this application.
124 """
125 self.consumer = consumer
126 self.token = token
127 self.user_agent = user_agent
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500128 self.store = None
129
130 # True if the credentials have been revoked
131 self._invalid = False
132
133 @property
134 def invalid(self):
135 """True if the credentials are invalid, such as being revoked."""
136 return getattr(self, "_invalid", False)
137
138 def set_store(self, store):
139 """Set the storage for the credential.
140
141 Args:
142 store: callable, a callable that when passed a Credential
143 will store the credential back to where it came from.
144 This is needed to store the latest access_token if it
145 has been revoked.
146 """
147 self.store = store
148
149 def __getstate__(self):
150 """Trim the state down to something that can be pickled.
151 """
152 d = copy.copy(self.__dict__)
153 del d['store']
154 return d
155
156 def __setstate__(self, state):
157 """Reconstitute the state of the object from being pickled.
158 """
159 self.__dict__.update(state)
160 self.store = None
Joe Gregorio845a5452010-09-08 13:50:34 -0400161
162 def authorize(self, http):
163 """
164 Args:
165 http - An instance of httplib2.Http
166 or something that acts like it.
167
168 Returns:
169 A modified instance of http that was passed in.
170
171 Example:
172
173 h = httplib2.Http()
174 h = credentials.authorize(h)
175
176 You can't create a new OAuth
177 subclass of httplib2.Authenication because
178 it never gets passed the absolute URI, which is
179 needed for signing. So instead we have to overload
180 'request' with a closure that adds in the
181 Authorization header and then calls the original version
182 of 'request()'.
183 """
184 request_orig = http.request
185 signer = oauth.SignatureMethod_HMAC_SHA1()
186
187 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400188 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400189 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
190 connection_type=None):
191 """Modify the request headers to add the appropriate
192 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500193 response_code = 302
194 http.follow_redirects = False
195 while response_code in [301, 302]:
196 req = oauth.Request.from_consumer_and_token(
197 self.consumer, self.token, http_method=method, http_url=uri)
198 req.sign_request(signer, self.consumer, self.token)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500199 if headers is None:
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500200 headers = {}
201 headers.update(req.to_header())
202 if 'user-agent' in headers:
203 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
204 else:
205 headers['user-agent'] = self.user_agent
206 resp, content = request_orig(uri, method, body, headers,
207 redirections, connection_type)
208 response_code = resp.status
209 if response_code in [301, 302]:
210 uri = resp['location']
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500211
212 # Update the stored credential if it becomes invalid.
213 if response_code == 401:
214 logging.info('Access token no longer valid: %s' % content)
215 self._invalid = True
216 if self.store is not None:
217 self.store(self)
218 raise CredentialsInvalidError("Credentials are no longer valid.")
219
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500220 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400221
222 http.request = new_request
223 return http
224
225
Joe Gregorio695fdc12011-01-16 16:46:55 -0500226class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400227 """Does the Three Legged Dance for OAuth 1.0a.
228 """
229
Joe Gregorio67d77772010-09-01 16:45:45 -0400230 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
231 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400232 """
233 discovery - Section of the API discovery document that describes
234 the OAuth endpoints.
235 consumer_key - OAuth consumer key
236 consumer_secret - OAuth consumer secret
237 user_agent - The HTTP User-Agent that identifies the application.
238 **kwargs - The keyword arguments are all optional and required
239 parameters for the OAuth calls.
240 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400241 self.discovery = discovery
242 self.consumer_key = consumer_key
243 self.consumer_secret = consumer_secret
244 self.user_agent = user_agent
245 self.params = kwargs
246 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400247 required = {}
248 for uriinfo in discovery.itervalues():
249 for name, value in uriinfo['parameters'].iteritems():
250 if value['required'] and not name.startswith('oauth_'):
251 required[name] = 1
252 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400253 if key not in self.params:
254 raise MissingParameter('Required parameter %s not supplied' % key)
255
Joe Gregorio845a5452010-09-08 13:50:34 -0400256 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400257 """Returns a URI to redirect to the provider.
258
Joe Gregorio845a5452010-09-08 13:50:34 -0400259 oauth_callback - Either the string 'oob' for a non-web-based application,
260 or a URI that handles the callback from the authorization
261 server.
262
263 If oauth_callback is 'oob' then pass in the
264 generated verification code to step2_exchange,
265 otherwise pass in the query parameters received
266 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400267 """
268 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
269 client = oauth.Client(consumer)
270
271 headers = {
272 'user-agent': self.user_agent,
273 'content-type': 'application/x-www-form-urlencoded'
274 }
275 body = urllib.urlencode({'oauth_callback': oauth_callback})
276 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400277
Joe Gregorio67d77772010-09-01 16:45:45 -0400278 resp, content = client.request(uri, 'POST', headers=headers,
279 body=body)
280 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500281 logging.error('Failed to retrieve temporary authorization: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700282 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400283
284 self.request_token = dict(parse_qsl(content))
285
286 auth_params = copy.copy(self.params)
287 auth_params['oauth_token'] = self.request_token['oauth_token']
288
Joe Gregorio845a5452010-09-08 13:50:34 -0400289 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400290
Joe Gregorio845a5452010-09-08 13:50:34 -0400291 def step2_exchange(self, verifier):
292 """Exhanges an authorized request token
293 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400294
Joe Gregorio845a5452010-09-08 13:50:34 -0400295 verifier - either the verifier token, or a dictionary
296 of the query parameters to the callback, which contains
297 the oauth_verifier.
298 """
299
300 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
301 verifier = verifier['oauth_verifier']
302
303 token = oauth.Token(
304 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400305 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400306 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400307 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
308 client = oauth.Client(consumer, token)
309
310 headers = {
311 'user-agent': self.user_agent,
312 'content-type': 'application/x-www-form-urlencoded'
313 }
314
315 uri = _oauth_uri('access', self.discovery, self.params)
316 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400317 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500318 logging.error('Failed to retrieve access token: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700319 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400320
Joe Gregorio845a5452010-09-08 13:50:34 -0400321 oauth_params = dict(parse_qsl(content))
322 token = oauth.Token(
323 oauth_params['oauth_token'],
324 oauth_params['oauth_token_secret'])
325
326 return OAuthCredentials(consumer, token, self.user_agent)