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
26 import urllib.request, urllib.parse, urllib.error
35 SIGNATURE_METHOD =
'PLAINTEXT'
39 """Generic exception class."""
40 def __init__(self, message='OAuth error occured.'):
44 """Optional WWW-Authenticate header (401 error)"""
45 return {
'WWW-Authenticate':
'OAuth realm="%s"' % realm}
48 """Escape a URL including any /."""
49 return urllib.parse.quote(s, safe=
'~')
52 """Convert unicode to utf-8."""
53 if isinstance(s, str):
54 return s.encode(
"utf-8")
59 """Get seconds since epoch (UTC)."""
60 return int(time.time())
63 """Generate pseudorandom number."""
64 return ''.join([str(random.randint(0, 9))
for i
in range(length)])
67 """Generate pseudorandom number."""
68 return ''.join([str(random.randint(0, 9))
for i
in range(length)])
72 """Consumer of OAuth authentication.
74 OAuthConsumer is a data type that represents the identity of the Consumer
75 via its shared secret with the Service Provider.
87 """OAuthToken is a data type that represents an End User via either an access
91 secret -- the token secret
97 callback_confirmed =
None
109 if verifier
is not None:
117 parts = urllib.parse.urlparse(self.
callback)
118 scheme, netloc, path, params, query, fragment = parts[:6]
120 query =
'%s&oauth_verifier=%s' % (query, self.
verifier)
122 query =
'oauth_verifier=%s' % self.
verifier
123 return urllib.parse.urlunparse((scheme, netloc, path, params,
129 'oauth_token': self.
key,
130 'oauth_token_secret': self.
secret,
134 return urllib.parse.urlencode(data)
137 """ Returns a token from something like:
138 oauth_token_secret=xxx&oauth_token=xxx
140 params = cgi.parse_qs(s, keep_blank_values=
False)
141 key = params[
'oauth_token'][0]
142 secret = params[
'oauth_token_secret'][0]
145 token.callback_confirmed = params[
'oauth_callback_confirmed'][0]
149 from_string = staticmethod(from_string)
156 """OAuthRequest represents the request and can be serialized.
161 - oauth_signature_method
167 ... any additional parameters, as defined by the Service Provider.
170 http_method = HTTP_METHOD
174 def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None):
186 raise OAuthError(
'Parameter not found: %s' % parameter)
193 """Get any non-OAuth parameters."""
197 if k.find(
'oauth_') < 0:
202 """Serialize as a header for an HTTPAuth request."""
203 auth_header =
'OAuth realm="%s"' % realm
207 if k[:6] ==
'oauth_':
208 auth_header +=
', %s="%s"' % (k,
escape(str(v)))
209 return {
'Authorization': auth_header}
212 """Serialize as post data for a POST request."""
213 return '&'.join([
'%s=%s' % (
escape(str(k)),
escape(str(v))) \
217 """Serialize as a URL for a GET request."""
221 """Return a string that contains the parameters that must be signed."""
225 del params[
'oauth_signature']
230 for k,v
in list(params.items())]
234 return '&'.join([
'%s=%s' % (k, v)
for k, v
in key_values])
237 """Uppercases the http method."""
241 """Parses the URL and rebuilds it to be scheme://host/path."""
242 parts = urllib.parse.urlparse(self.
http_url)
243 scheme, netloc, path = parts[:3]
245 if scheme ==
'http' and netloc[-3:] ==
':80':
247 elif scheme ==
'https' and netloc[-4:] ==
':443':
249 return '%s://%s%s' % (scheme, netloc, path)
252 """Set the signature parameter to the result of build_signature."""
255 signature_method.get_name())
261 """Calls the build signature method within the signature method."""
262 return signature_method.build_signature(self, consumer, token)
266 """Combines multiple parameter sources."""
267 if parameters
is None:
271 if headers
and 'Authorization' in headers:
272 auth_header = headers[
'Authorization']
274 if auth_header[:6] ==
'OAuth ':
275 auth_header = auth_header[6:]
278 header_params = OAuthRequest._split_header(auth_header)
279 parameters.update(header_params)
281 raise OAuthError(
'Unable to parse OAuth parameters from '
282 'Authorization header.')
286 query_params = OAuthRequest._split_url_string(query_string)
287 parameters.update(query_params)
290 param_str = urllib.parse.urlparse(http_url)[4]
291 url_params = OAuthRequest._split_url_string(param_str)
292 parameters.update(url_params)
298 from_request = staticmethod(from_request)
301 callback=None, verifier=None, http_method=HTTP_METHOD,
302 http_url=None, parameters=None):
307 'oauth_consumer_key': oauth_consumer.key,
310 'oauth_version': OAuthRequest.version,
313 defaults.update(parameters)
314 parameters = defaults
317 parameters[
'oauth_token'] = token.key
319 parameters[
'oauth_callback'] = token.callback
322 parameters[
'oauth_verifier'] = verifier
325 parameters[
'oauth_callback'] = callback
328 from_consumer_and_token = staticmethod(from_consumer_and_token)
331 http_url=None, parameters=None):
335 parameters[
'oauth_token'] = token.key
338 parameters[
'oauth_callback'] = callback
341 from_token_and_callback = staticmethod(from_token_and_callback)
344 """Turn Authorization: header into parameters."""
346 parts = header.split(
',')
349 if param.find(
'realm') > -1:
352 param = param.strip()
354 param_parts = param.split(
'=', 1)
356 params[param_parts[0]] = urllib.parse.unquote(param_parts[1].strip(
'\"'))
358 _split_header = staticmethod(_split_header)
361 """Turn URL string into parameters."""
362 parameters = cgi.parse_qs(param_str, keep_blank_values=
False)
363 for k, v
in parameters.items():
364 parameters[k] = urllib.parse.unquote(v[0])
366 _split_url_string = staticmethod(_split_url_string)
369 """A worker to check the validity of a request against a data store."""
370 timestamp_threshold = 300
372 signature_methods =
None
375 def __init__(self, data_store=None, signature_methods=None):
390 """Processes a request_token request and returns the
391 request token on success.
395 token = self.
_get_token(oauth_request,
'request')
410 """Processes an access_token request and returns the
411 access token on success.
420 token = self.
_get_token(oauth_request,
'request')
426 """Verifies an api call and checks all the parameters."""
431 token = self.
_get_token(oauth_request,
'access')
433 parameters = oauth_request.get_nonoauth_parameters()
434 return consumer, token, parameters
437 """Authorize a request token."""
438 return self.
data_store.authorize_request_token(token, user)
441 """Get the callback URL."""
442 return oauth_request.get_parameter(
'oauth_callback')
445 """Optional support for the authenticate header."""
446 return {
'WWW-Authenticate':
'OAuth realm="%s"' % realm}
449 """Verify the correct version request for this server."""
451 version = oauth_request.get_parameter(
'oauth_version')
454 if version
and version != self.
version:
455 raise OAuthError(
'OAuth version %s not supported.' % str(version))
459 """Figure out the signature with some defaults."""
461 signature_method = oauth_request.get_parameter(
462 'oauth_signature_method')
464 signature_method = SIGNATURE_METHOD
470 raise OAuthError(
'Signature method %s not supported try one of the '
471 'following: %s' % (signature_method, signature_method_names))
473 return signature_method
476 consumer_key = oauth_request.get_parameter(
'oauth_consumer_key')
477 consumer = self.
data_store.lookup_consumer(consumer_key)
483 """Try to find the token for the provided request token key."""
484 token_field = oauth_request.get_parameter(
'oauth_token')
485 token = self.
data_store.lookup_token(token_type, token_field)
487 raise OAuthError(
'Invalid %s token: %s' % (token_type, token_field))
491 return oauth_request.get_parameter(
'oauth_verifier')
494 timestamp, nonce = oauth_request._get_timestamp_nonce()
499 signature = oauth_request.get_parameter(
'oauth_signature')
503 valid_sig = signature_method.check_signature(oauth_request, consumer,
506 key, base = signature_method.build_signature_base_string(
507 oauth_request, consumer, token)
508 raise OAuthError(
'Invalid signature. Expected signature base '
510 built = signature_method.build_signature(oauth_request, consumer, token)
513 """Verify that timestamp is recentish."""
514 timestamp = int(timestamp)
515 now = int(time.time())
516 lapsed = abs(now - timestamp)
518 raise OAuthError(
'Expired timestamp: given %d and now %s has a '
519 'greater difference than threshold %d' %
523 """Verify that the nonce is uniqueish."""
524 nonce = self.
data_store.lookup_nonce(consumer, token, nonce)
526 raise OAuthError(
'Nonce already used: %s' % str(nonce))
530 """OAuthClient is a worker to attempt to execute a request."""
536 self.
token = oauth_token
546 raise NotImplementedError
550 raise NotImplementedError
553 """-> Some protected resource."""
554 raise NotImplementedError
558 """A database abstraction used to lookup consumers and tokens."""
561 """-> OAuthConsumer."""
562 raise NotImplementedError
566 raise NotImplementedError
570 raise NotImplementedError
574 raise NotImplementedError
578 raise NotImplementedError
582 raise NotImplementedError
586 """A strategy class that implements a signature method."""
589 raise NotImplementedError
592 """-> str key, str raw."""
593 raise NotImplementedError
597 raise NotImplementedError
601 return built == signature
611 escape(oauth_request.get_normalized_http_method()),
612 escape(oauth_request.get_normalized_http_url()),
613 escape(oauth_request.get_normalized_parameters()),
616 key =
'%s&' %
escape(consumer.secret)
618 key +=
escape(token.secret)
623 """Builds the base signature string."""
630 hashed = hmac.new(key, raw, hashlib.sha1)
633 hashed = hmac.new(key, raw, sha)
636 return binascii.b2a_base64(hashed.digest())[:-1]
645 """Concatenates the consumer key and secret."""
646 sig =
'%s&' %
escape(consumer.secret)
648 sig = sig +
escape(token.secret)