blob: de20336f1dbe5dd98abb4f427d2c0e0cb109283c [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:
134 headers['user-agent'] += ' '
135 else:
136 headers['user-agent'] = ''
137 headers['user-agent'] += self.user_agent
Joe Gregorio845a5452010-09-08 13:50:34 -0400138 return request_orig(uri, method, body, headers,
139 redirections, connection_type)
140
141 http.request = new_request
142 return http
143
144
145class FlowThreeLegged(object):
146 """Does the Three Legged Dance for OAuth 1.0a.
147 """
148
Joe Gregorio67d77772010-09-01 16:45:45 -0400149 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
150 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400151 """
152 discovery - Section of the API discovery document that describes
153 the OAuth endpoints.
154 consumer_key - OAuth consumer key
155 consumer_secret - OAuth consumer secret
156 user_agent - The HTTP User-Agent that identifies the application.
157 **kwargs - The keyword arguments are all optional and required
158 parameters for the OAuth calls.
159 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400160 self.discovery = discovery
161 self.consumer_key = consumer_key
162 self.consumer_secret = consumer_secret
163 self.user_agent = user_agent
164 self.params = kwargs
165 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400166 required = {}
167 for uriinfo in discovery.itervalues():
168 for name, value in uriinfo['parameters'].iteritems():
169 if value['required'] and not name.startswith('oauth_'):
170 required[name] = 1
171 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400172 if key not in self.params:
173 raise MissingParameter('Required parameter %s not supplied' % key)
174
Joe Gregorio845a5452010-09-08 13:50:34 -0400175 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400176 """Returns a URI to redirect to the provider.
177
Joe Gregorio845a5452010-09-08 13:50:34 -0400178 oauth_callback - Either the string 'oob' for a non-web-based application,
179 or a URI that handles the callback from the authorization
180 server.
181
182 If oauth_callback is 'oob' then pass in the
183 generated verification code to step2_exchange,
184 otherwise pass in the query parameters received
185 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400186 """
187 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
188 client = oauth.Client(consumer)
189
190 headers = {
191 'user-agent': self.user_agent,
192 'content-type': 'application/x-www-form-urlencoded'
193 }
194 body = urllib.urlencode({'oauth_callback': oauth_callback})
195 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400196
Joe Gregorio67d77772010-09-01 16:45:45 -0400197 resp, content = client.request(uri, 'POST', headers=headers,
198 body=body)
199 if resp['status'] != '200':
Joe Gregorio845a5452010-09-08 13:50:34 -0400200 logging.error('Failed to retrieve temporary authorization: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700201 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400202
203 self.request_token = dict(parse_qsl(content))
204
205 auth_params = copy.copy(self.params)
206 auth_params['oauth_token'] = self.request_token['oauth_token']
207
Joe Gregorio845a5452010-09-08 13:50:34 -0400208 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400209
Joe Gregorio845a5452010-09-08 13:50:34 -0400210 def step2_exchange(self, verifier):
211 """Exhanges an authorized request token
212 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400213
Joe Gregorio845a5452010-09-08 13:50:34 -0400214 verifier - either the verifier token, or a dictionary
215 of the query parameters to the callback, which contains
216 the oauth_verifier.
217 """
218
219 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
220 verifier = verifier['oauth_verifier']
221
222 token = oauth.Token(
223 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400224 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400225 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400226 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
227 client = oauth.Client(consumer, token)
228
229 headers = {
230 'user-agent': self.user_agent,
231 'content-type': 'application/x-www-form-urlencoded'
232 }
233
234 uri = _oauth_uri('access', self.discovery, self.params)
235 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400236 if resp['status'] != '200':
237 logging.error('Failed to retrieve access token: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700238 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400239
Joe Gregorio845a5452010-09-08 13:50:34 -0400240 oauth_params = dict(parse_qsl(content))
241 token = oauth.Token(
242 oauth_params['oauth_token'],
243 oauth_params['oauth_token_secret'])
244
245 return OAuthCredentials(consumer, token, self.user_agent)