blob: 469b6e0e8f403998e8944ef00def5db822ba467f [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 Gregorio695fdc12011-01-16 16:46:55 -050082class Flow(object):
83 """Base class for all Flow objects."""
84 pass
Joe Gregorio845a5452010-09-08 13:50:34 -040085
86class OAuthCredentials(Credentials):
87 """Credentials object for OAuth 1.0a
88 """
89
90 def __init__(self, consumer, token, user_agent):
91 """
92 consumer - An instance of oauth.Consumer.
93 token - An instance of oauth.Token constructed with
94 the access token and secret.
95 user_agent - The HTTP User-Agent to provide for this application.
96 """
97 self.consumer = consumer
98 self.token = token
99 self.user_agent = user_agent
100
101 def authorize(self, http):
102 """
103 Args:
104 http - An instance of httplib2.Http
105 or something that acts like it.
106
107 Returns:
108 A modified instance of http that was passed in.
109
110 Example:
111
112 h = httplib2.Http()
113 h = credentials.authorize(h)
114
115 You can't create a new OAuth
116 subclass of httplib2.Authenication because
117 it never gets passed the absolute URI, which is
118 needed for signing. So instead we have to overload
119 'request' with a closure that adds in the
120 Authorization header and then calls the original version
121 of 'request()'.
122 """
123 request_orig = http.request
124 signer = oauth.SignatureMethod_HMAC_SHA1()
125
126 # The closure that will replace 'httplib2.Http.request'.
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400127 def new_request(uri, method='GET', body=None, headers=None,
Joe Gregorio845a5452010-09-08 13:50:34 -0400128 redirections=httplib2.DEFAULT_MAX_REDIRECTS,
129 connection_type=None):
130 """Modify the request headers to add the appropriate
131 Authorization header."""
Joe Gregorio48c1caa2011-01-05 11:22:47 -0500132 response_code = 302
133 http.follow_redirects = False
134 while response_code in [301, 302]:
135 req = oauth.Request.from_consumer_and_token(
136 self.consumer, self.token, http_method=method, http_url=uri)
137 req.sign_request(signer, self.consumer, self.token)
138 if headers == None:
139 headers = {}
140 headers.update(req.to_header())
141 if 'user-agent' in headers:
142 headers['user-agent'] = self.user_agent + ' ' + headers['user-agent']
143 else:
144 headers['user-agent'] = self.user_agent
145 resp, content = request_orig(uri, method, body, headers,
146 redirections, connection_type)
147 response_code = resp.status
148 if response_code in [301, 302]:
149 uri = resp['location']
150 return resp, content
Joe Gregorio845a5452010-09-08 13:50:34 -0400151
152 http.request = new_request
153 return http
154
155
Joe Gregorio695fdc12011-01-16 16:46:55 -0500156class FlowThreeLegged(Flow):
Joe Gregorio845a5452010-09-08 13:50:34 -0400157 """Does the Three Legged Dance for OAuth 1.0a.
158 """
159
Joe Gregorio67d77772010-09-01 16:45:45 -0400160 def __init__(self, discovery, consumer_key, consumer_secret, user_agent,
161 **kwargs):
Joe Gregorio845a5452010-09-08 13:50:34 -0400162 """
163 discovery - Section of the API discovery document that describes
164 the OAuth endpoints.
165 consumer_key - OAuth consumer key
166 consumer_secret - OAuth consumer secret
167 user_agent - The HTTP User-Agent that identifies the application.
168 **kwargs - The keyword arguments are all optional and required
169 parameters for the OAuth calls.
170 """
Joe Gregorio67d77772010-09-01 16:45:45 -0400171 self.discovery = discovery
172 self.consumer_key = consumer_key
173 self.consumer_secret = consumer_secret
174 self.user_agent = user_agent
175 self.params = kwargs
176 self.request_token = {}
Joe Gregorio7943c5d2010-09-08 16:11:43 -0400177 required = {}
178 for uriinfo in discovery.itervalues():
179 for name, value in uriinfo['parameters'].iteritems():
180 if value['required'] and not name.startswith('oauth_'):
181 required[name] = 1
182 for key in required.iterkeys():
Joe Gregorio67d77772010-09-01 16:45:45 -0400183 if key not in self.params:
184 raise MissingParameter('Required parameter %s not supplied' % key)
185
Joe Gregorio845a5452010-09-08 13:50:34 -0400186 def step1_get_authorize_url(self, oauth_callback='oob'):
Joe Gregorio67d77772010-09-01 16:45:45 -0400187 """Returns a URI to redirect to the provider.
188
Joe Gregorio845a5452010-09-08 13:50:34 -0400189 oauth_callback - Either the string 'oob' for a non-web-based application,
190 or a URI that handles the callback from the authorization
191 server.
192
193 If oauth_callback is 'oob' then pass in the
194 generated verification code to step2_exchange,
195 otherwise pass in the query parameters received
196 at the callback uri to step2_exchange.
Joe Gregorio67d77772010-09-01 16:45:45 -0400197 """
198 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
199 client = oauth.Client(consumer)
200
201 headers = {
202 'user-agent': self.user_agent,
203 'content-type': 'application/x-www-form-urlencoded'
204 }
205 body = urllib.urlencode({'oauth_callback': oauth_callback})
206 uri = _oauth_uri('request', self.discovery, self.params)
Joe Gregorio845a5452010-09-08 13:50:34 -0400207
Joe Gregorio67d77772010-09-01 16:45:45 -0400208 resp, content = client.request(uri, 'POST', headers=headers,
209 body=body)
210 if resp['status'] != '200':
Joe Gregorio845a5452010-09-08 13:50:34 -0400211 logging.error('Failed to retrieve temporary authorization: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700212 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400213
214 self.request_token = dict(parse_qsl(content))
215
216 auth_params = copy.copy(self.params)
217 auth_params['oauth_token'] = self.request_token['oauth_token']
218
Joe Gregorio845a5452010-09-08 13:50:34 -0400219 return _oauth_uri('authorize', self.discovery, auth_params)
Joe Gregorio67d77772010-09-01 16:45:45 -0400220
Joe Gregorio845a5452010-09-08 13:50:34 -0400221 def step2_exchange(self, verifier):
222 """Exhanges an authorized request token
223 for OAuthCredentials.
Joe Gregorio67d77772010-09-01 16:45:45 -0400224
Joe Gregorio845a5452010-09-08 13:50:34 -0400225 verifier - either the verifier token, or a dictionary
226 of the query parameters to the callback, which contains
227 the oauth_verifier.
228 """
229
230 if not (isinstance(verifier, str) or isinstance(verifier, unicode)):
231 verifier = verifier['oauth_verifier']
232
233 token = oauth.Token(
234 self.request_token['oauth_token'],
Joe Gregorio67d77772010-09-01 16:45:45 -0400235 self.request_token['oauth_token_secret'])
Joe Gregorio845a5452010-09-08 13:50:34 -0400236 token.set_verifier(verifier)
Joe Gregorio67d77772010-09-01 16:45:45 -0400237 consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
238 client = oauth.Client(consumer, token)
239
240 headers = {
241 'user-agent': self.user_agent,
242 'content-type': 'application/x-www-form-urlencoded'
243 }
244
245 uri = _oauth_uri('access', self.discovery, self.params)
246 resp, content = client.request(uri, 'POST', headers=headers)
Joe Gregorio845a5452010-09-08 13:50:34 -0400247 if resp['status'] != '200':
248 logging.error('Failed to retrieve access token: %s' % content)
Tom Miller05cd4f52010-10-06 11:09:12 -0700249 raise RequestError('Invalid response %s.' % resp['status'])
Joe Gregorio67d77772010-09-01 16:45:45 -0400250
Joe Gregorio845a5452010-09-08 13:50:34 -0400251 oauth_params = dict(parse_qsl(content))
252 token = oauth.Token(
253 oauth_params['oauth_token'],
254 oauth_params['oauth_token_secret'])
255
256 return OAuthCredentials(consumer, token, self.user_agent)
Joe Gregorio695fdc12011-01-16 16:46:55 -0500257