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)])
67 def generate_verifier(length=8):
68 """Generate pseudorandom number."""
69 return ''.join([str(random.randint(0, 9)) for i in range(length)])
72 class OAuthConsumer(object):
73 """Consumer of OAuth authentication.
75 OAuthConsumer is a data type that represents the identity of the Consumer
76 via its shared secret with the Service Provider.
82 def __init__(self, key, secret):
87 class OAuthToken(object):
88 """OAuthToken is a data type that represents an End User via either an access
92 secret -- the token secret
98 callback_confirmed = None
101 def __init__(self, key, secret):
105 def set_callback(self, callback):
106 self.callback = callback
107 self.callback_confirmed = 'true'
109 def set_verifier(self, verifier=None):
110 if verifier is not None:
111 self.verifier = verifier
113 self.verifier = generate_verifier()
115 def get_callback_url(self):
116 if self.callback and self.verifier:
117 # Append the oauth_verifier.
118 parts = urlparse.urlparse(self.callback)
119 scheme, netloc, path, params, query, fragment = parts[:6]
121 query = '%s&oauth_verifier=%s' % (query, self.verifier)
123 query = 'oauth_verifier=%s' % self.verifier
124 return urlparse.urlunparse((scheme, netloc, path, params,
130 'oauth_token': self.key,
131 'oauth_token_secret': self.secret,
133 if self.callback_confirmed is not None:
134 data['oauth_callback_confirmed'] = self.callback_confirmed
135 return urllib.urlencode(data)
138 """ Returns a token from something like:
139 oauth_token_secret=xxx&oauth_token=xxx
141 params = cgi.parse_qs(s, keep_blank_values=False)
142 key = params['oauth_token'][0]
143 secret = params['oauth_token_secret'][0]
144 token = OAuthToken(key, secret)
146 token.callback_confirmed = params['oauth_callback_confirmed'][0]
148 pass # 1.0, no callback confirmed.
150 from_string = staticmethod(from_string)
153 return self.to_string()
156 class OAuthRequest(object):
157 """OAuthRequest represents the request and can be serialized.
162 - oauth_signature_method
168 ... any additional parameters, as defined by the Service Provider.
170 parameters = None # OAuth parameters.
171 http_method = HTTP_METHOD
175 def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
176 self.http_method = http_method
177 self.http_url = http_url
178 self.parameters = parameters or {}
180 def set_parameter(self, parameter, value):
181 self.parameters[parameter] = value
183 def get_parameter(self, parameter):
185 return self.parameters[parameter]
187 raise OAuthError('Parameter not found: %s' % parameter)
189 def _get_timestamp_nonce(self):
190 return self.get_parameter('oauth_timestamp'), self.get_parameter(
193 def get_nonoauth_parameters(self):
194 """Get any non-OAuth parameters."""
196 for k, v in self.parameters.iteritems():
197 # Ignore oauth parameters.
198 if k.find('oauth_') < 0:
202 def to_header(self, realm=''):
203 """Serialize as a header for an HTTPAuth request."""
204 auth_header = 'OAuth realm="%s"' % realm
205 # Add the oauth parameters.
207 for k, v in self.parameters.iteritems():
208 if k[:6] == 'oauth_':
209 auth_header += ', %s="%s"' % (k, escape(str(v)))
210 return {'Authorization': auth_header}
212 def to_postdata(self):
213 """Serialize as post data for a POST request."""
214 return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
215 for k, v in self.parameters.iteritems()])
218 """Serialize as a URL for a GET request."""
219 return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
221 def get_normalized_parameters(self):
222 """Return a string that contains the parameters that must be signed."""
223 params = self.parameters
225 # Exclude the signature if it exists.
226 del params['oauth_signature']
229 # Escape key values before sorting.
230 key_values = [(escape(_utf8_str(k)), escape(_utf8_str(v))) \
231 for k,v in params.items()]
232 # Sort lexicographically, first after key, then after value.
234 # Combine key value pairs into a string.
235 return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
237 def get_normalized_http_method(self):
238 """Uppercases the http method."""
239 return self.http_method.upper()
241 def get_normalized_http_url(self):
242 """Parses the URL and rebuilds it to be scheme://host/path."""
243 parts = urlparse.urlparse(self.http_url)
244 scheme, netloc, path = parts[:3]
245 # Exclude default port numbers.
246 if scheme == 'http' and netloc[-3:] == ':80':
248 elif scheme == 'https' and netloc[-4:] == ':443':
250 return '%s://%s%s' % (scheme, netloc, path)
252 def sign_request(self, signature_method, consumer, token):
253 """Set the signature parameter to the result of build_signature."""
254 # Set the signature method.
255 self.set_parameter('oauth_signature_method',
256 signature_method.get_name())
258 self.set_parameter('oauth_signature',
259 self.build_signature(signature_method, consumer, token))
261 def build_signature(self, signature_method, consumer, token):
262 """Calls the build signature method within the signature method."""
263 return signature_method.build_signature(self, consumer, token)
265 def from_request(http_method, http_url, headers=None, parameters=None,
267 """Combines multiple parameter sources."""
268 if parameters is None:
272 if headers and 'Authorization' in headers:
273 auth_header = headers['Authorization']
274 # Check that the authorization header is OAuth.
275 if auth_header[:6] == 'OAuth ':
276 auth_header = auth_header[6:]
278 # Get the parameters from the header.
279 header_params = OAuthRequest._split_header(auth_header)
280 parameters.update(header_params)
282 raise OAuthError('Unable to parse OAuth parameters from '
283 'Authorization header.')
285 # GET or POST query string.
287 query_params = OAuthRequest._split_url_string(query_string)
288 parameters.update(query_params)
291 param_str = urlparse.urlparse(http_url)[4] # query
292 url_params = OAuthRequest._split_url_string(param_str)
293 parameters.update(url_params)
296 return OAuthRequest(http_method, http_url, parameters)
299 from_request = staticmethod(from_request)
301 def from_consumer_and_token(oauth_consumer, token=None,
302 callback=None, verifier=None, http_method=HTTP_METHOD,
303 http_url=None, parameters=None):
308 'oauth_consumer_key': oauth_consumer.key,
309 'oauth_timestamp': generate_timestamp(),
310 'oauth_nonce': generate_nonce(),
311 'oauth_version': OAuthRequest.version,
314 defaults.update(parameters)
315 parameters = defaults
318 parameters['oauth_token'] = token.key
319 parameters['oauth_callback'] = token.callback
320 # 1.0a support for verifier.
321 parameters['oauth_verifier'] = verifier
323 # 1.0a support for callback in the request token request.
324 parameters['oauth_callback'] = callback
326 return OAuthRequest(http_method, http_url, parameters)
327 from_consumer_and_token = staticmethod(from_consumer_and_token)
329 def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
330 http_url=None, parameters=None):
334 parameters['oauth_token'] = token.key
337 parameters['oauth_callback'] = callback
339 return OAuthRequest(http_method, http_url, parameters)
340 from_token_and_callback = staticmethod(from_token_and_callback)
342 def _split_header(header):
343 """Turn Authorization: header into parameters."""
345 parts = header.split(',')
347 # Ignore realm parameter.
348 if param.find('realm') > -1:
351 param = param.strip()
353 param_parts = param.split('=', 1)
354 # Remove quotes and unescape the value.
355 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
357 _split_header = staticmethod(_split_header)
359 def _split_url_string(param_str):
360 """Turn URL string into parameters."""
361 parameters = cgi.parse_qs(param_str, keep_blank_values=False)
362 for k, v in parameters.iteritems():
363 parameters[k] = urllib.unquote(v[0])
365 _split_url_string = staticmethod(_split_url_string)
367 class OAuthServer(object):
368 """A worker to check the validity of a request against a data store."""
369 timestamp_threshold = 300 # In seconds, five minutes.
371 signature_methods = None
374 def __init__(self, data_store=None, signature_methods=None):
375 self.data_store = data_store
376 self.signature_methods = signature_methods or {}
378 def set_data_store(self, data_store):
379 self.data_store = data_store
381 def get_data_store(self):
382 return self.data_store
384 def add_signature_method(self, signature_method):
385 self.signature_methods[signature_method.get_name()] = signature_method
386 return self.signature_methods
388 def fetch_request_token(self, oauth_request):
389 """Processes a request_token request and returns the
390 request token on success.
393 # Get the request token for authorization.
394 token = self._get_token(oauth_request, 'request')
396 # No token required for the initial token request.
397 version = self._get_version(oauth_request)
398 consumer = self._get_consumer(oauth_request)
400 callback = self.get_callback(oauth_request)
402 callback = None # 1.0, no callback specified.
403 self._check_signature(oauth_request, consumer, None)
405 token = self.data_store.fetch_request_token(consumer, callback)
408 def fetch_access_token(self, oauth_request):
409 """Processes an access_token request and returns the
410 access token on success.
412 version = self._get_version(oauth_request)
413 consumer = self._get_consumer(oauth_request)
414 verifier = self._get_verifier(oauth_request)
415 # Get the request token.
416 token = self._get_token(oauth_request, 'request')
417 self._check_signature(oauth_request, consumer, token)
418 new_token = self.data_store.fetch_access_token(consumer, token, verifier)
421 def verify_request(self, oauth_request):
422 """Verifies an api call and checks all the parameters."""
423 # -> consumer and token
424 version = self._get_version(oauth_request)
425 consumer = self._get_consumer(oauth_request)
426 # Get the access token.
427 token = self._get_token(oauth_request, 'access')
428 self._check_signature(oauth_request, consumer, token)
429 parameters = oauth_request.get_nonoauth_parameters()
430 return consumer, token, parameters
432 def authorize_token(self, token, user):
433 """Authorize a request token."""
434 return self.data_store.authorize_request_token(token, user)
436 def get_callback(self, oauth_request):
437 """Get the callback URL."""
438 return oauth_request.get_parameter('oauth_callback')
440 def build_authenticate_header(self, realm=''):
441 """Optional support for the authenticate header."""
442 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
444 def _get_version(self, oauth_request):
445 """Verify the correct version request for this server."""
447 version = oauth_request.get_parameter('oauth_version')
450 if version and version != self.version:
451 raise OAuthError('OAuth version %s not supported.' % str(version))
454 def _get_signature_method(self, oauth_request):
455 """Figure out the signature with some defaults."""
457 signature_method = oauth_request.get_parameter(
458 'oauth_signature_method')
460 signature_method = SIGNATURE_METHOD
462 # Get the signature method object.
463 signature_method = self.signature_methods[signature_method]
465 signature_method_names = ', '.join(self.signature_methods.keys())
466 raise OAuthError('Signature method %s not supported try one of the '
467 'following: %s' % (signature_method, signature_method_names))
469 return signature_method
471 def _get_consumer(self, oauth_request):
472 consumer_key = oauth_request.get_parameter('oauth_consumer_key')
473 consumer = self.data_store.lookup_consumer(consumer_key)
475 raise OAuthError('Invalid consumer.')
478 def _get_token(self, oauth_request, token_type='access'):
479 """Try to find the token for the provided request token key."""
480 token_field = oauth_request.get_parameter('oauth_token')
481 token = self.data_store.lookup_token(token_type, token_field)
483 raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
486 def _get_verifier(self, oauth_request):
487 return oauth_request.get_parameter('oauth_verifier')
489 def _check_signature(self, oauth_request, consumer, token):
490 timestamp, nonce = oauth_request._get_timestamp_nonce()
491 self._check_timestamp(timestamp)
492 self._check_nonce(consumer, token, nonce)
493 signature_method = self._get_signature_method(oauth_request)
495 signature = oauth_request.get_parameter('oauth_signature')
497 raise OAuthError('Missing signature.')
498 # Validate the signature.
499 valid_sig = signature_method.check_signature(oauth_request, consumer,
502 key, base = signature_method.build_signature_base_string(
503 oauth_request, consumer, token)
504 raise OAuthError('Invalid signature. Expected signature base '
506 built = signature_method.build_signature(oauth_request, consumer, token)
508 def _check_timestamp(self, timestamp):
509 """Verify that timestamp is recentish."""
510 timestamp = int(timestamp)
511 now = int(time.time())
512 lapsed = now - timestamp
513 if lapsed > self.timestamp_threshold:
514 raise OAuthError('Expired timestamp: given %d and now %s has a '
515 'greater difference than threshold %d' %
516 (timestamp, now, self.timestamp_threshold))
518 def _check_nonce(self, consumer, token, nonce):
519 """Verify that the nonce is uniqueish."""
520 nonce = self.data_store.lookup_nonce(consumer, token, nonce)
522 raise OAuthError('Nonce already used: %s' % str(nonce))
525 class OAuthClient(object):
526 """OAuthClient is a worker to attempt to execute a request."""
530 def __init__(self, oauth_consumer, oauth_token):
531 self.consumer = oauth_consumer
532 self.token = oauth_token
534 def get_consumer(self):
540 def fetch_request_token(self, oauth_request):
542 raise NotImplementedError
544 def fetch_access_token(self, oauth_request):
546 raise NotImplementedError
548 def access_resource(self, oauth_request):
549 """-> Some protected resource."""
550 raise NotImplementedError
553 class OAuthDataStore(object):
554 """A database abstraction used to lookup consumers and tokens."""
556 def lookup_consumer(self, key):
557 """-> OAuthConsumer."""
558 raise NotImplementedError
560 def lookup_token(self, oauth_consumer, token_type, token_token):
562 raise NotImplementedError
564 def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
566 raise NotImplementedError
568 def fetch_request_token(self, oauth_consumer, oauth_callback):
570 raise NotImplementedError
572 def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
574 raise NotImplementedError
576 def authorize_request_token(self, oauth_token, user):
578 raise NotImplementedError
581 class OAuthSignatureMethod(object):
582 """A strategy class that implements a signature method."""
585 raise NotImplementedError
587 def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
588 """-> str key, str raw."""
589 raise NotImplementedError
591 def build_signature(self, oauth_request, oauth_consumer, oauth_token):
593 raise NotImplementedError
595 def check_signature(self, oauth_request, consumer, token, signature):
596 built = self.build_signature(oauth_request, consumer, token)
597 return built == signature
600 class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
605 def build_signature_base_string(self, oauth_request, consumer, token):
607 escape(oauth_request.get_normalized_http_method()),
608 escape(oauth_request.get_normalized_http_url()),
609 escape(oauth_request.get_normalized_parameters()),
612 key = '%s&' % escape(consumer.secret)
614 key += escape(token.secret)
618 def build_signature(self, oauth_request, consumer, token):
619 """Builds the base signature string."""
620 key, raw = self.build_signature_base_string(oauth_request, consumer,
626 hashed = hmac.new(key, raw, hashlib.sha1)
628 import sha # Deprecated
629 hashed = hmac.new(key, raw, sha)
631 # Calculate the digest base 64.
632 return binascii.b2a_base64(hashed.digest())[:-1]
635 class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
640 def build_signature_base_string(self, oauth_request, consumer, token):
641 """Concatenates the consumer key and secret."""
642 sig = '%s&' % escape(consumer.secret)
644 sig = sig + escape(token.secret)
647 def build_signature(self, oauth_request, consumer, token):
648 key, raw = self.build_signature_base_string(oauth_request, consumer,