blob: 059cb5afa75aca60c601d5bb4d28719ff4d7f30c [file] [log] [blame]
Joe Gregorio67d77772010-09-01 16:45:45 -04001#!/usr/bin/python2.4
2#
3# Copyright 2010 Google Inc. All Rights Reserved.
4
5"""Utilities for OAuth.
6
7Utilities for making it easier to work with OAuth.
8"""
9
10__author__ = 'jcgregorio@google.com (Joe Gregorio)'
11
12import copy
Joe Gregorio845a5452010-09-08 13:50:34 -040013import httplib2
Joe Gregorio67d77772010-09-01 16:45:45 -040014import oauth2 as oauth
Joe Gregorio845a5452010-09-08 13:50:34 -040015import urllib
16import logging
Joe Gregorio67d77772010-09-01 16:45:45 -040017
18try:
19 from urlparse import parse_qs, parse_qsl
20except ImportError:
21 from cgi import parse_qs, parse_qsl
Joe Gregorio845a5452010-09-08 13:50:34 -040022
23
Joe Gregorio67d77772010-09-01 16:45:45 -040024class MissingParameter(Exception):
25 pass
26
Joe Gregorio845a5452010-09-08 13:50:34 -040027
28def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040029 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040030
31
Joe Gregorio67d77772010-09-01 16:45:45 -040032buzz_discovery = {
Joe Gregorio67d77772010-09-01 16:45:45 -040033 'request': {
34 'url': 'https://www.google.com/accounts/OAuthGetRequestToken',
Joe Gregorio7943c5d2010-09-08 16:11:43 -040035 'parameters': {
36 'xoauth_displayname': {
37 'parameterType': 'query',
38 'required': False
39 },
40 'domain': {
41 'parameterType': 'query',
42 'required': True
43 },
44 'scope': {
45 'parameterType': 'query',
46 'required': True
47 },
Joe Gregorio67d77772010-09-01 16:45:45 -040048 },
Joe Gregorio7943c5d2010-09-08 16:11:43 -040049 },
Joe Gregorio67d77772010-09-01 16:45:45 -040050 'authorize': {
51 'url': 'https://www.google.com/buzz/api/auth/OAuthAuthorizeToken',
Joe Gregorio7943c5d2010-09-08 16:11:43 -040052 'parameters': {
53 'oauth_token': {
54 'parameterType': 'query',
55 'required': True
56 },
57 'iconUrl': {
58 'parameterType': 'query',
59 'required': False
60 },
61 'domain': {
62 'parameterType': 'query',
63 'required': True
64 },
65 'scope': {
66 'parameterType': 'query',
67 'required': True
68 },
Joe Gregorio67d77772010-09-01 16:45:45 -040069 },
Joe Gregorio7943c5d2010-09-08 16:11:43 -040070 },
Joe Gregorio67d77772010-09-01 16:45:45 -040071 'access': {
72 'url': 'https://www.google.com/accounts/OAuthGetAccessToken',
Joe Gregorio7943c5d2010-09-08 16:11:43 -040073 'parameters': {
74 'domain': {
75 'parameterType': 'query',
76 'required': True
77 },
78 'scope': {
79 'parameterType': 'query',
80 'required': True
81 },
Joe Gregorio67d77772010-09-01 16:45:45 -040082 },
Joe Gregorio7943c5d2010-09-08 16:11:43 -040083 },
Joe Gregorio67d77772010-09-01 16:45:45 -040084 }
85
Joe Gregorio845a5452010-09-08 13:50:34 -040086
Joe Gregorio67d77772010-09-01 16:45:45 -040087def _oauth_uri(name, discovery, params):
Joe Gregorio845a5452010-09-08 13:50:34 -040088 """Look up the OAuth UR from the discovery
89 document and add query parameters based on
90 params.
91
92 name - The name of the OAuth URI to lookup, one
93 of 'request', 'access', or 'authorize'.
94 discovery - Portion of discovery document the describes
95 the OAuth endpoints.
96 params - Dictionary that is used to form the query parameters
97 for the specified URI.
98 """
Joe Gregorio67d77772010-09-01 16:45:45 -040099 if name not in ['request', 'access', 'authorize']:
100 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400101 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -0400102 query = {}
103 for key in keys:
104 if key in params:
105 query[key] = params[key]
106 return discovery[name]['url'] + '?' + urllib.urlencode(query)
107
Joe Gregorio845a5452010-09-08 13:50:34 -0400108
109class Credentials(object):
110 """Base class for all Credentials objects.
111
112 Subclasses must define an authorize() method
113 that applies the credentials to an HTTP transport.
114 """
115
116 def authorize(self, http):
117 """Take an httplib2.Http instance (or equivalent) and
118 authorizes it for the set of credentials, usually by
119 replacing http.request() with a method that adds in
120 the appropriate headers and then delegates to the original
121 Http.request() method.
122 """
123 _abstract()
124
125
126class 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
140
141 def authorize(self, http):
142 """
143 Args:
144 http - An instance of httplib2.Http
145 or something that acts like it.
146
147 Returns:
148 A modified instance of http that was passed in.
149
150 Example:
151
152 h = httplib2.Http()
153 h = credentials.authorize(h)
154
155 You can't create a new OAuth
156 subclass of httplib2.Authenication because
157 it never gets passed the absolute URI, which is
158 needed for signing. So instead we have to overload
159 'request' with a closure that adds in the
160 Authorization header and then calls the original version
161 of 'request()'.
162 """
163 request_orig = http.request
164 signer = oauth.SignatureMethod_HMAC_SHA1()
165
166 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400167 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400168 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
169 connection_type=None):
170 """Modify the request headers to add the appropriate
171 Authorization header."""
172 req = oauth.Request.from_consumer_and_token(
173 self.consumer, self.token, http_method=method, http_url=uri)
174 req.sign_request(signer, self.consumer, self.token)
175 if headers == None:
176 headers = {}
177 headers.update(req.to_header())
178 if 'user-agent' not in headers:
179 headers['user-agent'] = self.user_agent
180 return request_orig(uri, method, body, headers,
181 redirections, connection_type)
182
183 http.request = new_request
184 return http
185
186
187class FlowThreeLegged(object):
188 """Does the Three Legged Dance for OAuth 1.0a.
189 """
190
Joe Gregorio67d77772010-09-01 16:45:45 -0400191 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
192 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400193 """
194 discovery - Section of the API discovery document that describes
195 the OAuth endpoints.
196 consumer_key - OAuth consumer key
197 consumer_secret - OAuth consumer secret
198 user_agent - The HTTP User-Agent that identifies the application.
199 **kwargs - The keyword arguments are all optional and required
200 parameters for the OAuth calls.
201 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400202 self.discovery = discovery
203 self.consumer_key = consumer_key
204 self.consumer_secret = consumer_secret
205 self.user_agent = user_agent
206 self.params = kwargs
207 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400208 required = {}
209 for uriinfo in discovery.itervalues():
210 for name, value in uriinfo['parameters'].iteritems():
211 if value['required'] and not name.startswith('oauth_'):
212 required[name] = 1
213 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400214 if key not in self.params:
215 raise MissingParameter('Required parameter %s not supplied' % key)
216
Joe Gregorio845a5452010-09-08 13:50:34 -0400217 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400218 """Returns a URI to redirect to the provider.
219
Joe Gregorio845a5452010-09-08 13:50:34 -0400220 oauth_callback - Either the string 'oob' for a non-web-based application,
221 or a URI that handles the callback from the authorization
222 server.
223
224 If oauth_callback is 'oob' then pass in the
225 generated verification code to step2_exchange,
226 otherwise pass in the query parameters received
227 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400228 """
229 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
230 client = oauth.Client(consumer)
231
232 headers = {
233 'user-agent': self.user_agent,
234 'content-type': 'application/x-www-form-urlencoded'
235 }
236 body = urllib.urlencode({'oauth_callback': oauth_callback})
237 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400238
Joe Gregorio67d77772010-09-01 16:45:45 -0400239 resp, content = client.request(uri, 'POST', headers=headers,
240 body=body)
241 if resp['status'] != '200':
Joe Gregorio845a5452010-09-08 13:50:34 -0400242 logging.error('Failed to retrieve temporary authorization: %s' % content)
Joe Gregorio67d77772010-09-01 16:45:45 -0400243 raise Exception('Invalid response %s.' % resp['status'])
244
245 self.request_token = dict(parse_qsl(content))
246
247 auth_params = copy.copy(self.params)
248 auth_params['oauth_token'] = self.request_token['oauth_token']
249
Joe Gregorio845a5452010-09-08 13:50:34 -0400250 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400251
Joe Gregorio845a5452010-09-08 13:50:34 -0400252 def step2_exchange(self, verifier):
253 """Exhanges an authorized request token
254 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400255
Joe Gregorio845a5452010-09-08 13:50:34 -0400256 verifier - either the verifier token, or a dictionary
257 of the query parameters to the callback, which contains
258 the oauth_verifier.
259 """
260
261 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
262 verifier = verifier['oauth_verifier']
263
264 token = oauth.Token(
265 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400266 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400267 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400268 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
269 client = oauth.Client(consumer, token)
270
271 headers = {
272 'user-agent': self.user_agent,
273 'content-type': 'application/x-www-form-urlencoded'
274 }
275
276 uri = _oauth_uri('access', self.discovery, self.params)
277 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400278 if resp['status'] != '200':
279 logging.error('Failed to retrieve access token: %s' % content)
280 raise Exception('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400281
Joe Gregorio845a5452010-09-08 13:50:34 -0400282 oauth_params = dict(parse_qsl(content))
283 token = oauth.Token(
284 oauth_params['oauth_token'],
285 oauth_params['oauth_token_secret'])
286
287 return OAuthCredentials(consumer, token, self.user_agent)