blob: 1cc6cefbe1c353d48f15e754199967eaad41fe0b [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
10import copy
Joe Gregorio845a5452010-09-08 13:50:34 -040011import httplib2
Joe Gregorio695fdc12011-01-16 16:46:55 -050012import logging
Joe Gregorio67d77772010-09-01 16:45:45 -040013import oauth2 as oauth
Joe Gregorio845a5452010-09-08 13:50:34 -040014import urllib
Joe Gregorio695fdc12011-01-16 16:46:55 -050015import urlparse
16
17from 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
39def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040040 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040041
42
Joe Gregorio67d77772010-09-01 16:45:45 -040043def _oauth_uri(name, discovery, params):
ade@google.coma9907a22010-12-11 02:44:37 +000044 """Look up the OAuth URI from the discovery
Joe Gregorio845a5452010-09-08 13:50:34 -040045 document and add query parameters based on
46 params.
47
48 name - The name of the OAuth URI to lookup, one
49 of 'request', 'access', or 'authorize'.
50 discovery - Portion of discovery document the describes
51 the OAuth endpoints.
52 params - Dictionary that is used to form the query parameters
53 for the specified URI.
54 """
Joe Gregorio67d77772010-09-01 16:45:45 -040055 if name not in ['request', 'access', 'authorize']:
56 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040057 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040058 query = {}
59 for key in keys:
60 if key in params:
61 query[key] = params[key]
62 return discovery[name]['url'] + '?' + urllib.urlencode(query)
63
Joe Gregorio845a5452010-09-08 13:50:34 -040064
65class Credentials(object):
66 """Base class for all Credentials objects.
67
68 Subclasses must define an authorize() method
69 that applies the credentials to an HTTP transport.
70 """
71
72 def authorize(self, http):
73 """Take an httplib2.Http instance (or equivalent) and
74 authorizes it for the set of credentials, usually by
75 replacing http.request() with a method that adds in
76 the appropriate headers and then delegates to the original
77 Http.request() method.
78 """
79 _abstract()
80
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050081
Joe Gregorio695fdc12011-01-16 16:46:55 -050082class Flow(object):
83 """Base class for all Flow objects."""
84 pass
Joe Gregorio845a5452010-09-08 13:50:34 -040085
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050086
Joe Gregorio845a5452010-09-08 13:50:34 -040087class OAuthCredentials(Credentials):
88 """Credentials object for OAuth 1.0a
89 """
90
91 def __init__(self, consumer, token, user_agent):
92 """
93 consumer - An instance of oauth.Consumer.
94 token - An instance of oauth.Token constructed with
95 the access token and secret.
96 user_agent - The HTTP User-Agent to provide for this application.
97 """
98 self.consumer = consumer
99 self.token = token
100 self.user_agent = user_agent
101
102 def authorize(self, http):
103 """
104 Args:
105 http - An instance of httplib2.Http
106 or something that acts like it.
107
108 Returns:
109 A modified instance of http that was passed in.
110
111 Example:
112
113 h = httplib2.Http()
114 h = credentials.authorize(h)
115
116 You can't create a new OAuth
117 subclass of httplib2.Authenication because
118 it never gets passed the absolute URI, which is
119 needed for signing. So instead we have to overload
120 'request' with a closure that adds in the
121 Authorization header and then calls the original version
122 of 'request()'.
123 """
124 request_orig = http.request
125 signer = oauth.SignatureMethod_HMAC_SHA1()
126
127 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400128 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400129 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
130 connection_type=None):
131 """Modify the request headers to add the appropriate
132 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500133 response_code = 302
134 http.follow_redirects = False
135 while response_code in [301, 302]:
136 req = oauth.Request.from_consumer_and_token(
137 self.consumer, self.token, http_method=method, http_url=uri)
138 req.sign_request(signer, self.consumer, self.token)
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500139 if headers is None:
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500140 headers = {}
141 headers.update(req.to_header())
142 if 'user-agent' in headers:
143 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
144 else:
145 headers['user-agent'] = self.user_agent
146 resp, content = request_orig(uri, method, body, headers,
147 redirections, connection_type)
148 response_code = resp.status
149 if response_code in [301, 302]:
150 uri = resp['location']
151 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400152
153 http.request = new_request
154 return http
155
156
Joe Gregorio695fdc12011-01-16 16:46:55 -0500157class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400158 """Does the Three Legged Dance for OAuth 1.0a.
159 """
160
Joe Gregorio67d77772010-09-01 16:45:45 -0400161 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
162 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400163 """
164 discovery - Section of the API discovery document that describes
165 the OAuth endpoints.
166 consumer_key - OAuth consumer key
167 consumer_secret - OAuth consumer secret
168 user_agent - The HTTP User-Agent that identifies the application.
169 **kwargs - The keyword arguments are all optional and required
170 parameters for the OAuth calls.
171 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400172 self.discovery = discovery
173 self.consumer_key = consumer_key
174 self.consumer_secret = consumer_secret
175 self.user_agent = user_agent
176 self.params = kwargs
177 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400178 required = {}
179 for uriinfo in discovery.itervalues():
180 for name, value in uriinfo['parameters'].iteritems():
181 if value['required'] and not name.startswith('oauth_'):
182 required[name] = 1
183 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400184 if key not in self.params:
185 raise MissingParameter('Required parameter %s not supplied' % key)
186
Joe Gregorio845a5452010-09-08 13:50:34 -0400187 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400188 """Returns a URI to redirect to the provider.
189
Joe Gregorio845a5452010-09-08 13:50:34 -0400190 oauth_callback - Either the string 'oob' for a non-web-based application,
191 or a URI that handles the callback from the authorization
192 server.
193
194 If oauth_callback is 'oob' then pass in the
195 generated verification code to step2_exchange,
196 otherwise pass in the query parameters received
197 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400198 """
199 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
200 client = oauth.Client(consumer)
201
202 headers = {
203 'user-agent': self.user_agent,
204 'content-type': 'application/x-www-form-urlencoded'
205 }
206 body = urllib.urlencode({'oauth_callback': oauth_callback})
207 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400208
Joe Gregorio67d77772010-09-01 16:45:45 -0400209 resp, content = client.request(uri, 'POST', headers=headers,
210 body=body)
211 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500212 logging.error('Failed to retrieve temporary authorization: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700213 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400214
215 self.request_token = dict(parse_qsl(content))
216
217 auth_params = copy.copy(self.params)
218 auth_params['oauth_token'] = self.request_token['oauth_token']
219
Joe Gregorio845a5452010-09-08 13:50:34 -0400220 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400221
Joe Gregorio845a5452010-09-08 13:50:34 -0400222 def step2_exchange(self, verifier):
223 """Exhanges an authorized request token
224 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400225
Joe Gregorio845a5452010-09-08 13:50:34 -0400226 verifier - either the verifier token, or a dictionary
227 of the query parameters to the callback, which contains
228 the oauth_verifier.
229 """
230
231 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
232 verifier = verifier['oauth_verifier']
233
234 token = oauth.Token(
235 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400236 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400237 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400238 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
239 client = oauth.Client(consumer, token)
240
241 headers = {
242 'user-agent': self.user_agent,
243 'content-type': 'application/x-www-form-urlencoded'
244 }
245
246 uri = _oauth_uri('access', self.discovery, self.params)
247 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400248 if resp['status'] != '200':
Joe Gregorio7c22ab22011-02-16 15:32:39 -0500249 logging.error('Failed to retrieve access token: %s', content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700250 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400251
Joe Gregorio845a5452010-09-08 13:50:34 -0400252 oauth_params = dict(parse_qsl(content))
253 token = oauth.Token(
254 oauth_params['oauth_token'],
255 oauth_params['oauth_token_secret'])
256
257 return OAuthCredentials(consumer, token, self.user_agent)