blob: fe78e6182e825d3fc77e0d9bf381c17717f2a795 [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
Tom Miller05cd4f52010-10-06 11:09:12 -070024class Error(Exception):
25 """Base error for this module."""
26 pass
27
28
29class RequestError(Error):
30 """Error occurred during request."""
31 pass
32
33
34class MissingParameter(Error):
Joe Gregorio67d77772010-09-01 16:45:45 -040035 pass
36
Joe Gregorio845a5452010-09-08 13:50:34 -040037
38def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040039 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040040
41
Joe Gregorio67d77772010-09-01 16:45:45 -040042def _oauth_uri(name, discovery, params):
ade@google.coma9907a22010-12-11 02:44:37 +000043 """Look up the OAuth URI from the discovery
Joe Gregorio845a5452010-09-08 13:50:34 -040044 document and add query parameters based on
45 params.
46
47 name - The name of the OAuth URI to lookup, one
48 of 'request', 'access', or 'authorize'.
49 discovery - Portion of discovery document the describes
50 the OAuth endpoints.
51 params - Dictionary that is used to form the query parameters
52 for the specified URI.
53 """
Joe Gregorio67d77772010-09-01 16:45:45 -040054 if name not in ['request', 'access', 'authorize']:
55 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040056 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040057 query = {}
58 for key in keys:
59 if key in params:
60 query[key] = params[key]
61 return discovery[name]['url'] + '?' + urllib.urlencode(query)
62
Joe Gregorio845a5452010-09-08 13:50:34 -040063
64class Credentials(object):
65 """Base class for all Credentials objects.
66
67 Subclasses must define an authorize() method
68 that applies the credentials to an HTTP transport.
69 """
70
71 def authorize(self, http):
72 """Take an httplib2.Http instance (or equivalent) and
73 authorizes it for the set of credentials, usually by
74 replacing http.request() with a method that adds in
75 the appropriate headers and then delegates to the original
76 Http.request() method.
77 """
78 _abstract()
79
80
81class OAuthCredentials(Credentials):
82 """Credentials object for OAuth 1.0a
83 """
84
85 def __init__(self, consumer, token, user_agent):
86 """
87 consumer - An instance of oauth.Consumer.
88 token - An instance of oauth.Token constructed with
89 the access token and secret.
90 user_agent - The HTTP User-Agent to provide for this application.
91 """
92 self.consumer = consumer
93 self.token = token
94 self.user_agent = user_agent
95
96 def authorize(self, http):
97 """
98 Args:
99 http - An instance of httplib2.Http
100 or something that acts like it.
101
102 Returns:
103 A modified instance of http that was passed in.
104
105 Example:
106
107 h = httplib2.Http()
108 h = credentials.authorize(h)
109
110 You can't create a new OAuth
111 subclass of httplib2.Authenication because
112 it never gets passed the absolute URI, which is
113 needed for signing. So instead we have to overload
114 'request' with a closure that adds in the
115 Authorization header and then calls the original version
116 of 'request()'.
117 """
118 request_orig = http.request
119 signer = oauth.SignatureMethod_HMAC_SHA1()
120
121 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400122 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400123 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
124 connection_type=None):
125 """Modify the request headers to add the appropriate
126 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500127 response_code = 302
128 http.follow_redirects = False
129 while response_code in [301, 302]:
130 req = oauth.Request.from_consumer_and_token(
131 self.consumer, self.token, http_method=method, http_url=uri)
132 req.sign_request(signer, self.consumer, self.token)
133 if headers == None:
134 headers = {}
135 headers.update(req.to_header())
136 if 'user-agent' in headers:
137 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
138 else:
139 headers['user-agent'] = self.user_agent
140 resp, content = request_orig(uri, method, body, headers,
141 redirections, connection_type)
142 response_code = resp.status
143 if response_code in [301, 302]:
144 uri = resp['location']
145 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400146
147 http.request = new_request
148 return http
149
150
151class FlowThreeLegged(object):
152 """Does the Three Legged Dance for OAuth 1.0a.
153 """
154
Joe Gregorio67d77772010-09-01 16:45:45 -0400155 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
156 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400157 """
158 discovery - Section of the API discovery document that describes
159 the OAuth endpoints.
160 consumer_key - OAuth consumer key
161 consumer_secret - OAuth consumer secret
162 user_agent - The HTTP User-Agent that identifies the application.
163 **kwargs - The keyword arguments are all optional and required
164 parameters for the OAuth calls.
165 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400166 self.discovery = discovery
167 self.consumer_key = consumer_key
168 self.consumer_secret = consumer_secret
169 self.user_agent = user_agent
170 self.params = kwargs
171 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400172 required = {}
173 for uriinfo in discovery.itervalues():
174 for name, value in uriinfo['parameters'].iteritems():
175 if value['required'] and not name.startswith('oauth_'):
176 required[name] = 1
177 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400178 if key not in self.params:
179 raise MissingParameter('Required parameter %s not supplied' % key)
180
Joe Gregorio845a5452010-09-08 13:50:34 -0400181 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400182 """Returns a URI to redirect to the provider.
183
Joe Gregorio845a5452010-09-08 13:50:34 -0400184 oauth_callback - Either the string 'oob' for a non-web-based application,
185 or a URI that handles the callback from the authorization
186 server.
187
188 If oauth_callback is 'oob' then pass in the
189 generated verification code to step2_exchange,
190 otherwise pass in the query parameters received
191 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400192 """
193 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
194 client = oauth.Client(consumer)
195
196 headers = {
197 'user-agent': self.user_agent,
198 'content-type': 'application/x-www-form-urlencoded'
199 }
200 body = urllib.urlencode({'oauth_callback': oauth_callback})
201 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400202
Joe Gregorio67d77772010-09-01 16:45:45 -0400203 resp, content = client.request(uri, 'POST', headers=headers,
204 body=body)
205 if resp['status'] != '200':
Joe Gregorio845a5452010-09-08 13:50:34 -0400206 logging.error('Failed to retrieve temporary authorization: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700207 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400208
209 self.request_token = dict(parse_qsl(content))
210
211 auth_params = copy.copy(self.params)
212 auth_params['oauth_token'] = self.request_token['oauth_token']
213
Joe Gregorio845a5452010-09-08 13:50:34 -0400214 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400215
Joe Gregorio845a5452010-09-08 13:50:34 -0400216 def step2_exchange(self, verifier):
217 """Exhanges an authorized request token
218 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400219
Joe Gregorio845a5452010-09-08 13:50:34 -0400220 verifier - either the verifier token, or a dictionary
221 of the query parameters to the callback, which contains
222 the oauth_verifier.
223 """
224
225 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
226 verifier = verifier['oauth_verifier']
227
228 token = oauth.Token(
229 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400230 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400231 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400232 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
233 client = oauth.Client(consumer, token)
234
235 headers = {
236 'user-agent': self.user_agent,
237 'content-type': 'application/x-www-form-urlencoded'
238 }
239
240 uri = _oauth_uri('access', self.discovery, self.params)
241 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400242 if resp['status'] != '200':
243 logging.error('Failed to retrieve access token: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700244 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400245
Joe Gregorio845a5452010-09-08 13:50:34 -0400246 oauth_params = dict(parse_qsl(content))
247 token = oauth.Token(
248 oauth_params['oauth_token'],
249 oauth_params['oauth_token_secret'])
250
251 return OAuthCredentials(consumer, token, self.user_agent)