00001 """
00002 The MIT License
00003
00004 Copyright (c) 2007 Leah Culver
00005
00006 Permission is hereby granted, free of charge, to any person obtaining a copy
00007 of this software and associated documentation files (the "Software"), to deal
00008 in the Software without restriction, including without limitation the rights
00009 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
00010 copies of the Software, and to permit persons to whom the Software is
00011 furnished to do so, subject to the following conditions:
00012
00013 The above copyright notice and this permission notice shall be included in
00014 all copies or substantial portions of the Software.
00015
00016 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
00017 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
00018 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
00019 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
00020 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
00021 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
00022 THE SOFTWARE.
00023 """
00024
00025 import cgi
00026 import urllib
00027 import time
00028 import random
00029 import urlparse
00030 import hmac
00031 import binascii
00032
00033
00034 VERSION = '1.0'
00035 HTTP_METHOD = 'GET'
00036 SIGNATURE_METHOD = 'PLAINTEXT'
00037
00038
00039 class OAuthError(RuntimeError):
00040 """Generic exception class."""
00041 def __init__(self, message='OAuth error occured.'):
00042 self.message = message
00043
00044 def build_authenticate_header(realm=''):
00045 """Optional WWW-Authenticate header (401 error)"""
00046 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
00047
00048 def escape(s):
00049 """Escape a URL including any /."""
00050 return urllib.quote(s, safe='~')
00051
00052 def _utf8_str(s):
00053 """Convert unicode to utf-8."""
00054 if isinstance(s, unicode):
00055 return s.encode("utf-8")
00056 else:
00057 return str(s)
00058
00059 def generate_timestamp():
00060 """Get seconds since epoch (UTC)."""
00061 return int(time.time())
00062
00063 def generate_nonce(length=8):
00064 """Generate pseudorandom number."""
00065 return ''.join([str(random.randint(0, 9)) for i in range(length)])
00066
00067 def generate_verifier(length=8):
00068 """Generate pseudorandom number."""
00069 return ''.join([str(random.randint(0, 9)) for i in range(length)])
00070
00071
00072 class OAuthConsumer(object):
00073 """Consumer of OAuth authentication.
00074
00075 OAuthConsumer is a data type that represents the identity of the Consumer
00076 via its shared secret with the Service Provider.
00077
00078 """
00079 key = None
00080 secret = None
00081
00082 def __init__(self, key, secret):
00083 self.key = key
00084 self.secret = secret
00085
00086
00087 class OAuthToken(object):
00088 """OAuthToken is a data type that represents an End User via either an access
00089 or request token.
00090
00091 key -- the token
00092 secret -- the token secret
00093
00094 """
00095 key = None
00096 secret = None
00097 callback = None
00098 callback_confirmed = None
00099 verifier = None
00100
00101 def __init__(self, key, secret):
00102 self.key = key
00103 self.secret = secret
00104
00105 def set_callback(self, callback):
00106 self.callback = callback
00107 self.callback_confirmed = 'true'
00108
00109 def set_verifier(self, verifier=None):
00110 if verifier is not None:
00111 self.verifier = verifier
00112 else:
00113 self.verifier = generate_verifier()
00114
00115 def get_callback_url(self):
00116 if self.callback and self.verifier:
00117
00118 parts = urlparse.urlparse(self.callback)
00119 scheme, netloc, path, params, query, fragment = parts[:6]
00120 if query:
00121 query = '%s&oauth_verifier=%s' % (query, self.verifier)
00122 else:
00123 query = 'oauth_verifier=%s' % self.verifier
00124 return urlparse.urlunparse((scheme, netloc, path, params,
00125 query, fragment))
00126 return self.callback
00127
00128 def to_string(self):
00129 data = {
00130 'oauth_token': self.key,
00131 'oauth_token_secret': self.secret,
00132 }
00133 if self.callback_confirmed is not None:
00134 data['oauth_callback_confirmed'] = self.callback_confirmed
00135 return urllib.urlencode(data)
00136
00137 def from_string(s):
00138 """ Returns a token from something like:
00139 oauth_token_secret=xxx&oauth_token=xxx
00140 """
00141 params = cgi.parse_qs(s, keep_blank_values=False)
00142 key = params['oauth_token'][0]
00143 secret = params['oauth_token_secret'][0]
00144 token = OAuthToken(key, secret)
00145 try:
00146 token.callback_confirmed = params['oauth_callback_confirmed'][0]
00147 except KeyError:
00148 pass
00149 return token
00150 from_string = staticmethod(from_string)
00151
00152 def __str__(self):
00153 return self.to_string()
00154
00155
00156 class OAuthRequest(object):
00157 """OAuthRequest represents the request and can be serialized.
00158
00159 OAuth parameters:
00160 - oauth_consumer_key
00161 - oauth_token
00162 - oauth_signature_method
00163 - oauth_signature
00164 - oauth_timestamp
00165 - oauth_nonce
00166 - oauth_version
00167 - oauth_verifier
00168 ... any additional parameters, as defined by the Service Provider.
00169 """
00170 parameters = None
00171 http_method = HTTP_METHOD
00172 http_url = None
00173 version = VERSION
00174
00175 def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
00176 self.http_method = http_method
00177 self.http_url = http_url
00178 self.parameters = parameters or {}
00179
00180 def set_parameter(self, parameter, value):
00181 self.parameters[parameter] = value
00182
00183 def get_parameter(self, parameter):
00184 try:
00185 return self.parameters[parameter]
00186 except:
00187 raise OAuthError('Parameter not found: %s' % parameter)
00188
00189 def _get_timestamp_nonce(self):
00190 return self.get_parameter('oauth_timestamp'), self.get_parameter(
00191 'oauth_nonce')
00192
00193 def get_nonoauth_parameters(self):
00194 """Get any non-OAuth parameters."""
00195 parameters = {}
00196 for k, v in self.parameters.iteritems():
00197
00198 if k.find('oauth_') < 0:
00199 parameters[k] = v
00200 return parameters
00201
00202 def to_header(self, realm=''):
00203 """Serialize as a header for an HTTPAuth request."""
00204 auth_header = 'OAuth realm="%s"' % realm
00205
00206 if self.parameters:
00207 for k, v in self.parameters.iteritems():
00208 if k[:6] == 'oauth_':
00209 auth_header += ', %s="%s"' % (k, escape(str(v)))
00210 return {'Authorization': auth_header}
00211
00212 def to_postdata(self):
00213 """Serialize as post data for a POST request."""
00214 return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) \
00215 for k, v in self.parameters.iteritems()])
00216
00217 def to_url(self):
00218 """Serialize as a URL for a GET request."""
00219 return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata())
00220
00221 def get_normalized_parameters(self):
00222 """Return a string that contains the parameters that must be signed."""
00223 params = self.parameters
00224 try:
00225
00226 del params['oauth_signature']
00227 except:
00228 pass
00229
00230 key_values = [(_utf8_str(k), _utf8_str(v)) \
00231 for k,v in params.items()]
00232
00233 key_values.sort()
00234
00235 return '&'.join(['%s=%s' % (k, v) for k, v in key_values])
00236
00237 def get_normalized_http_method(self):
00238 """Uppercases the http method."""
00239 return self.http_method.upper()
00240
00241 def get_normalized_http_url(self):
00242 """Parses the URL and rebuilds it to be scheme://host/path."""
00243 parts = urlparse.urlparse(self.http_url)
00244 scheme, netloc, path = parts[:3]
00245
00246 if scheme == 'http' and netloc[-3:] == ':80':
00247 netloc = netloc[:-3]
00248 elif scheme == 'https' and netloc[-4:] == ':443':
00249 netloc = netloc[:-4]
00250 return '%s://%s%s' % (scheme, netloc, path)
00251
00252 def sign_request(self, signature_method, consumer, token):
00253 """Set the signature parameter to the result of build_signature."""
00254
00255 self.set_parameter('oauth_signature_method',
00256 signature_method.get_name())
00257
00258 self.set_parameter('oauth_signature',
00259 self.build_signature(signature_method, consumer, token))
00260
00261 def build_signature(self, signature_method, consumer, token):
00262 """Calls the build signature method within the signature method."""
00263 return signature_method.build_signature(self, consumer, token)
00264
00265 def from_request(http_method, http_url, headers=None, parameters=None,
00266 query_string=None):
00267 """Combines multiple parameter sources."""
00268 if parameters is None:
00269 parameters = {}
00270
00271
00272 if headers and 'Authorization' in headers:
00273 auth_header = headers['Authorization']
00274
00275 if auth_header[:6] == 'OAuth ':
00276 auth_header = auth_header[6:]
00277 try:
00278
00279 header_params = OAuthRequest._split_header(auth_header)
00280 parameters.update(header_params)
00281 except:
00282 raise OAuthError('Unable to parse OAuth parameters from '
00283 'Authorization header.')
00284
00285
00286 if query_string:
00287 query_params = OAuthRequest._split_url_string(query_string)
00288 parameters.update(query_params)
00289
00290
00291 param_str = urlparse.urlparse(http_url)[4]
00292 url_params = OAuthRequest._split_url_string(param_str)
00293 parameters.update(url_params)
00294
00295 if parameters:
00296 return OAuthRequest(http_method, http_url, parameters)
00297
00298 return None
00299 from_request = staticmethod(from_request)
00300
00301 def from_consumer_and_token(oauth_consumer, token=None,
00302 callback=None, verifier=None, http_method=HTTP_METHOD,
00303 http_url=None, parameters=None):
00304 if not parameters:
00305 parameters = {}
00306
00307 defaults = {
00308 'oauth_consumer_key': oauth_consumer.key,
00309 'oauth_timestamp': generate_timestamp(),
00310 'oauth_nonce': generate_nonce(),
00311 'oauth_version': OAuthRequest.version,
00312 }
00313
00314 defaults.update(parameters)
00315 parameters = defaults
00316
00317 if token:
00318 parameters['oauth_token'] = token.key
00319 if token.callback:
00320 parameters['oauth_callback'] = token.callback
00321
00322 if verifier:
00323 parameters['oauth_verifier'] = verifier
00324 elif callback:
00325
00326 parameters['oauth_callback'] = callback
00327 return OAuthRequest(http_method, http_url, parameters)
00328 from_consumer_and_token = staticmethod(from_consumer_and_token)
00329
00330 def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
00331 http_url=None, parameters=None):
00332 if not parameters:
00333 parameters = {}
00334
00335 parameters['oauth_token'] = token.key
00336
00337 if callback:
00338 parameters['oauth_callback'] = callback
00339
00340 return OAuthRequest(http_method, http_url, parameters)
00341 from_token_and_callback = staticmethod(from_token_and_callback)
00342
00343 def _split_header(header):
00344 """Turn Authorization: header into parameters."""
00345 params = {}
00346 parts = header.split(',')
00347 for param in parts:
00348
00349 if param.find('realm') > -1:
00350 continue
00351
00352 param = param.strip()
00353
00354 param_parts = param.split('=', 1)
00355
00356 params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
00357 return params
00358 _split_header = staticmethod(_split_header)
00359
00360 def _split_url_string(param_str):
00361 """Turn URL string into parameters."""
00362 parameters = cgi.parse_qs(param_str, keep_blank_values=False)
00363 for k, v in parameters.iteritems():
00364 parameters[k] = urllib.unquote(v[0])
00365 return parameters
00366 _split_url_string = staticmethod(_split_url_string)
00367
00368 class OAuthServer(object):
00369 """A worker to check the validity of a request against a data store."""
00370 timestamp_threshold = 300
00371 version = VERSION
00372 signature_methods = None
00373 data_store = None
00374
00375 def __init__(self, data_store=None, signature_methods=None):
00376 self.data_store = data_store
00377 self.signature_methods = signature_methods or {}
00378
00379 def set_data_store(self, data_store):
00380 self.data_store = data_store
00381
00382 def get_data_store(self):
00383 return self.data_store
00384
00385 def add_signature_method(self, signature_method):
00386 self.signature_methods[signature_method.get_name()] = signature_method
00387 return self.signature_methods
00388
00389 def fetch_request_token(self, oauth_request):
00390 """Processes a request_token request and returns the
00391 request token on success.
00392 """
00393 try:
00394
00395 token = self._get_token(oauth_request, 'request')
00396 except OAuthError:
00397
00398 version = self._get_version(oauth_request)
00399 consumer = self._get_consumer(oauth_request)
00400 try:
00401 callback = self.get_callback(oauth_request)
00402 except OAuthError:
00403 callback = None
00404 self._check_signature(oauth_request, consumer, None)
00405
00406 token = self.data_store.fetch_request_token(consumer, callback)
00407 return token
00408
00409 def fetch_access_token(self, oauth_request):
00410 """Processes an access_token request and returns the
00411 access token on success.
00412 """
00413 version = self._get_version(oauth_request)
00414 consumer = self._get_consumer(oauth_request)
00415 try:
00416 verifier = self._get_verifier(oauth_request)
00417 except OAuthError:
00418 verifier = None
00419
00420 token = self._get_token(oauth_request, 'request')
00421 self._check_signature(oauth_request, consumer, token)
00422 new_token = self.data_store.fetch_access_token(consumer, token, verifier)
00423 return new_token
00424
00425 def verify_request(self, oauth_request):
00426 """Verifies an api call and checks all the parameters."""
00427
00428 version = self._get_version(oauth_request)
00429 consumer = self._get_consumer(oauth_request)
00430
00431 token = self._get_token(oauth_request, 'access')
00432 self._check_signature(oauth_request, consumer, token)
00433 parameters = oauth_request.get_nonoauth_parameters()
00434 return consumer, token, parameters
00435
00436 def authorize_token(self, token, user):
00437 """Authorize a request token."""
00438 return self.data_store.authorize_request_token(token, user)
00439
00440 def get_callback(self, oauth_request):
00441 """Get the callback URL."""
00442 return oauth_request.get_parameter('oauth_callback')
00443
00444 def build_authenticate_header(self, realm=''):
00445 """Optional support for the authenticate header."""
00446 return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
00447
00448 def _get_version(self, oauth_request):
00449 """Verify the correct version request for this server."""
00450 try:
00451 version = oauth_request.get_parameter('oauth_version')
00452 except:
00453 version = VERSION
00454 if version and version != self.version:
00455 raise OAuthError('OAuth version %s not supported.' % str(version))
00456 return version
00457
00458 def _get_signature_method(self, oauth_request):
00459 """Figure out the signature with some defaults."""
00460 try:
00461 signature_method = oauth_request.get_parameter(
00462 'oauth_signature_method')
00463 except:
00464 signature_method = SIGNATURE_METHOD
00465 try:
00466
00467 signature_method = self.signature_methods[signature_method]
00468 except:
00469 signature_method_names = ', '.join(self.signature_methods.keys())
00470 raise OAuthError('Signature method %s not supported try one of the '
00471 'following: %s' % (signature_method, signature_method_names))
00472
00473 return signature_method
00474
00475 def _get_consumer(self, oauth_request):
00476 consumer_key = oauth_request.get_parameter('oauth_consumer_key')
00477 consumer = self.data_store.lookup_consumer(consumer_key)
00478 if not consumer:
00479 raise OAuthError('Invalid consumer.')
00480 return consumer
00481
00482 def _get_token(self, oauth_request, token_type='access'):
00483 """Try to find the token for the provided request token key."""
00484 token_field = oauth_request.get_parameter('oauth_token')
00485 token = self.data_store.lookup_token(token_type, token_field)
00486 if not token:
00487 raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
00488 return token
00489
00490 def _get_verifier(self, oauth_request):
00491 return oauth_request.get_parameter('oauth_verifier')
00492
00493 def _check_signature(self, oauth_request, consumer, token):
00494 timestamp, nonce = oauth_request._get_timestamp_nonce()
00495 self._check_timestamp(timestamp)
00496 self._check_nonce(consumer, token, nonce)
00497 signature_method = self._get_signature_method(oauth_request)
00498 try:
00499 signature = oauth_request.get_parameter('oauth_signature')
00500 except:
00501 raise OAuthError('Missing signature.')
00502
00503 valid_sig = signature_method.check_signature(oauth_request, consumer,
00504 token, signature)
00505 if not valid_sig:
00506 key, base = signature_method.build_signature_base_string(
00507 oauth_request, consumer, token)
00508 raise OAuthError('Invalid signature. Expected signature base '
00509 'string: %s' % base)
00510 built = signature_method.build_signature(oauth_request, consumer, token)
00511
00512 def _check_timestamp(self, timestamp):
00513 """Verify that timestamp is recentish."""
00514 timestamp = int(timestamp)
00515 now = int(time.time())
00516 lapsed = abs(now - timestamp)
00517 if lapsed > self.timestamp_threshold:
00518 raise OAuthError('Expired timestamp: given %d and now %s has a '
00519 'greater difference than threshold %d' %
00520 (timestamp, now, self.timestamp_threshold))
00521
00522 def _check_nonce(self, consumer, token, nonce):
00523 """Verify that the nonce is uniqueish."""
00524 nonce = self.data_store.lookup_nonce(consumer, token, nonce)
00525 if nonce:
00526 raise OAuthError('Nonce already used: %s' % str(nonce))
00527
00528
00529 class OAuthClient(object):
00530 """OAuthClient is a worker to attempt to execute a request."""
00531 consumer = None
00532 token = None
00533
00534 def __init__(self, oauth_consumer, oauth_token):
00535 self.consumer = oauth_consumer
00536 self.token = oauth_token
00537
00538 def get_consumer(self):
00539 return self.consumer
00540
00541 def get_token(self):
00542 return self.token
00543
00544 def fetch_request_token(self, oauth_request):
00545 """-> OAuthToken."""
00546 raise NotImplementedError
00547
00548 def fetch_access_token(self, oauth_request):
00549 """-> OAuthToken."""
00550 raise NotImplementedError
00551
00552 def access_resource(self, oauth_request):
00553 """-> Some protected resource."""
00554 raise NotImplementedError
00555
00556
00557 class OAuthDataStore(object):
00558 """A database abstraction used to lookup consumers and tokens."""
00559
00560 def lookup_consumer(self, key):
00561 """-> OAuthConsumer."""
00562 raise NotImplementedError
00563
00564 def lookup_token(self, oauth_consumer, token_type, token_token):
00565 """-> OAuthToken."""
00566 raise NotImplementedError
00567
00568 def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
00569 """-> OAuthToken."""
00570 raise NotImplementedError
00571
00572 def fetch_request_token(self, oauth_consumer, oauth_callback):
00573 """-> OAuthToken."""
00574 raise NotImplementedError
00575
00576 def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
00577 """-> OAuthToken."""
00578 raise NotImplementedError
00579
00580 def authorize_request_token(self, oauth_token, user):
00581 """-> OAuthToken."""
00582 raise NotImplementedError
00583
00584
00585 class OAuthSignatureMethod(object):
00586 """A strategy class that implements a signature method."""
00587 def get_name(self):
00588 """-> str."""
00589 raise NotImplementedError
00590
00591 def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
00592 """-> str key, str raw."""
00593 raise NotImplementedError
00594
00595 def build_signature(self, oauth_request, oauth_consumer, oauth_token):
00596 """-> str."""
00597 raise NotImplementedError
00598
00599 def check_signature(self, oauth_request, consumer, token, signature):
00600 built = self.build_signature(oauth_request, consumer, token)
00601 return built == signature
00602
00603
00604 class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
00605
00606 def get_name(self):
00607 return 'HMAC-SHA1'
00608
00609 def build_signature_base_string(self, oauth_request, consumer, token):
00610 sig = (
00611 escape(oauth_request.get_normalized_http_method()),
00612 escape(oauth_request.get_normalized_http_url()),
00613 escape(oauth_request.get_normalized_parameters()),
00614 )
00615
00616 key = '%s&' % escape(consumer.secret)
00617 if token:
00618 key += escape(token.secret)
00619 raw = '&'.join(sig)
00620 return key, raw
00621
00622 def build_signature(self, oauth_request, consumer, token):
00623 """Builds the base signature string."""
00624 key, raw = self.build_signature_base_string(oauth_request, consumer,
00625 token)
00626
00627 try:
00628 import hashlib
00629 hashed = hmac.new(key, raw, hashlib.sha1)
00630 except:
00631 import sha
00632 hashed = hmac.new(key, raw, sha)
00633
00634
00635 return binascii.b2a_base64(hashed.digest())[:-1]
00636
00637
00638 class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
00639
00640 def get_name(self):
00641 return 'PLAINTEXT'
00642
00643 def build_signature_base_string(self, oauth_request, consumer, token):
00644 """Concatenates the consumer key and secret."""
00645 sig = '%s&' % escape(consumer.secret)
00646 if token:
00647 sig = sig + escape(token.secret)
00648 return sig, sig
00649
00650 def build_signature(self, oauth_request, consumer, token):
00651 key, raw = self.build_signature_base_string(oauth_request, consumer,
00652 token)
00653 return key