blob: 9cc6e6651f8aea3caaf82cdadb0eb02f38ba6d22 [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):
Joe Gregorio845a5452010-09-08 13:50:34 -040043 """Look up the OAuth UR from the discovery
44 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."""
127 req = oauth.Request.from_consumer_and_token(
128 self.consumer, self.token, http_method=method, http_url=uri)
129 req.sign_request(signer, self.consumer, self.token)
130 if headers == None:
131 headers = {}
132 headers.update(req.to_header())
Joe Gregorio46b0ff62010-10-09 22:13:12 -0400133 if 'user-agent' in headers:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500134 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
Joe Gregorio46b0ff62010-10-09 22:13:12 -0400135 else:
Joe Gregorioaf276d22010-12-09 14:26:58 -0500136 headers['user-agent'] = self.user_agent
Joe Gregorio845a5452010-09-08 13:50:34 -0400137 return request_orig(uri, method, body, headers,
138 redirections, connection_type)
139
140 http.request = new_request
141 return http
142
143
144class FlowThreeLegged(object):
145 """Does the Three Legged Dance for OAuth 1.0a.
146 """
147
Joe Gregorio67d77772010-09-01 16:45:45 -0400148 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
149 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400150 """
151 discovery - Section of the API discovery document that describes
152 the OAuth endpoints.
153 consumer_key - OAuth consumer key
154 consumer_secret - OAuth consumer secret
155 user_agent - The HTTP User-Agent that identifies the application.
156 **kwargs - The keyword arguments are all optional and required
157 parameters for the OAuth calls.
158 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400159 self.discovery = discovery
160 self.consumer_key = consumer_key
161 self.consumer_secret = consumer_secret
162 self.user_agent = user_agent
163 self.params = kwargs
164 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400165 required = {}
166 for uriinfo in discovery.itervalues():
167 for name, value in uriinfo['parameters'].iteritems():
168 if value['required'] and not name.startswith('oauth_'):
169 required[name] = 1
170 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400171 if key not in self.params:
172 raise MissingParameter('Required parameter %s not supplied' % key)
173
Joe Gregorio845a5452010-09-08 13:50:34 -0400174 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400175 """Returns a URI to redirect to the provider.
176
Joe Gregorio845a5452010-09-08 13:50:34 -0400177 oauth_callback - Either the string 'oob' for a non-web-based application,
178 or a URI that handles the callback from the authorization
179 server.
180
181 If oauth_callback is 'oob' then pass in the
182 generated verification code to step2_exchange,
183 otherwise pass in the query parameters received
184 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400185 """
186 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
187 client = oauth.Client(consumer)
188
189 headers = {
190 'user-agent': self.user_agent,
191 'content-type': 'application/x-www-form-urlencoded'
192 }
193 body = urllib.urlencode({'oauth_callback': oauth_callback})
194 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400195
Joe Gregorio67d77772010-09-01 16:45:45 -0400196 resp, content = client.request(uri, 'POST', headers=headers,
197 body=body)
198 if resp['status'] != '200':
Joe Gregorio845a5452010-09-08 13:50:34 -0400199 logging.error('Failed to retrieve temporary authorization: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700200 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400201
202 self.request_token = dict(parse_qsl(content))
203
204 auth_params = copy.copy(self.params)
205 auth_params['oauth_token'] = self.request_token['oauth_token']
206
Joe Gregorio845a5452010-09-08 13:50:34 -0400207 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400208
Joe Gregorio845a5452010-09-08 13:50:34 -0400209 def step2_exchange(self, verifier):
210 """Exhanges an authorized request token
211 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400212
Joe Gregorio845a5452010-09-08 13:50:34 -0400213 verifier - either the verifier token, or a dictionary
214 of the query parameters to the callback, which contains
215 the oauth_verifier.
216 """
217
218 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
219 verifier = verifier['oauth_verifier']
220
221 token = oauth.Token(
222 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400223 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400224 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400225 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
226 client = oauth.Client(consumer, token)
227
228 headers = {
229 'user-agent': self.user_agent,
230 'content-type': 'application/x-www-form-urlencoded'
231 }
232
233 uri = _oauth_uri('access', self.discovery, self.params)
234 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400235 if resp['status'] != '200':
236 logging.error('Failed to retrieve access token: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700237 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400238
Joe Gregorio845a5452010-09-08 13:50:34 -0400239 oauth_params = dict(parse_qsl(content))
240 token = oauth.Token(
241 oauth_params['oauth_token'],
242 oauth_params['oauth_token_secret'])
243
244 return OAuthCredentials(consumer, token, self.user_agent)