4 Copyright (c) 2007 Leah Culver
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
13 The above copyright notice and this permission notice shall be included in
14 all copies or substantial portions of the Software.
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34 VERSION = '1.0' # Hi Blaine!
36 SIGNATURE_METHOD = 'PLAINTEXT'
39 class OAuthError(RuntimeError):
40 """Generic exception class."""
41 def __init__(self, message='OAuth error occured.'):
42 self.message = message
44 def build_authenticate_header(realm=''):
45 """Optional WWW-Authenticate header (401 error)"""
46 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
49 """Escape a URL including any /."""
50 return urllib.quote(s, safe='~')
53 """Convert unicode to utf-8."""
54 if isinstance(s, unicode):
55 return s.encode("utf-8")
59 def generate_timestamp():
60 """Get seconds since epoch (UTC)."""
61 return int(time.time())
63 def generate_nonce(length=8):
64 """Generate pseudorandom number."""
65 return ''.join([str(random.randint(0, 9)) for i in range(length)])
68 class OAuthConsumer(object):
69 """Consumer of OAuth authentication.
71 OAuthConsumer is a data type that represents the identity of the Consumer
72 via its shared secret with the Service Provider.
78 def __init__(self, key, secret):
83 class OAuthToken(object):
84 """OAuthToken is a data type that represents an End User via either an access
88 secret -- the token secret
94 def __init__(self, key, secret):
99 return urllib.urlencode({'oauth_token': self.key,
100 'oauth_token_secret': self.secret})
103 """ Returns a token from something like:
104 oauth_token_secret=xxx&oauth_token=xxx
106 params = cgi.parse_qs(s, keep_blank_values=False)
107 key = params['oauth_token'][0]
108 secret = params['oauth_token_secret'][0]
109 return OAuthToken(key, secret)
110 from_string = staticmethod(from_string)
113 return self.to_string()
116 class OAuthRequest(object):
117 """OAuthRequest represents the request and can be serialized.
122 - oauth_signature_method
127 ... any additional parameters, as defined by the Service Provider.
129 parameters = None # OAuth parameters.
130 http_method = HTTP_METHOD
134 def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
135 self.http_method = http_method
136 self.http_url = http_url
137 self.parameters = parameters or {}
139 def set_parameter(self, parameter, value):
140 self.parameters[parameter] = value
142 def get_parameter(self, parameter):
144 return self.parameters[parameter]
146 raise OAuthError('Parameter not found: %s' % parameter)
148 def _get_timestamp_nonce(self):
149 return self.get_parameter('oauth_timestamp'), self.get_parameter(
152 def get_nonoauth_parameters(self):
153 """Get any non-OAuth parameters."""
155 for k, v in self.parameters.iteritems():
156 # Ignore oauth parameters.
157 if k.find('oauth_') < 0:
161 def to_header(self, realm=''):
162 """Serialize as a header for an HTTPAuth request."""
163 auth_header = 'OAuth realm="%s"' % realm
164 # Add the oauth parameters.
166 for k, v in self.parameters.iteritems():
167 if k[:6] == 'oauth_':
168 auth_header += ', %s="%s"' % (k, escape(str(v)))
169 return {'Authorization': auth_header}
171 def to_postdata(self):
172 """Serialize as post data for a POST request."""
173 return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
174 for k, v in self.parameters.iteritems()])
177 """Serialize as a URL for a GET request."""
178 return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
180 def get_normalized_parameters(self):
181 """Return a string that contains the parameters that must be signed."""
182 params = self.parameters
184 # Exclude the signature if it exists.
185 del params['oauth_signature']
188 # Escape key values before sorting.
189 key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
190 for k,v in params.items()]
191 # Sort lexicographically, first after key, then after value.
193 # Combine key value pairs into a string.
194 return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
196 def get_normalized_http_method(self):
197 """Uppercases the http method."""
198 return self.http_method.upper()
200 def get_normalized_http_url(self):
201 """Parses the URL and rebuilds it to be scheme://host/path."""
202 parts = urlparse.urlparse(self.http_url)
203 scheme, netloc, path = parts[:3]
204 # Exclude default port numbers.
205 if scheme == 'http' and netloc[-3:] == ':80':
207 elif scheme == 'https' and netloc[-4:] == ':443':
209 return '%s://%s%s' % (scheme, netloc, path)
211 def sign_request(self, signature_method, consumer, token):
212 """Set the signature parameter to the result of build_signature."""
213 # Set the signature method.
214 self.set_parameter('oauth_signature_method',
215 signature_method.get_name())
217 self.set_parameter('oauth_signature',
218 self.build_signature(signature_method, consumer, token))
220 def build_signature(self, signature_method, consumer, token):
221 """Calls the build signature method within the signature method."""
222 return signature_method.build_signature(self, consumer, token)
224 def from_request(http_method, http_url, headers=None, parameters=None,
226 """Combines multiple parameter sources."""
227 if parameters is None:
231 if headers and 'Authorization' in headers:
232 auth_header = headers['Authorization']
233 # Check that the authorization header is OAuth.
234 if auth_header.index('OAuth') > -1:
235 auth_header = auth_header.lstrip('OAuth ')
237 # Get the parameters from the header.
238 header_params = OAuthRequest._split_header(auth_header)
239 parameters.update(header_params)
241 raise OAuthError('Unable to parse OAuth parameters from '
242 'Authorization header.')
244 # GET or POST query string.
246 query_params = OAuthRequest._split_url_string(query_string)
247 parameters.update(query_params)
250 param_str = urlparse.urlparse(http_url)[4] # query
251 url_params = OAuthRequest._split_url_string(param_str)
252 parameters.update(url_params)
255 return OAuthRequest(http_method, http_url, parameters)
258 from_request = staticmethod(from_request)
260 def from_consumer_and_token(oauth_consumer, token=None,
261 http_method=HTTP_METHOD, http_url=None, parameters=None):
266 'oauth_consumer_key': oauth_consumer.key,
267 'oauth_timestamp': generate_timestamp(),
268 'oauth_nonce': generate_nonce(),
269 'oauth_version': OAuthRequest.version,
272 defaults.update(parameters)
273 parameters = defaults
276 parameters['oauth_token'] = token.key
278 return OAuthRequest(http_method, http_url, parameters)
279 from_consumer_and_token = staticmethod(from_consumer_and_token)
281 def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
282 http_url=None, parameters=None):
286 parameters['oauth_token'] = token.key
289 parameters['oauth_callback'] = callback
291 return OAuthRequest(http_method, http_url, parameters)
292 from_token_and_callback = staticmethod(from_token_and_callback)
294 def _split_header(header):
295 """Turn Authorization: header into parameters."""
297 parts = header.split(',')
299 # Ignore realm parameter.
300 if param.find('realm') > -1:
303 param = param.strip()
305 param_parts = param.split('=', 1)
306 # Remove quotes and unescape the value.
307 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
309 _split_header = staticmethod(_split_header)
311 def _split_url_string(param_str):
312 """Turn URL string into parameters."""
313 parameters = cgi.parse_qs(param_str, keep_blank_values=False)
314 for k, v in parameters.iteritems():
315 parameters[k] = urllib.unquote(v[0])
317 _split_url_string = staticmethod(_split_url_string)
319 class OAuthServer(object):
320 """A worker to check the validity of a request against a data store."""
321 timestamp_threshold = 300 # In seconds, five minutes.
323 signature_methods = None
326 def __init__(self, data_store=None, signature_methods=None):
327 self.data_store = data_store
328 self.signature_methods = signature_methods or {}
330 def set_data_store(self, data_store):
331 self.data_store = data_store
333 def get_data_store(self):
334 return self.data_store
336 def add_signature_method(self, signature_method):
337 self.signature_methods[signature_method.get_name()] = signature_method
338 return self.signature_methods
340 def fetch_request_token(self, oauth_request):
341 """Processes a request_token request and returns the
342 request token on success.
345 # Get the request token for authorization.
346 token = self._get_token(oauth_request, 'request')
348 # No token required for the initial token request.
349 version = self._get_version(oauth_request)
350 consumer = self._get_consumer(oauth_request)
351 self._check_signature(oauth_request, consumer, None)
353 token = self.data_store.fetch_request_token(consumer)
356 def fetch_access_token(self, oauth_request):
357 """Processes an access_token request and returns the
358 access token on success.
360 version = self._get_version(oauth_request)
361 consumer = self._get_consumer(oauth_request)
362 # Get the request token.
363 token = self._get_token(oauth_request, 'request')
364 self._check_signature(oauth_request, consumer, token)
365 new_token = self.data_store.fetch_access_token(consumer, token)
368 def verify_request(self, oauth_request):
369 """Verifies an api call and checks all the parameters."""
370 # -> consumer and token
371 version = self._get_version(oauth_request)
372 consumer = self._get_consumer(oauth_request)
373 # Get the access token.
374 token = self._get_token(oauth_request, 'access')
375 self._check_signature(oauth_request, consumer, token)
376 parameters = oauth_request.get_nonoauth_parameters()
377 return consumer, token, parameters
379 def authorize_token(self, token, user):
380 """Authorize a request token."""
381 return self.data_store.authorize_request_token(token, user)
383 def get_callback(self, oauth_request):
384 """Get the callback URL."""
385 return oauth_request.get_parameter('oauth_callback')
387 def build_authenticate_header(self, realm=''):
388 """Optional support for the authenticate header."""
389 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
391 def _get_version(self, oauth_request):
392 """Verify the correct version request for this server."""
394 version = oauth_request.get_parameter('oauth_version')
397 if version and version != self.version:
398 raise OAuthError('OAuth version %s not supported.' % str(version))
401 def _get_signature_method(self, oauth_request):
402 """Figure out the signature with some defaults."""
404 signature_method = oauth_request.get_parameter(
405 'oauth_signature_method')
407 signature_method = SIGNATURE_METHOD
409 # Get the signature method object.
410 signature_method = self.signature_methods[signature_method]
412 signature_method_names = ', '.join(self.signature_methods.keys())
413 raise OAuthError('Signature method %s not supported try one of the '
414 'following: %s' % (signature_method, signature_method_names))
416 return signature_method
418 def _get_consumer(self, oauth_request):
419 consumer_key = oauth_request.get_parameter('oauth_consumer_key')
420 consumer = self.data_store.lookup_consumer(consumer_key)
422 raise OAuthError('Invalid consumer.')
425 def _get_token(self, oauth_request, token_type='access'):
426 """Try to find the token for the provided request token key."""
427 token_field = oauth_request.get_parameter('oauth_token')
428 token = self.data_store.lookup_token(token_type, token_field)
430 raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
433 def _check_signature(self, oauth_request, consumer, token):
434 timestamp, nonce = oauth_request._get_timestamp_nonce()
435 self._check_timestamp(timestamp)
436 self._check_nonce(consumer, token, nonce)
437 signature_method = self._get_signature_method(oauth_request)
439 signature = oauth_request.get_parameter('oauth_signature')
441 raise OAuthError('Missing signature.')
442 # Validate the signature.
443 valid_sig = signature_method.check_signature(oauth_request, consumer,
446 key, base = signature_method.build_signature_base_string(
447 oauth_request, consumer, token)
448 raise OAuthError('Invalid signature. Expected signature base '
450 built = signature_method.build_signature(oauth_request, consumer, token)
452 def _check_timestamp(self, timestamp):
453 """Verify that timestamp is recentish."""
454 timestamp = int(timestamp)
455 now = int(time.time())
456 lapsed = now - timestamp
457 if lapsed > self.timestamp_threshold:
458 raise OAuthError('Expired timestamp: given %d and now %s has a '
459 'greater difference than threshold %d' %
460 (timestamp, now, self.timestamp_threshold))
462 def _check_nonce(self, consumer, token, nonce):
463 """Verify that the nonce is uniqueish."""
464 nonce = self.data_store.lookup_nonce(consumer, token, nonce)
466 raise OAuthError('Nonce already used: %s' % str(nonce))
469 class OAuthClient(object):
470 """OAuthClient is a worker to attempt to execute a request."""
474 def __init__(self, oauth_consumer, oauth_token):
475 self.consumer = oauth_consumer
476 self.token = oauth_token
478 def get_consumer(self):
484 def fetch_request_token(self, oauth_request):
486 raise NotImplementedError
488 def fetch_access_token(self, oauth_request):
490 raise NotImplementedError
492 def access_resource(self, oauth_request):
493 """-> Some protected resource."""
494 raise NotImplementedError
497 class OAuthDataStore(object):
498 """A database abstraction used to lookup consumers and tokens."""
500 def lookup_consumer(self, key):
501 """-> OAuthConsumer."""
502 raise NotImplementedError
504 def lookup_token(self, oauth_consumer, token_type, token_token):
506 raise NotImplementedError
508 def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
510 raise NotImplementedError
512 def fetch_request_token(self, oauth_consumer):
514 raise NotImplementedError
516 def fetch_access_token(self, oauth_consumer, oauth_token):
518 raise NotImplementedError
520 def authorize_request_token(self, oauth_token, user):
522 raise NotImplementedError
525 class OAuthSignatureMethod(object):
526 """A strategy class that implements a signature method."""
529 raise NotImplementedError
531 def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
532 """-> str key, str raw."""
533 raise NotImplementedError
535 def build_signature(self, oauth_request, oauth_consumer, oauth_token):
537 raise NotImplementedError
539 def check_signature(self, oauth_request, consumer, token, signature):
540 built = self.build_signature(oauth_request, consumer, token)
541 return built == signature
544 class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
549 def build_signature_base_string(self, oauth_request, consumer, token):
551 escape(oauth_request.get_normalized_http_method()),
552 escape(oauth_request.get_normalized_http_url()),
553 escape(oauth_request.get_normalized_parameters()),
556 key = '%s&' % escape(consumer.secret)
558 key += escape(token.secret)
562 def build_signature(self, oauth_request, consumer, token):
563 """Builds the base signature string."""
564 key, raw = self.build_signature_base_string(oauth_request, consumer,
570 hashed = hmac.new(key, raw, hashlib.sha1)
572 import sha # Deprecated
573 hashed = hmac.new(key, raw, sha)
575 # Calculate the digest base 64.
576 return binascii.b2a_base64(hashed.digest())[:-1]
579 class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
584 def build_signature_base_string(self, oauth_request, consumer, token):
585 """Concatenates the consumer key and secret."""
586 sig = '%s&' % escape(consumer.secret)
588 sig = sig + escape(token.secret)
591 def build_signature(self, oauth_request, consumer, token):
592 key, raw = self.build_signature_base_string(oauth_request, consumer,