blob: 68e4300e4c57e1bd1654c1900219b04b5b3d8ab5 [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 Gregorio695fdc12011-01-16 16:46:55 -050011import datetime
Joe Gregorio845a5452010-09-08 13:50:34 -040012import httplib2
Joe Gregorio695fdc12011-01-16 16:46:55 -050013import logging
Joe Gregorio67d77772010-09-01 16:45:45 -040014import oauth2 as oauth
Joe Gregorio845a5452010-09-08 13:50:34 -040015import urllib
Joe Gregorio695fdc12011-01-16 16:46:55 -050016import urlparse
17
18from anyjson import simplejson
Joe Gregorio67d77772010-09-01 16:45:45 -040019
20try:
21 from urlparse import parse_qs, parse_qsl
22except ImportError:
23 from cgi import parse_qs, parse_qsl
Joe Gregorio845a5452010-09-08 13:50:34 -040024
25
Tom Miller05cd4f52010-10-06 11:09:12 -070026class Error(Exception):
27 """Base error for this module."""
28 pass
29
30
31class RequestError(Error):
32 """Error occurred during request."""
33 pass
34
35
36class MissingParameter(Error):
Joe Gregorio67d77772010-09-01 16:45:45 -040037 pass
38
Joe Gregorio845a5452010-09-08 13:50:34 -040039
40def _abstract():
Joe Gregorio7943c5d2010-09-08 16:11:43 -040041 raise NotImplementedError('You need to override this function')
Joe Gregorio67d77772010-09-01 16:45:45 -040042
43
Joe Gregorio67d77772010-09-01 16:45:45 -040044def _oauth_uri(name, discovery, params):
ade@google.coma9907a22010-12-11 02:44:37 +000045 """Look up the OAuth URI from the discovery
Joe Gregorio845a5452010-09-08 13:50:34 -040046 document and add query parameters based on
47 params.
48
49 name - The name of the OAuth URI to lookup, one
50 of 'request', 'access', or 'authorize'.
51 discovery - Portion of discovery document the describes
52 the OAuth endpoints.
53 params - Dictionary that is used to form the query parameters
54 for the specified URI.
55 """
Joe Gregorio67d77772010-09-01 16:45:45 -040056 if name not in ['request', 'access', 'authorize']:
57 raise KeyError(name)
Joe Gregorio7943c5d2010-09-08 16:11:43 -040058 keys = discovery[name]['parameters'].keys()
Joe Gregorio67d77772010-09-01 16:45:45 -040059 query = {}
60 for key in keys:
61 if key in params:
62 query[key] = params[key]
63 return discovery[name]['url'] + '?' + urllib.urlencode(query)
64
Joe Gregorio845a5452010-09-08 13:50:34 -040065
66class Credentials(object):
67 """Base class for all Credentials objects.
68
69 Subclasses must define an authorize() method
70 that applies the credentials to an HTTP transport.
71 """
72
73 def authorize(self, http):
74 """Take an httplib2.Http instance (or equivalent) and
75 authorizes it for the set of credentials, usually by
76 replacing http.request() with a method that adds in
77 the appropriate headers and then delegates to the original
78 Http.request() method.
79 """
80 _abstract()
81
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050082
Joe Gregorio695fdc12011-01-16 16:46:55 -050083class Flow(object):
84 """Base class for all Flow objects."""
85 pass
Joe Gregorio845a5452010-09-08 13:50:34 -040086
Joe Gregorio7a6df3a2011-01-31 21:55:21 -050087
Joe Gregorio845a5452010-09-08 13:50:34 -040088class OAuthCredentials(Credentials):
89 """Credentials object for OAuth 1.0a
90 """
91
92 def __init__(self, consumer, token, user_agent):
93 """
94 consumer - An instance of oauth.Consumer.
95 token - An instance of oauth.Token constructed with
96 the access token and secret.
97 user_agent - The HTTP User-Agent to provide for this application.
98 """
99 self.consumer = consumer
100 self.token = token
101 self.user_agent = user_agent
102
103 def authorize(self, http):
104 """
105 Args:
106 http - An instance of httplib2.Http
107 or something that acts like it.
108
109 Returns:
110 A modified instance of http that was passed in.
111
112 Example:
113
114 h = httplib2.Http()
115 h = credentials.authorize(h)
116
117 You can't create a new OAuth
118 subclass of httplib2.Authenication because
119 it never gets passed the absolute URI, which is
120 needed for signing. So instead we have to overload
121 'request' with a closure that adds in the
122 Authorization header and then calls the original version
123 of 'request()'.
124 """
125 request_orig = http.request
126 signer = oauth.SignatureMethod_HMAC_SHA1()
127
128 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400129 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400130 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
131 connection_type=None):
132 """Modify the request headers to add the appropriate
133 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500134 response_code = 302
135 http.follow_redirects = False
136 while response_code in [301, 302]:
137 req = oauth.Request.from_consumer_and_token(
138 self.consumer, self.token, http_method=method, http_url=uri)
139 req.sign_request(signer, self.consumer, self.token)
140 if headers == None:
141 headers = {}
142 headers.update(req.to_header())
143 if 'user-agent' in headers:
144 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
145 else:
146 headers['user-agent'] = self.user_agent
147 resp, content = request_orig(uri, method, body, headers,
148 redirections, connection_type)
149 response_code = resp.status
150 if response_code in [301, 302]:
151 uri = resp['location']
152 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400153
154 http.request = new_request
155 return http
156
157
Joe Gregorio695fdc12011-01-16 16:46:55 -0500158class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400159 """Does the Three Legged Dance for OAuth 1.0a.
160 """
161
Joe Gregorio67d77772010-09-01 16:45:45 -0400162 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
163 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400164 """
165 discovery - Section of the API discovery document that describes
166 the OAuth endpoints.
167 consumer_key - OAuth consumer key
168 consumer_secret - OAuth consumer secret
169 user_agent - The HTTP User-Agent that identifies the application.
170 **kwargs - The keyword arguments are all optional and required
171 parameters for the OAuth calls.
172 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400173 self.discovery = discovery
174 self.consumer_key = consumer_key
175 self.consumer_secret = consumer_secret
176 self.user_agent = user_agent
177 self.params = kwargs
178 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400179 required = {}
180 for uriinfo in discovery.itervalues():
181 for name, value in uriinfo['parameters'].iteritems():
182 if value['required'] and not name.startswith('oauth_'):
183 required[name] = 1
184 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400185 if key not in self.params:
186 raise MissingParameter('Required parameter %s not supplied' % key)
187
Joe Gregorio845a5452010-09-08 13:50:34 -0400188 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400189 """Returns a URI to redirect to the provider.
190
Joe Gregorio845a5452010-09-08 13:50:34 -0400191 oauth_callback - Either the string 'oob' for a non-web-based application,
192 or a URI that handles the callback from the authorization
193 server.
194
195 If oauth_callback is 'oob' then pass in the
196 generated verification code to step2_exchange,
197 otherwise pass in the query parameters received
198 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400199 """
200 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
201 client = oauth.Client(consumer)
202
203 headers = {
204 'user-agent': self.user_agent,
205 'content-type': 'application/x-www-form-urlencoded'
206 }
207 body = urllib.urlencode({'oauth_callback': oauth_callback})
208 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400209
Joe Gregorio67d77772010-09-01 16:45:45 -0400210 resp, content = client.request(uri, 'POST', headers=headers,
211 body=body)
212 if resp['status'] != '200':
Joe Gregorio845a5452010-09-08 13:50:34 -0400213 logging.error('Failed to retrieve temporary authorization: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700214 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400215
216 self.request_token = dict(parse_qsl(content))
217
218 auth_params = copy.copy(self.params)
219 auth_params['oauth_token'] = self.request_token['oauth_token']
220
Joe Gregorio845a5452010-09-08 13:50:34 -0400221 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400222
Joe Gregorio845a5452010-09-08 13:50:34 -0400223 def step2_exchange(self, verifier):
224 """Exhanges an authorized request token
225 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400226
Joe Gregorio845a5452010-09-08 13:50:34 -0400227 verifier - either the verifier token, or a dictionary
228 of the query parameters to the callback, which contains
229 the oauth_verifier.
230 """
231
232 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
233 verifier = verifier['oauth_verifier']
234
235 token = oauth.Token(
236 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400237 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400238 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400239 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
240 client = oauth.Client(consumer, token)
241
242 headers = {
243 'user-agent': self.user_agent,
244 'content-type': 'application/x-www-form-urlencoded'
245 }
246
247 uri = _oauth_uri('access', self.discovery, self.params)
248 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400249 if resp['status'] != '200':
250 logging.error('Failed to retrieve access token: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700251 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400252
Joe Gregorio845a5452010-09-08 13:50:34 -0400253 oauth_params = dict(parse_qsl(content))
254 token = oauth.Token(
255 oauth_params['oauth_token'],
256 oauth_params['oauth_token_secret'])
257
258 return OAuthCredentials(consumer, token, self.user_agent)