blob: 618b29fe4e2740a507c2b02dc064ba1c28b7a6e9 [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):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500150 """Trim the state down to something that can be pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500151 d = copy.copy(self.__dict__)
152 del d['store']
153 return d
154
155 def __setstate__(self, state):
Joe Gregorio825d78d2011-02-18 15:07:17 -0500156 """Reconstitute the state of the object from being pickled."""
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500157 self.__dict__.update(state)
158 self.store = None
Joe Gregorio845a5452010-09-08 13:50:34 -0400159
160 def authorize(self, http):
161 """
162 Args:
163 http - An instance of httplib2.Http
164 or something that acts like it.
165
166 Returns:
167 A modified instance of http that was passed in.
168
169 Example:
170
171 h = httplib2.Http()
172 h = credentials.authorize(h)
173
174 You can't create a new OAuth
175 subclass of httplib2.Authenication because
176 it never gets passed the absolute URI, which is
177 needed for signing. So instead we have to overload
178 'request' with a closure that adds in the
179 Authorization header and then calls the original version
180 of 'request()'.
181 """
182 request_orig = http.request
183 signer = oauth.SignatureMethod_HMAC_SHA1()
184
185 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400186 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400187 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
188 connection_type=None):
189 """Modify the request headers to add the appropriate
190 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500191 response_code = 302
192 http.follow_redirects = False
193 while response_code in [301, 302]:
194 req = oauth.Request.from_consumer_and_token(
195 self.consumer, self.token, http_method=method, http_url=uri)
196 req.sign_request(signer, self.consumer, self.token)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500197 if headers is None:
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500198 headers = {}
199 headers.update(req.to_header())
200 if 'user-agent' in headers:
201 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
202 else:
203 headers['user-agent'] = self.user_agent
204 resp, content = request_orig(uri, method, body, headers,
205 redirections, connection_type)
206 response_code = resp.status
207 if response_code in [301, 302]:
208 uri = resp['location']
Joe Gregorioa0a52e42011-02-17 17:13:26 -0500209
210 # Update the stored credential if it becomes invalid.
211 if response_code == 401:
212 logging.info('Access token no longer valid: %s' % content)
213 self._invalid = True
214 if self.store is not None:
215 self.store(self)
216 raise CredentialsInvalidError("Credentials are no longer valid.")
217
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500218 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400219
220 http.request = new_request
221 return http
222
223
Joe Gregorio695fdc12011-01-16 16:46:55 -0500224class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400225 """Does the Three Legged Dance for OAuth 1.0a.
226 """
227
Joe Gregorio67d77772010-09-01 16:45:45 -0400228 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
229 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400230 """
231 discovery - Section of the API discovery document that describes
232 the OAuth endpoints.
233 consumer_key - OAuth consumer key
234 consumer_secret - OAuth consumer secret
235 user_agent - The HTTP User-Agent that identifies the application.
236 **kwargs - The keyword arguments are all optional and required
237 parameters for the OAuth calls.
238 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400239 self.discovery = discovery
240 self.consumer_key = consumer_key
241 self.consumer_secret = consumer_secret
242 self.user_agent = user_agent
243 self.params = kwargs
244 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400245 required = {}
246 for uriinfo in discovery.itervalues():
247 for name, value in uriinfo['parameters'].iteritems():
248 if value['required'] and not name.startswith('oauth_'):
249 required[name] = 1
250 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400251 if key not in self.params:
252 raise MissingParameter('Required parameter %s not supplied' % key)
253
Joe Gregorio845a5452010-09-08 13:50:34 -0400254 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400255 """Returns a URI to redirect to the provider.
256
Joe Gregorio845a5452010-09-08 13:50:34 -0400257 oauth_callback - Either the string 'oob' for a non-web-based application,
258 or a URI that handles the callback from the authorization
259 server.
260
261 If oauth_callback is 'oob' then pass in the
262 generated verification code to step2_exchange,
263 otherwise pass in the query parameters received
264 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400265 """
266 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
267 client = oauth.Client(consumer)
268
269 headers = {
270 'user-agent': self.user_agent,
271 'content-type': 'application/x-www-form-urlencoded'
272 }
273 body = urllib.urlencode({'oauth_callback': oauth_callback})
274 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400275
Joe Gregorio67d77772010-09-01 16:45:45 -0400276 resp, content = client.request(uri, 'POST', headers=headers,
277 body=body)
278 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500279 logging.error('Failed to retrieve temporary authorization: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700280 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400281
282 self.request_token = dict(parse_qsl(content))
283
284 auth_params = copy.copy(self.params)
285 auth_params['oauth_token'] = self.request_token['oauth_token']
286
Joe Gregorio845a5452010-09-08 13:50:34 -0400287 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400288
Joe Gregorio845a5452010-09-08 13:50:34 -0400289 def step2_exchange(self, verifier):
290 """Exhanges an authorized request token
291 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400292
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500293 Args:
294 verifier: string, dict - either the verifier token, or a dictionary
Joe Gregorio845a5452010-09-08 13:50:34 -0400295 of the query parameters to the callback, which contains
296 the oauth_verifier.
Joe Gregorio9e5fe4d2011-03-10 09:33:47 -0500297 Returns:
298 The Credentials object.
Joe Gregorio845a5452010-09-08 13:50:34 -0400299 """
300
301 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
302 verifier = verifier['oauth_verifier']
303
304 token = oauth.Token(
305 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400306 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400307 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400308 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
309 client = oauth.Client(consumer, token)
310
311 headers = {
312 'user-agent': self.user_agent,
313 'content-type': 'application/x-www-form-urlencoded'
314 }
315
316 uri = _oauth_uri('access', self.discovery, self.params)
317 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400318 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500319 logging.error('Failed to retrieve access token: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700320 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400321
Joe Gregorio845a5452010-09-08 13:50:34 -0400322 oauth_params = dict(parse_qsl(content))
323 token = oauth.Token(
324 oauth_params['oauth_token'],
325 oauth_params['oauth_token_secret'])
326
327 return OAuthCredentials(consumer, token, self.user_agent)