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
320
parameters['oauth_callback'] = token.callback
321
# 1.0a support for verifier.
323
parameters['oauth_verifier'] = verifier
325
# 1.0a support for callback in the request token request.
326
parameters['oauth_callback'] = callback
328
return OAuthRequest(http_method, http_url, parameters)
329
from_consumer_and_token = staticmethod(from_consumer_and_token)
331
def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD,
332
http_url=None, parameters=None):
336
parameters['oauth_token'] = token.key
339
parameters['oauth_callback'] = callback
341
return OAuthRequest(http_method, http_url, parameters)
342
from_token_and_callback = staticmethod(from_token_and_callback)
344
def _split_header(header):
345
"""Turn Authorization: header into parameters."""
347
parts = header.split(',')
349
# Ignore realm parameter.
350
if param.find('realm') > -1:
353
param = param.strip()
355
param_parts = param.split('=', 1)
356
# Remove quotes and unescape the value.
357
params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
359
_split_header = staticmethod(_split_header)
361
def _split_url_string(param_str):
362
"""Turn URL string into parameters."""
363
parameters = cgi.parse_qs(param_str, keep_blank_values=False)
364
for k, v in parameters.iteritems():
365
parameters[k] = urllib.unquote(v[0])
367
_split_url_string = staticmethod(_split_url_string)
369
class OAuthServer(object):
370
"""A worker to check the validity of a request against a data store."""
371
timestamp_threshold = 300 # In seconds, five minutes.
373
signature_methods = None
376
def __init__(self, data_store=None, signature_methods=None):
377
self.data_store = data_store
378
self.signature_methods = signature_methods or {}
380
def set_data_store(self, data_store):
381
self.data_store = data_store
383
def get_data_store(self):
384
return self.data_store
386
def add_signature_method(self, signature_method):
387
self.signature_methods[signature_method.get_name()] = signature_method
388
return self.signature_methods
390
def fetch_request_token(self, oauth_request):
391
"""Processes a request_token request and returns the
392
request token on success.
395
# Get the request token for authorization.
396
token = self._get_token(oauth_request, 'request')
398
# No token required for the initial token request.
399
version = self._get_version(oauth_request)
400
consumer = self._get_consumer(oauth_request)
402
callback = self.get_callback(oauth_request)
404
callback = None # 1.0, no callback specified.
405
self._check_signature(oauth_request, consumer, None)
407
token = self.data_store.fetch_request_token(consumer, callback)
410
def fetch_access_token(self, oauth_request):
411
"""Processes an access_token request and returns the
412
access token on success.
414
version = self._get_version(oauth_request)
415
consumer = self._get_consumer(oauth_request)
417
verifier = self._get_verifier(oauth_request)
420
# Get the request token.
421
token = self._get_token(oauth_request, 'request')
422
self._check_signature(oauth_request, consumer, token)
423
new_token = self.data_store.fetch_access_token(consumer, token, verifier)
426
def verify_request(self, oauth_request):
427
"""Verifies an api call and checks all the parameters."""
428
# -> consumer and token
429
version = self._get_version(oauth_request)
430
consumer = self._get_consumer(oauth_request)
431
# Get the access token.
432
token = self._get_token(oauth_request, 'access')
433
self._check_signature(oauth_request, consumer, token)
434
parameters = oauth_request.get_nonoauth_parameters()
435
return consumer, token, parameters
437
def authorize_token(self, token, user):
438
"""Authorize a request token."""
439
return self.data_store.authorize_request_token(token, user)
441
def get_callback(self, oauth_request):
442
"""Get the callback URL."""
443
return oauth_request.get_parameter('oauth_callback')
445
def build_authenticate_header(self, realm=''):
446
"""Optional support for the authenticate header."""
447
return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
449
def _get_version(self, oauth_request):
450
"""Verify the correct version request for this server."""
452
version = oauth_request.get_parameter('oauth_version')
455
if version and version != self.version:
456
raise OAuthError('OAuth version %s not supported.' % str(version))
459
def _get_signature_method(self, oauth_request):
460
"""Figure out the signature with some defaults."""
462
signature_method = oauth_request.get_parameter(
463
'oauth_signature_method')
465
signature_method = SIGNATURE_METHOD
467
# Get the signature method object.
468
signature_method = self.signature_methods[signature_method]
470
signature_method_names = ', '.join(self.signature_methods.keys())
471
raise OAuthError('Signature method %s not supported try one of the '
472
'following: %s' % (signature_method, signature_method_names))
474
return signature_method
476
def _get_consumer(self, oauth_request):
477
consumer_key = oauth_request.get_parameter('oauth_consumer_key')
478
consumer = self.data_store.lookup_consumer(consumer_key)
480
raise OAuthError('Invalid consumer.')
483
def _get_token(self, oauth_request, token_type='access'):
484
"""Try to find the token for the provided request token key."""
485
token_field = oauth_request.get_parameter('oauth_token')
486
token = self.data_store.lookup_token(token_type, token_field)
488
raise OAuthError('Invalid %s token: %s' % (token_type, token_field))
491
def _get_verifier(self, oauth_request):
492
return oauth_request.get_parameter('oauth_verifier')
494
def _check_signature(self, oauth_request, consumer, token):
495
timestamp, nonce = oauth_request._get_timestamp_nonce()
496
self._check_timestamp(timestamp)
497
self._check_nonce(consumer, token, nonce)
498
signature_method = self._get_signature_method(oauth_request)
500
signature = oauth_request.get_parameter('oauth_signature')
502
raise OAuthError('Missing signature.')
503
# Validate the signature.
504
valid_sig = signature_method.check_signature(oauth_request, consumer,
507
key, base = signature_method.build_signature_base_string(
508
oauth_request, consumer, token)
509
raise OAuthError('Invalid signature. Expected signature base '
511
built = signature_method.build_signature(oauth_request, consumer, token)
513
def _check_timestamp(self, timestamp):
514
"""Verify that timestamp is recentish."""
515
timestamp = int(timestamp)
516
now = int(time.time())
517
lapsed = abs(now - timestamp)
518
if lapsed > self.timestamp_threshold:
519
raise OAuthError('Expired timestamp: given %d and now %s has a '
520
'greater difference than threshold %d' %
521
(timestamp, now, self.timestamp_threshold))
523
def _check_nonce(self, consumer, token, nonce):
524
"""Verify that the nonce is uniqueish."""
525
nonce = self.data_store.lookup_nonce(consumer, token, nonce)
527
raise OAuthError('Nonce already used: %s' % str(nonce))
530
class OAuthClient(object):
531
"""OAuthClient is a worker to attempt to execute a request."""
535
def __init__(self, oauth_consumer, oauth_token):
536
self.consumer = oauth_consumer
537
self.token = oauth_token
539
def get_consumer(self):
545
def fetch_request_token(self, oauth_request):
547
raise NotImplementedError
549
def fetch_access_token(self, oauth_request):
551
raise NotImplementedError
553
def access_resource(self, oauth_request):
554
"""-> Some protected resource."""
555
raise NotImplementedError
558
class OAuthDataStore(object):
559
"""A database abstraction used to lookup consumers and tokens."""
561
def lookup_consumer(self, key):
562
"""-> OAuthConsumer."""
563
raise NotImplementedError
565
def lookup_token(self, oauth_consumer, token_type, token_token):
567
raise NotImplementedError
569
def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
571
raise NotImplementedError
573
def fetch_request_token(self, oauth_consumer, oauth_callback):
575
raise NotImplementedError
577
def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
579
raise NotImplementedError
581
def authorize_request_token(self, oauth_token, user):
583
raise NotImplementedError
586
class OAuthSignatureMethod(object):
587
"""A strategy class that implements a signature method."""
590
raise NotImplementedError
592
def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token):
593
"""-> str key, str raw."""
594
raise NotImplementedError
596
def build_signature(self, oauth_request, oauth_consumer, oauth_token):
598
raise NotImplementedError
600
def check_signature(self, oauth_request, consumer, token, signature):
601
built = self.build_signature(oauth_request, consumer, token)
602
return built == signature
605
class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod):
610
def build_signature_base_string(self, oauth_request, consumer, token):
612
escape(oauth_request.get_normalized_http_method()),
613
escape(oauth_request.get_normalized_http_url()),
614
escape(oauth_request.get_normalized_parameters()),
617
key = '%s&' % escape(consumer.secret)
619
key += escape(token.secret)
623
def build_signature(self, oauth_request, consumer, token):
624
"""Builds the base signature string."""
625
key, raw = self.build_signature_base_string(oauth_request, consumer,
631
hashed = hmac.new(key, raw, hashlib.sha1)
633
import sha # Deprecated
634
hashed = hmac.new(key, raw, sha)
636
# Calculate the digest base 64.
637
return binascii.b2a_base64(hashed.digest())[:-1]
640
class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod):
645
def build_signature_base_string(self, oauth_request, consumer, token):
646
"""Concatenates the consumer key and secret."""
647
sig = '%s&' % escape(consumer.secret)
649
sig = sig + escape(token.secret)
652
def build_signature(self, oauth_request, consumer, token):
653
key, raw = self.build_signature_base_string(oauth_request, consumer,
b'\\ No newline at end of file'