mirror of
https://github.com/kennethreitz/requests.git
synced 2026-06-05 22:50:18 +00:00
no more oauth
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
requests._oauth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains the path hack necessary for oauthlib to be vendored into
|
||||
requests while allowing upstream changes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from oauthlib.oauth1 import rfc5849
|
||||
from oauthlib.common import extract_params
|
||||
from oauthlib.oauth1.rfc5849 import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER)
|
||||
except ImportError:
|
||||
from .packages import oauthlib
|
||||
sys.modules['oauthlib'] = oauthlib
|
||||
from oauthlib.oauth1 import rfc5849
|
||||
from oauthlib.common import extract_params
|
||||
from oauthlib.oauth1.rfc5849 import (Client, SIGNATURE_HMAC, SIGNATURE_TYPE_AUTH_HEADER)
|
||||
@@ -1,229 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
"""
|
||||
oauthlib.common
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module provides data structures and utilities common
|
||||
to all implementations of OAuth.
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import time
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
UNICODE_ASCII_CHARACTER_SET = (string.ascii_letters.decode('ascii') +
|
||||
string.digits.decode('ascii'))
|
||||
|
||||
always_safe = (u'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
u'abcdefghijklmnopqrstuvwxyz'
|
||||
u'0123456789' u'_.-')
|
||||
|
||||
|
||||
def quote(s, safe=u'/'):
|
||||
encoded = s.encode("utf-8")
|
||||
quoted = urllib.quote(encoded, safe)
|
||||
return quoted.decode("utf-8")
|
||||
|
||||
|
||||
def unquote(s):
|
||||
encoded = s.encode("utf-8")
|
||||
unquoted = urllib.unquote(encoded)
|
||||
return unquoted.decode("utf-8")
|
||||
|
||||
|
||||
def urlencode(params):
|
||||
utf8_params = encode_params_utf8(params)
|
||||
urlencoded = urllib.urlencode(utf8_params)
|
||||
return urlencoded.decode("utf-8")
|
||||
|
||||
|
||||
def encode_params_utf8(params):
|
||||
"""Ensures that all parameters in a list of 2-element tuples are encoded to
|
||||
bytestrings using UTF-8
|
||||
"""
|
||||
encoded = []
|
||||
for k, v in params:
|
||||
encoded.append((
|
||||
k.encode('utf-8') if isinstance(k, unicode) else k,
|
||||
v.encode('utf-8') if isinstance(v, unicode) else v))
|
||||
return encoded
|
||||
|
||||
|
||||
def decode_params_utf8(params):
|
||||
"""Ensures that all parameters in a list of 2-element tuples are decoded to
|
||||
unicode using UTF-8.
|
||||
"""
|
||||
decoded = []
|
||||
for k, v in params:
|
||||
decoded.append((
|
||||
k.decode('utf-8') if isinstance(k, str) else k,
|
||||
v.decode('utf-8') if isinstance(v, str) else v))
|
||||
return decoded
|
||||
|
||||
|
||||
urlencoded = set(always_safe) | set(u'=&;%+~')
|
||||
|
||||
|
||||
def urldecode(query):
|
||||
"""Decode a query string in x-www-form-urlencoded format into a sequence
|
||||
of two-element tuples.
|
||||
|
||||
Unlike urlparse.parse_qsl(..., strict_parsing=True) urldecode will enforce
|
||||
correct formatting of the query string by validation. If validation fails
|
||||
a ValueError will be raised. urllib.parse_qsl will only raise errors if
|
||||
any of name-value pairs omits the equals sign.
|
||||
"""
|
||||
# Check if query contains invalid characters
|
||||
if query and not set(query) <= urlencoded:
|
||||
raise ValueError('Invalid characters in query string.')
|
||||
|
||||
# Check for correctly hex encoded values using a regular expression
|
||||
# All encoded values begin with % followed by two hex characters
|
||||
# correct = %00, %A0, %0A, %FF
|
||||
# invalid = %G0, %5H, %PO
|
||||
invalid_hex = u'%[^0-9A-Fa-f]|%[0-9A-Fa-f][^0-9A-Fa-f]'
|
||||
if len(re.findall(invalid_hex, query)):
|
||||
raise ValueError('Invalid hex encoding in query string.')
|
||||
|
||||
query = query.decode('utf-8') if isinstance(query, str) else query
|
||||
# We want to allow queries such as "c2" whereas urlparse.parse_qsl
|
||||
# with the strict_parsing flag will not.
|
||||
params = urlparse.parse_qsl(query, keep_blank_values=True)
|
||||
|
||||
# unicode all the things
|
||||
return decode_params_utf8(params)
|
||||
|
||||
|
||||
def extract_params(raw):
|
||||
"""Extract parameters and return them as a list of 2-tuples.
|
||||
|
||||
Will successfully extract parameters from urlencoded query strings,
|
||||
dicts, or lists of 2-tuples. Empty strings/dicts/lists will return an
|
||||
empty list of parameters. Any other input will result in a return
|
||||
value of None.
|
||||
"""
|
||||
if isinstance(raw, basestring):
|
||||
try:
|
||||
params = urldecode(raw)
|
||||
except ValueError:
|
||||
params = None
|
||||
elif hasattr(raw, '__iter__'):
|
||||
try:
|
||||
dict(raw)
|
||||
except ValueError:
|
||||
params = None
|
||||
except TypeError:
|
||||
params = None
|
||||
else:
|
||||
params = list(raw.items() if isinstance(raw, dict) else raw)
|
||||
params = decode_params_utf8(params)
|
||||
else:
|
||||
params = None
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def generate_nonce():
|
||||
"""Generate pseudorandom nonce that is unlikely to repeat.
|
||||
|
||||
Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
|
||||
Per `section 3.2.1`_ of the MAC Access Authentication spec.
|
||||
|
||||
A random 64-bit number is appended to the epoch timestamp for both
|
||||
randomness and to decrease the likelihood of collisions.
|
||||
|
||||
.. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
|
||||
.. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
|
||||
"""
|
||||
return unicode(unicode(random.getrandbits(64)) + generate_timestamp())
|
||||
|
||||
|
||||
def generate_timestamp():
|
||||
"""Get seconds since epoch (UTC).
|
||||
|
||||
Per `section 3.3`_ of the OAuth 1 RFC 5849 spec.
|
||||
Per `section 3.2.1`_ of the MAC Access Authentication spec.
|
||||
|
||||
.. _`section 3.2.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-3.2.1
|
||||
.. _`section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
|
||||
"""
|
||||
return unicode(int(time.time()))
|
||||
|
||||
|
||||
def generate_token(length=30, chars=UNICODE_ASCII_CHARACTER_SET):
|
||||
"""Generates a non-guessable OAuth token
|
||||
|
||||
OAuth (1 and 2) does not specify the format of tokens except that they
|
||||
should be strings of random characters. Tokens should not be guessable
|
||||
and entropy when generating the random characters is important. Which is
|
||||
why SystemRandom is used instead of the default random.choice method.
|
||||
"""
|
||||
rand = random.SystemRandom()
|
||||
return u''.join(rand.choice(chars) for x in range(length))
|
||||
|
||||
|
||||
def add_params_to_qs(query, params):
|
||||
"""Extend a query with a list of two-tuples."""
|
||||
queryparams = urlparse.parse_qsl(query, keep_blank_values=True)
|
||||
queryparams.extend(params)
|
||||
return urlencode(queryparams)
|
||||
|
||||
|
||||
def add_params_to_uri(uri, params):
|
||||
"""Add a list of two-tuples to the uri query components."""
|
||||
sch, net, path, par, query, fra = urlparse.urlparse(uri)
|
||||
query = add_params_to_qs(query, params)
|
||||
return urlparse.urlunparse((sch, net, path, par, query, fra))
|
||||
|
||||
def safe_string_equals(a, b):
|
||||
""" Near-constant time string comparison.
|
||||
|
||||
Used in order to avoid timing attacks on sensitive information such
|
||||
as secret keys during request verification (`rootLabs`_).
|
||||
|
||||
.. _`rootLabs`: http://rdist.root.org/2010/01/07/timing-independent-array-comparison/
|
||||
|
||||
"""
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
|
||||
result = 0
|
||||
for x, y in zip(a, b):
|
||||
result |= ord(x) ^ ord(y)
|
||||
return result == 0
|
||||
|
||||
class Request(object):
|
||||
"""A malleable representation of a signable HTTP request.
|
||||
|
||||
Body argument may contain any data, but parameters will only be decoded if
|
||||
they are one of:
|
||||
|
||||
* urlencoded query string
|
||||
* dict
|
||||
* list of 2-tuples
|
||||
|
||||
Anything else will be treated as raw body data to be passed through
|
||||
unmolested.
|
||||
"""
|
||||
|
||||
def __init__(self, uri, http_method=u'GET', body=None, headers=None):
|
||||
self.uri = uri
|
||||
self.http_method = http_method
|
||||
self.headers = headers or {}
|
||||
self.body = body
|
||||
self.decoded_body = extract_params(body)
|
||||
self.oauth_params = []
|
||||
|
||||
@property
|
||||
def uri_query(self):
|
||||
return urlparse.urlparse(self.uri).query
|
||||
|
||||
@property
|
||||
def uri_query_params(self):
|
||||
return urlparse.parse_qsl(self.uri_query, keep_blank_values=True,
|
||||
strict_parsing=True)
|
||||
@@ -1,13 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
"""
|
||||
oauthlib.oauth1
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is a wrapper for the most recent implementation of OAuth 1.0 Client
|
||||
and Server classes.
|
||||
"""
|
||||
|
||||
from .rfc5849 import Client, Server
|
||||
|
||||
@@ -1,889 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for signing and checking OAuth 1.0 RFC 5849 requests.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
import urlparse
|
||||
|
||||
from oauthlib.common import Request, urlencode, generate_nonce
|
||||
from oauthlib.common import generate_timestamp
|
||||
from . import parameters, signature, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SIGNATURE_HMAC = u"HMAC-SHA1"
|
||||
SIGNATURE_RSA = u"RSA-SHA1"
|
||||
SIGNATURE_PLAINTEXT = u"PLAINTEXT"
|
||||
SIGNATURE_METHODS = (SIGNATURE_HMAC, SIGNATURE_RSA, SIGNATURE_PLAINTEXT)
|
||||
|
||||
SIGNATURE_TYPE_AUTH_HEADER = u'AUTH_HEADER'
|
||||
SIGNATURE_TYPE_QUERY = u'QUERY'
|
||||
SIGNATURE_TYPE_BODY = u'BODY'
|
||||
|
||||
CONTENT_TYPE_FORM_URLENCODED = u'application/x-www-form-urlencoded'
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""A client used to sign OAuth 1.0 RFC 5849 requests"""
|
||||
def __init__(self, client_key,
|
||||
client_secret=None,
|
||||
resource_owner_key=None,
|
||||
resource_owner_secret=None,
|
||||
callback_uri=None,
|
||||
signature_method=SIGNATURE_HMAC,
|
||||
signature_type=SIGNATURE_TYPE_AUTH_HEADER,
|
||||
rsa_key=None, verifier=None):
|
||||
self.client_key = client_key
|
||||
self.client_secret = client_secret
|
||||
self.resource_owner_key = resource_owner_key
|
||||
self.resource_owner_secret = resource_owner_secret
|
||||
self.signature_method = signature_method
|
||||
self.signature_type = signature_type
|
||||
self.callback_uri = callback_uri
|
||||
self.rsa_key = rsa_key
|
||||
self.verifier = verifier
|
||||
|
||||
if self.signature_method == SIGNATURE_RSA and self.rsa_key is None:
|
||||
raise ValueError('rsa_key is required when using RSA signature method.')
|
||||
|
||||
def get_oauth_signature(self, request):
|
||||
"""Get an OAuth signature to be used in signing a request
|
||||
"""
|
||||
if self.signature_method == SIGNATURE_PLAINTEXT:
|
||||
# fast-path
|
||||
return signature.sign_plaintext(self.client_secret,
|
||||
self.resource_owner_secret)
|
||||
|
||||
uri, headers, body = self._render(request)
|
||||
|
||||
collected_params = signature.collect_parameters(
|
||||
uri_query=urlparse.urlparse(uri).query,
|
||||
body=body,
|
||||
headers=headers)
|
||||
logger.debug("Collected params: {0}".format(collected_params))
|
||||
|
||||
normalized_params = signature.normalize_parameters(collected_params)
|
||||
normalized_uri = signature.normalize_base_string_uri(request.uri)
|
||||
logger.debug("Normalized params: {0}".format(normalized_params))
|
||||
logger.debug("Normalized URI: {0}".format(normalized_uri))
|
||||
|
||||
base_string = signature.construct_base_string(request.http_method,
|
||||
normalized_uri, normalized_params)
|
||||
|
||||
logger.debug("Base signing string: {0}".format(base_string))
|
||||
|
||||
if self.signature_method == SIGNATURE_HMAC:
|
||||
sig = signature.sign_hmac_sha1(base_string, self.client_secret,
|
||||
self.resource_owner_secret)
|
||||
elif self.signature_method == SIGNATURE_RSA:
|
||||
sig = signature.sign_rsa_sha1(base_string, self.rsa_key)
|
||||
else:
|
||||
sig = signature.sign_plaintext(self.client_secret,
|
||||
self.resource_owner_secret)
|
||||
|
||||
logger.debug("Signature: {0}".format(sig))
|
||||
return sig
|
||||
|
||||
def get_oauth_params(self):
|
||||
"""Get the basic OAuth parameters to be used in generating a signature.
|
||||
"""
|
||||
params = [
|
||||
(u'oauth_nonce', generate_nonce()),
|
||||
(u'oauth_timestamp', generate_timestamp()),
|
||||
(u'oauth_version', u'1.0'),
|
||||
(u'oauth_signature_method', self.signature_method),
|
||||
(u'oauth_consumer_key', self.client_key),
|
||||
]
|
||||
if self.resource_owner_key:
|
||||
params.append((u'oauth_token', self.resource_owner_key))
|
||||
if self.callback_uri:
|
||||
params.append((u'oauth_callback', self.callback_uri))
|
||||
if self.verifier:
|
||||
params.append((u'oauth_verifier', self.verifier))
|
||||
|
||||
return params
|
||||
|
||||
def _render(self, request, formencode=False):
|
||||
"""Render a signed request according to signature type
|
||||
|
||||
Returns a 3-tuple containing the request URI, headers, and body.
|
||||
|
||||
If the formencode argument is True and the body contains parameters, it
|
||||
is escaped and returned as a valid formencoded string.
|
||||
"""
|
||||
# TODO what if there are body params on a header-type auth?
|
||||
# TODO what if there are query params on a body-type auth?
|
||||
|
||||
uri, headers, body = request.uri, request.headers, request.body
|
||||
|
||||
# TODO: right now these prepare_* methods are very narrow in scope--they
|
||||
# only affect their little thing. In some cases (for example, with
|
||||
# header auth) it might be advantageous to allow these methods to touch
|
||||
# other parts of the request, like the headers—so the prepare_headers
|
||||
# method could also set the Content-Type header to x-www-form-urlencoded
|
||||
# like the spec requires. This would be a fundamental change though, and
|
||||
# I'm not sure how I feel about it.
|
||||
if self.signature_type == SIGNATURE_TYPE_AUTH_HEADER:
|
||||
headers = parameters.prepare_headers(request.oauth_params, request.headers)
|
||||
elif self.signature_type == SIGNATURE_TYPE_BODY and request.decoded_body is not None:
|
||||
body = parameters.prepare_form_encoded_body(request.oauth_params, request.decoded_body)
|
||||
if formencode:
|
||||
body = urlencode(body)
|
||||
headers['Content-Type'] = u'application/x-www-form-urlencoded'
|
||||
elif self.signature_type == SIGNATURE_TYPE_QUERY:
|
||||
uri = parameters.prepare_request_uri_query(request.oauth_params, request.uri)
|
||||
else:
|
||||
raise ValueError('Unknown signature type specified.')
|
||||
|
||||
return uri, headers, body
|
||||
|
||||
def sign(self, uri, http_method=u'GET', body=None, headers=None):
|
||||
"""Sign a request
|
||||
|
||||
Signs an HTTP request with the specified parts.
|
||||
|
||||
Returns a 3-tuple of the signed request's URI, headers, and body.
|
||||
Note that http_method is not returned as it is unaffected by the OAuth
|
||||
signing process.
|
||||
|
||||
The body argument may be a dict, a list of 2-tuples, or a formencoded
|
||||
string. The Content-Type header must be 'application/x-www-form-urlencoded'
|
||||
if it is present.
|
||||
|
||||
If the body argument is not one of the above, it will be returned
|
||||
verbatim as it is unaffected by the OAuth signing process. Attempting to
|
||||
sign a request with non-formencoded data using the OAuth body signature
|
||||
type is invalid and will raise an exception.
|
||||
|
||||
If the body does contain parameters, it will be returned as a properly-
|
||||
formatted formencoded string.
|
||||
|
||||
All string data MUST be unicode. This includes strings inside body
|
||||
dicts, for example.
|
||||
"""
|
||||
# normalize request data
|
||||
request = Request(uri, http_method, body, headers)
|
||||
|
||||
# sanity check
|
||||
content_type = request.headers.get('Content-Type', None)
|
||||
multipart = content_type and content_type.startswith('multipart/')
|
||||
should_have_params = content_type == CONTENT_TYPE_FORM_URLENCODED
|
||||
has_params = request.decoded_body is not None
|
||||
# 3.4.1.3.1. Parameter Sources
|
||||
# [Parameters are collected from the HTTP request entity-body, but only
|
||||
# if [...]:
|
||||
# * The entity-body is single-part.
|
||||
if multipart and has_params:
|
||||
raise ValueError("Headers indicate a multipart body but body contains parameters.")
|
||||
# * The entity-body follows the encoding requirements of the
|
||||
# "application/x-www-form-urlencoded" content-type as defined by
|
||||
# [W3C.REC-html40-19980424].
|
||||
elif should_have_params and not has_params:
|
||||
raise ValueError("Headers indicate a formencoded body but body was not decodable.")
|
||||
# * The HTTP request entity-header includes the "Content-Type"
|
||||
# header field set to "application/x-www-form-urlencoded".
|
||||
elif not should_have_params and has_params:
|
||||
raise ValueError("Body contains parameters but Content-Type header was not set.")
|
||||
|
||||
# 3.5.2. Form-Encoded Body
|
||||
# Protocol parameters can be transmitted in the HTTP request entity-
|
||||
# body, but only if the following REQUIRED conditions are met:
|
||||
# o The entity-body is single-part.
|
||||
# o The entity-body follows the encoding requirements of the
|
||||
# "application/x-www-form-urlencoded" content-type as defined by
|
||||
# [W3C.REC-html40-19980424].
|
||||
# o The HTTP request entity-header includes the "Content-Type" header
|
||||
# field set to "application/x-www-form-urlencoded".
|
||||
elif self.signature_type == SIGNATURE_TYPE_BODY and not (
|
||||
should_have_params and has_params and not multipart):
|
||||
raise ValueError('Body signatures may only be used with form-urlencoded content')
|
||||
|
||||
# generate the basic OAuth parameters
|
||||
request.oauth_params = self.get_oauth_params()
|
||||
|
||||
# generate the signature
|
||||
request.oauth_params.append((u'oauth_signature', self.get_oauth_signature(request)))
|
||||
|
||||
# render the signed request and return it
|
||||
return self._render(request, formencode=True)
|
||||
|
||||
|
||||
class Server(object):
|
||||
"""A server base class used to verify OAuth 1.0 RFC 5849 requests
|
||||
|
||||
OAuth providers should inherit from Server and implement the methods
|
||||
and properties outlined below. Further details are provided in the
|
||||
documentation for each method and property.
|
||||
|
||||
Methods used to check the format of input parameters. Common tests include
|
||||
length, character set, membership, range or pattern. These tests are
|
||||
referred to as `whitelisting or blacklisting`_. Whitelisting is better
|
||||
but blacklisting can be usefull to spot malicious activity.
|
||||
The following have methods a default implementation:
|
||||
|
||||
- check_client_key
|
||||
- check_request_token
|
||||
- check_access_token
|
||||
- check_nonce
|
||||
- check_verifier
|
||||
- check_realm
|
||||
|
||||
The methods above default to whitelist input parameters, checking that they
|
||||
are alphanumerical and between a minimum and maximum length. Rather than
|
||||
overloading the methods a few properties can be used to configure these
|
||||
methods.
|
||||
|
||||
@ safe_characters -> (character set)
|
||||
@ client_key_length -> (min, max)
|
||||
@ request_token_length -> (min, max)
|
||||
@ access_token_length -> (min, max)
|
||||
@ nonce_length -> (min, max)
|
||||
@ verifier_length -> (min, max)
|
||||
@ realms -> [list, of, realms]
|
||||
|
||||
Methods used to validate input parameters. These checks usually hit either
|
||||
persistent or temporary storage such as databases or the filesystem. See
|
||||
each methods documentation for detailed usage.
|
||||
The following methods must be implemented:
|
||||
|
||||
- validate_client
|
||||
- validate_request_token
|
||||
- validate_access_token
|
||||
- validate_nonce_and_timestamp
|
||||
- validate_redirect_uri
|
||||
- validate_requested_realm
|
||||
- validate_realm
|
||||
- validate_verifier
|
||||
|
||||
Method used to retrieve sensitive information from storage.
|
||||
The following methods must be implemented:
|
||||
|
||||
- get_client_secret
|
||||
- get_request_token_secret
|
||||
- get_access_token_secret
|
||||
- get_rsa_key
|
||||
|
||||
To prevent timing attacks it is necessary to not exit early even if the
|
||||
client key or resource owner key is invalid. Instead dummy values should
|
||||
be used during the remaining verification process. It is very important
|
||||
that the dummy client and token are valid input parameters to the methods
|
||||
get_client_secret, get_rsa_key and get_(access/request)_token_secret and
|
||||
that the running time of those methods when given a dummy value remain
|
||||
equivalent to the running time when given a valid client/resource owner.
|
||||
The following properties must be implemented:
|
||||
|
||||
@ dummy_client
|
||||
@ dummy_request_token
|
||||
@ dummy_access_token
|
||||
|
||||
.. _`whitelisting or blacklisting`: http://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def allowed_signature_methods(self):
|
||||
return SIGNATURE_METHODS
|
||||
|
||||
@property
|
||||
def safe_characters(self):
|
||||
return set(utils.UNICODE_ASCII_CHARACTER_SET)
|
||||
|
||||
@property
|
||||
def client_key_length(self):
|
||||
return 20, 30
|
||||
|
||||
@property
|
||||
def request_token_length(self):
|
||||
return 20, 30
|
||||
|
||||
@property
|
||||
def access_token_length(self):
|
||||
return 20, 30
|
||||
|
||||
@property
|
||||
def timestamp_lifetime(self):
|
||||
return 600
|
||||
|
||||
@property
|
||||
def nonce_length(self):
|
||||
return 20, 30
|
||||
|
||||
@property
|
||||
def verifier_length(self):
|
||||
return 20, 30
|
||||
|
||||
@property
|
||||
def realms(self):
|
||||
return []
|
||||
|
||||
@property
|
||||
def enforce_ssl(self):
|
||||
return True
|
||||
|
||||
def check_client_key(self, client_key):
|
||||
"""Check that the client key only contains safe characters
|
||||
and is no shorter than lower and no longer than upper.
|
||||
"""
|
||||
lower, upper = self.client_key_length
|
||||
return (set(client_key) <= self.safe_characters and
|
||||
lower <= len(client_key) <= upper)
|
||||
|
||||
def check_request_token(self, request_token):
|
||||
"""Checks that the request token contains only safe characters
|
||||
and is no shorter than lower and no longer than upper.
|
||||
"""
|
||||
lower, upper = self.request_token_length
|
||||
return (set(request_token) <= self.safe_characters and
|
||||
lower <= len(request_token) <= upper)
|
||||
|
||||
def check_access_token(self, request_token):
|
||||
"""Checks that the token contains only safe characters
|
||||
and is no shorter than lower and no longer than upper.
|
||||
"""
|
||||
lower, upper = self.access_token_length
|
||||
return (set(request_token) <= self.safe_characters and
|
||||
lower <= len(request_token) <= upper)
|
||||
|
||||
def check_nonce(self, nonce):
|
||||
"""Checks that the nonce only contains only safe characters
|
||||
and is no shorter than lower and no longer than upper.
|
||||
"""
|
||||
lower, upper = self.nonce_length
|
||||
return (set(nonce) <= self.safe_characters and
|
||||
lower <= len(nonce) <= upper)
|
||||
|
||||
def check_verifier(self, verifier):
|
||||
"""Checks that the verifier contains only safe characters
|
||||
and is no shorter than lower and no longer than upper.
|
||||
"""
|
||||
lower, upper = self.verifier_length
|
||||
return (set(verifier) <= self.safe_characters and
|
||||
lower <= len(verifier) <= upper)
|
||||
|
||||
def check_realm(self, realm):
|
||||
"""Check that the realm is one of a set allowed realms.
|
||||
"""
|
||||
return realm in self.realms
|
||||
|
||||
def get_client_secret(self, client_key):
|
||||
"""Retrieves the client secret associated with the client key.
|
||||
|
||||
This method must allow the use of a dummy client_key value.
|
||||
Fetching the secret using the dummy key must take the same amount of
|
||||
time as fetching a secret for a valid client.
|
||||
|
||||
Note that the returned key must be in plaintext.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
@property
|
||||
def dummy_client(self):
|
||||
"""Dummy client used when an invalid client key is supplied.
|
||||
|
||||
The dummy client should be associated with either a client secret,
|
||||
a rsa key or both depending on which signature methods are supported.
|
||||
Providers should make sure that
|
||||
|
||||
get_client_secret(dummy_client)
|
||||
get_rsa_key(dummy_client)
|
||||
|
||||
return a valid secret or key for the dummy client.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def get_request_token_secret(self, client_key, request_token):
|
||||
"""Retrieves the shared secret associated with the request token.
|
||||
|
||||
This method must allow the use of a dummy values and the running time
|
||||
must be roughly equivalent to that of the running time of valid values.
|
||||
|
||||
Note that the returned key must be in plaintext.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def get_access_token_secret(self, client_key, access_token):
|
||||
"""Retrieves the shared secret associated with the access token.
|
||||
|
||||
This method must allow the use of a dummy values and the running time
|
||||
must be roughly equivalent to that of the running time of valid values.
|
||||
|
||||
Note that the returned key must be in plaintext.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
@property
|
||||
def dummy_request_token(self):
|
||||
"""Dummy request token used when an invalid token was supplied.
|
||||
|
||||
The dummy request token should be associated with a request token
|
||||
secret such that get_request_token_secret(.., dummy_request_token)
|
||||
returns a valid secret.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
@property
|
||||
def dummy_access_token(self):
|
||||
"""Dummy access token used when an invalid token was supplied.
|
||||
|
||||
The dummy access token should be associated with an access token
|
||||
secret such that get_access_token_secret(.., dummy_access_token)
|
||||
returns a valid secret.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def get_rsa_key(self, client_key):
|
||||
"""Retrieves a previously stored client provided RSA key.
|
||||
|
||||
This method must allow the use of a dummy client_key value. Fetching
|
||||
the rsa key using the dummy key must take the same aount of time
|
||||
as fetching a key for a valid client.
|
||||
|
||||
Note that the key must be returned in plaintext.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def get_signature_type_and_params(self, request):
|
||||
"""Extracts parameters from query, headers and body. Signature type
|
||||
is set to the source in which parameters were found.
|
||||
"""
|
||||
header_params = signature.collect_parameters(headers=request.headers,
|
||||
exclude_oauth_signature=False)
|
||||
body_params = signature.collect_parameters(body=request.body,
|
||||
exclude_oauth_signature=False)
|
||||
query_params = signature.collect_parameters(uri_query=request.uri_query,
|
||||
exclude_oauth_signature=False)
|
||||
|
||||
params = []
|
||||
params.extend(header_params)
|
||||
params.extend(body_params)
|
||||
params.extend(query_params)
|
||||
signature_types_with_oauth_params = filter(lambda s: s[2], (
|
||||
(SIGNATURE_TYPE_AUTH_HEADER, params,
|
||||
utils.filter_oauth_params(header_params)),
|
||||
(SIGNATURE_TYPE_BODY, params,
|
||||
utils.filter_oauth_params(body_params)),
|
||||
(SIGNATURE_TYPE_QUERY, params,
|
||||
utils.filter_oauth_params(query_params))
|
||||
))
|
||||
|
||||
if len(signature_types_with_oauth_params) > 1:
|
||||
raise ValueError('oauth_ params must come from only 1 signature type but were found in %s' % ', '.join(
|
||||
[s[0] for s in signature_types_with_oauth_params]))
|
||||
try:
|
||||
signature_type, params, oauth_params = signature_types_with_oauth_params[0]
|
||||
except IndexError:
|
||||
raise ValueError('oauth_ params are missing. Could not determine signature type.')
|
||||
|
||||
return signature_type, params, oauth_params
|
||||
|
||||
def validate_client_key(self, client_key):
|
||||
"""Validates that supplied client key is a registered and valid client.
|
||||
|
||||
Note that if the dummy client is supplied it should validate in same
|
||||
or nearly the same amount of time as a valid one.
|
||||
|
||||
Bad:
|
||||
|
||||
if client_key == self.dummy_client:
|
||||
return False
|
||||
else:
|
||||
return storage.has_client(client_key)
|
||||
|
||||
Good:
|
||||
|
||||
return storage.has_client(client_key) and client_key != self.dummy_client
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def validate_request_token(self, client_key, request_token):
|
||||
"""Validates that supplied request token is registered and valid.
|
||||
|
||||
Note that if the dummy request_token is supplied it should validate in
|
||||
the same nearly the same amount of time as a valid one.
|
||||
|
||||
Bad:
|
||||
|
||||
if request_token == self.dummy_request_token:
|
||||
return False
|
||||
else:
|
||||
return storage.has_request_token(request_token)
|
||||
|
||||
Good:
|
||||
|
||||
return (storage.has_request_token(request_token) and
|
||||
request_token != self.dummy_request_token)
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def validate_access_token(self, client_key, access_token):
|
||||
"""Validates that supplied access token is registered and valid.
|
||||
|
||||
Note that if the dummy access token is supplied it should validate in
|
||||
the same or nearly the same amount of time as a valid one.
|
||||
|
||||
Bad:
|
||||
|
||||
if access_token == self.dummy_access_token:
|
||||
return False
|
||||
else:
|
||||
return storage.has_access_token(access_token)
|
||||
|
||||
Good:
|
||||
|
||||
return (storage.has_access_token(access_token) and
|
||||
access_token != self.dummy_access_token)
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
|
||||
request_token=None, access_token=None):
|
||||
"""Validates that the nonce has not been used before.
|
||||
|
||||
Per `Section 3.3`_ of the spec.
|
||||
|
||||
"A nonce is a random string, uniquely generated by the client to allow
|
||||
the server to verify that a request has never been made before and
|
||||
helps prevent replay attacks when requests are made over a non-secure
|
||||
channel. The nonce value MUST be unique across all requests with the
|
||||
same timestamp, client credentials, and token combinations."
|
||||
|
||||
.. _`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
|
||||
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def validate_redirect_uri(self, client_key, redirect_uri):
|
||||
"""Validates the client supplied redirection URI.
|
||||
|
||||
It is highly recommended that OAuth providers require their clients
|
||||
to register all redirection URIs prior to using them in requests and
|
||||
register them as absolute URIs. See `CWE-601`_ for more information
|
||||
about open redirection attacks.
|
||||
|
||||
By requiring registration of all redirection URIs it should be
|
||||
straightforward for the provider to verify whether the supplied
|
||||
redirect_uri is valid or not.
|
||||
|
||||
.. _`CWE-601`: http://cwe.mitre.org/top25/index.html#CWE-601
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
|
||||
def validate_requested_realm(self, client_key, realm):
|
||||
"""Validates that the client may request access to the realm.
|
||||
|
||||
This method is invoked when obtaining a request token and should
|
||||
tie a realm to the request token and after user authorization
|
||||
this realm restriction should transfer to the access token.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def validate_realm(self, client_key, access_token, uri=None,
|
||||
required_realm=None):
|
||||
"""Validates access to the request realm.
|
||||
|
||||
How providers choose to use the realm parameter is outside the OAuth
|
||||
specification but it is commonly used to restrict access to a subset
|
||||
of protected resources such as "photos".
|
||||
|
||||
required_realm is a convenience parameter which can be used to provide
|
||||
a per view method pre-defined list of allowed realms.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def validate_verifier(self, client_key, request_token, verifier):
|
||||
"""Validates a verification code.
|
||||
|
||||
OAuth providers issue a verification code to clients after the
|
||||
resource owner authorizes access. This code is used by the client to
|
||||
obtain token credentials and the provider must verify that the
|
||||
verifier is valid and associated with the client as well as the
|
||||
resource owner.
|
||||
"""
|
||||
raise NotImplementedError("Subclasses must implement this function.")
|
||||
|
||||
def verify_request(self, uri, http_method=u'GET', body=None,
|
||||
headers=None, require_resource_owner=True, require_verifier=False,
|
||||
require_realm=False, required_realm=None):
|
||||
"""Verifies a request ensuring that the following is true:
|
||||
|
||||
Per `section 3.2`_ of the spec.
|
||||
|
||||
- all mandated OAuth parameters are supplied
|
||||
- parameters are only supplied in one source which may be the URI
|
||||
query, the Authorization header or the body
|
||||
- all parameters are checked and validated, see comments and the
|
||||
methods and properties of this class for further details.
|
||||
- the supplied signature is verified against a recalculated one
|
||||
|
||||
A ValueError will be raised if any parameter is missing,
|
||||
supplied twice or invalid. A HTTP 400 Response should be returned
|
||||
upon catching an exception.
|
||||
|
||||
A HTTP 401 Response should be returned if verify_request returns False.
|
||||
|
||||
`Timing attacks`_ are prevented through the use of dummy credentials to
|
||||
create near constant time verification even if an invalid credential
|
||||
is used. Early exit on invalid credentials would enable attackers
|
||||
to perform `enumeration attacks`_. Near constant time string comparison
|
||||
is used to prevent secret key guessing. Note that timing attacks can
|
||||
only be prevented through near constant time execution, not by adding
|
||||
a random delay which would only require more samples to be gathered.
|
||||
|
||||
.. _`section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2
|
||||
.. _`Timing attacks`: http://rdist.root.org/2010/07/19/exploiting-remote-timing-attacks/
|
||||
.. _`enumeration attacks`: http://www.sans.edu/research/security-laboratory/article/attacks-browsing
|
||||
"""
|
||||
# Only include body data from x-www-form-urlencoded requests
|
||||
headers = headers or {}
|
||||
if (u"Content-Type" in headers and
|
||||
headers[u"Content-Type"] == CONTENT_TYPE_FORM_URLENCODED):
|
||||
request = Request(uri, http_method, body, headers)
|
||||
else:
|
||||
request = Request(uri, http_method, u'', headers)
|
||||
|
||||
if self.enforce_ssl and not request.uri.lower().startswith("https://"):
|
||||
raise ValueError("Insecure transport, only HTTPS is allowed.")
|
||||
|
||||
signature_type, params, oauth_params = self.get_signature_type_and_params(request)
|
||||
|
||||
# The server SHOULD return a 400 (Bad Request) status code when
|
||||
# receiving a request with duplicated protocol parameters.
|
||||
if len(dict(oauth_params)) != len(oauth_params):
|
||||
raise ValueError("Duplicate OAuth entries.")
|
||||
|
||||
oauth_params = dict(oauth_params)
|
||||
request_signature = oauth_params.get(u'oauth_signature')
|
||||
client_key = oauth_params.get(u'oauth_consumer_key')
|
||||
resource_owner_key = oauth_params.get(u'oauth_token')
|
||||
nonce = oauth_params.get(u'oauth_nonce')
|
||||
timestamp = oauth_params.get(u'oauth_timestamp')
|
||||
callback_uri = oauth_params.get(u'oauth_callback')
|
||||
verifier = oauth_params.get(u'oauth_verifier')
|
||||
signature_method = oauth_params.get(u'oauth_signature_method')
|
||||
realm = dict(params).get(u'realm')
|
||||
|
||||
# The server SHOULD return a 400 (Bad Request) status code when
|
||||
# receiving a request with missing parameters.
|
||||
if not all((request_signature, client_key, nonce,
|
||||
timestamp, signature_method)):
|
||||
raise ValueError("Missing OAuth parameters.")
|
||||
|
||||
# OAuth does not mandate a particular signature method, as each
|
||||
# implementation can have its own unique requirements. Servers are
|
||||
# free to implement and document their own custom methods.
|
||||
# Recommending any particular method is beyond the scope of this
|
||||
# specification. Implementers should review the Security
|
||||
# Considerations section (`Section 4`_) before deciding on which
|
||||
# method to support.
|
||||
# .. _`Section 4`: http://tools.ietf.org/html/rfc5849#section-4
|
||||
if not signature_method in self.allowed_signature_methods:
|
||||
raise ValueError("Invalid signature method.")
|
||||
|
||||
# Servers receiving an authenticated request MUST validate it by:
|
||||
# If the "oauth_version" parameter is present, ensuring its value is
|
||||
# "1.0".
|
||||
if u'oauth_version' in oauth_params and oauth_params[u'oauth_version'] != u'1.0':
|
||||
raise ValueError("Invalid OAuth version.")
|
||||
|
||||
# The timestamp value MUST be a positive integer. Unless otherwise
|
||||
# specified by the server's documentation, the timestamp is expressed
|
||||
# in the number of seconds since January 1, 1970 00:00:00 GMT.
|
||||
if len(timestamp) != 10:
|
||||
raise ValueError("Invalid timestamp size")
|
||||
try:
|
||||
ts = int(timestamp)
|
||||
|
||||
except ValueError:
|
||||
raise ValueError("Timestamp must be an integer")
|
||||
|
||||
else:
|
||||
# To avoid the need to retain an infinite number of nonce values for
|
||||
# future checks, servers MAY choose to restrict the time period after
|
||||
# which a request with an old timestamp is rejected.
|
||||
if time.time() - ts > self.timestamp_lifetime:
|
||||
raise ValueError("Request too old, over 10 minutes.")
|
||||
|
||||
# Provider specific validation of parameters, used to enforce
|
||||
# restrictions such as character set and length.
|
||||
if not self.check_client_key(client_key):
|
||||
raise ValueError("Invalid client key.")
|
||||
|
||||
if not resource_owner_key and require_resource_owner:
|
||||
raise ValueError("Missing resource owner.")
|
||||
|
||||
if (require_resource_owner and not require_verifier and
|
||||
not self.check_access_token(resource_owner_key)):
|
||||
raise ValueError("Invalid resource owner key.")
|
||||
|
||||
if (require_resource_owner and require_verifier and
|
||||
not self.check_request_token(resource_owner_key)):
|
||||
raise ValueError("Invalid resource owner key.")
|
||||
|
||||
if not self.check_nonce(nonce):
|
||||
raise ValueError("Invalid nonce.")
|
||||
|
||||
if realm and not self.check_realm(realm):
|
||||
raise ValueError("Invalid realm. Allowed are %s" % self.realms)
|
||||
|
||||
if not verifier and require_verifier:
|
||||
raise ValueError("Missing verifier.")
|
||||
|
||||
if require_verifier and not self.check_verifier(verifier):
|
||||
raise ValueError("Invalid verifier.")
|
||||
|
||||
# Servers receiving an authenticated request MUST validate it by:
|
||||
# If using the "HMAC-SHA1" or "RSA-SHA1" signature methods, ensuring
|
||||
# that the combination of nonce/timestamp/token (if present)
|
||||
# received from the client has not been used before in a previous
|
||||
# request (the server MAY reject requests with stale timestamps as
|
||||
# described in `Section 3.3`_).
|
||||
# .._`Section 3.3`: http://tools.ietf.org/html/rfc5849#section-3.3
|
||||
#
|
||||
# We check this before validating client and resource owner for
|
||||
# increased security and performance, both gained by doing less work.
|
||||
if require_verifier:
|
||||
token = {"request_token": resource_owner_key}
|
||||
else:
|
||||
token = {"access_token": resource_owner_key}
|
||||
if not self.validate_timestamp_and_nonce(client_key, timestamp,
|
||||
nonce, **token):
|
||||
return False
|
||||
|
||||
# The server SHOULD return a 401 (Unauthorized) status code when
|
||||
# receiving a request with invalid client credentials.
|
||||
# Note: This is postponed in order to avoid timing attacks, instead
|
||||
# a dummy client is assigned and used to maintain near constant
|
||||
# time request verification.
|
||||
#
|
||||
# Note that early exit would enable client enumeration
|
||||
valid_client = self.validate_client_key(client_key)
|
||||
if not valid_client:
|
||||
client_key = self.dummy_client
|
||||
|
||||
# Ensure a valid redirection uri is used
|
||||
valid_redirect = self.validate_redirect_uri(client_key, callback_uri)
|
||||
|
||||
# The server SHOULD return a 401 (Unauthorized) status code when
|
||||
# receiving a request with invalid or expired token.
|
||||
# Note: This is postponed in order to avoid timing attacks, instead
|
||||
# a dummy token is assigned and used to maintain near constant
|
||||
# time request verification.
|
||||
#
|
||||
# Note that early exit would enable resource owner enumeration
|
||||
if resource_owner_key:
|
||||
if require_verifier:
|
||||
valid_resource_owner = self.validate_request_token(
|
||||
client_key, resource_owner_key)
|
||||
else:
|
||||
valid_resource_owner = self.validate_access_token(
|
||||
client_key, resource_owner_key)
|
||||
if not valid_resource_owner:
|
||||
resource_owner_key = self.dummy_resource_owner
|
||||
else:
|
||||
valid_resource_owner = True
|
||||
|
||||
# Note that `realm`_ is only used in authorization headers and how
|
||||
# it should be interepreted is not included in the OAuth spec.
|
||||
# However they could be seen as a scope or realm to which the
|
||||
# client has access and as such every client should be checked
|
||||
# to ensure it is authorized access to that scope or realm.
|
||||
# .. _`realm`: http://tools.ietf.org/html/rfc2617#section-1.2
|
||||
#
|
||||
# Note that early exit would enable client realm access enumeration.
|
||||
#
|
||||
# The require_realm indicates this is the first step in the OAuth
|
||||
# workflow where a client requests access to a specific realm.
|
||||
#
|
||||
# Clients obtaining an access token will not supply a realm and it will
|
||||
# not be checked. Instead the previously requested realm should be
|
||||
# transferred from the request token to the access token.
|
||||
#
|
||||
# Access to protected resources will always validate the realm but note
|
||||
# that the realm is now tied to the access token and not provided by
|
||||
# the client.
|
||||
if require_realm and not resource_owner_key:
|
||||
valid_realm = self.validate_requested_realm(client_key, realm)
|
||||
elif require_verifier:
|
||||
valid_realm = True
|
||||
else:
|
||||
valid_realm = self.validate_realm(client_key, resource_owner_key,
|
||||
uri=request.uri, required_realm=required_realm)
|
||||
|
||||
# The server MUST verify (Section 3.2) the validity of the request,
|
||||
# ensure that the resource owner has authorized the provisioning of
|
||||
# token credentials to the client, and ensure that the temporary
|
||||
# credentials have not expired or been used before. The server MUST
|
||||
# also verify the verification code received from the client.
|
||||
# .. _`Section 3.2`: http://tools.ietf.org/html/rfc5849#section-3.2
|
||||
#
|
||||
# Note that early exit would enable resource owner authorization
|
||||
# verifier enumertion.
|
||||
if verifier:
|
||||
valid_verifier = self.validate_verifier(client_key,
|
||||
resource_owner_key, verifier)
|
||||
else:
|
||||
valid_verifier = True
|
||||
|
||||
# Parameters to Client depend on signature method which may vary
|
||||
# for each request. Note that HMAC-SHA1 and PLAINTEXT share parameters
|
||||
|
||||
request.params = filter(lambda x: x[0] != "oauth_signature", params)
|
||||
request.signature = request_signature
|
||||
|
||||
# ---- RSA Signature verification ----
|
||||
if signature_method == SIGNATURE_RSA:
|
||||
# The server verifies the signature per `[RFC3447] section 8.2.2`_
|
||||
# .. _`[RFC3447] section 8.2.2`: http://tools.ietf.org/html/rfc3447#section-8.2.1
|
||||
rsa_key = self.get_rsa_key(client_key)
|
||||
valid_signature = signature.verify_rsa_sha1(request, rsa_key)
|
||||
|
||||
# ---- HMAC or Plaintext Signature verification ----
|
||||
else:
|
||||
# Servers receiving an authenticated request MUST validate it by:
|
||||
# Recalculating the request signature independently as described in
|
||||
# `Section 3.4`_ and comparing it to the value received from the
|
||||
# client via the "oauth_signature" parameter.
|
||||
# .. _`Section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
|
||||
client_secret = self.get_client_secret(client_key)
|
||||
if require_verifier:
|
||||
resource_owner_secret = self.get_request_token_secret(
|
||||
client_key, resource_owner_key)
|
||||
else:
|
||||
resource_owner_secret = self.get_access_token_secret(
|
||||
client_key, resource_owner_key)
|
||||
|
||||
if signature_method == SIGNATURE_HMAC:
|
||||
valid_signature = signature.verify_hmac_sha1(request,
|
||||
client_secret, resource_owner_secret)
|
||||
else:
|
||||
valid_signature = signature.verify_plaintext(request,
|
||||
client_secret, resource_owner_secret)
|
||||
|
||||
# We delay checking validity until the very end, using dummy values for
|
||||
# calculations and fetching secrets/keys to ensure the flow of every
|
||||
# request remains almost identical regardless of whether valid values
|
||||
# have been supplied. This ensures near constant time execution and
|
||||
# prevents malicious users from guessing sensitive information
|
||||
v = all((valid_client, valid_resource_owner, valid_realm,
|
||||
valid_redirect, valid_verifier, valid_signature))
|
||||
logger = logging.getLogger("oauthlib")
|
||||
if not v:
|
||||
logger.info("[Failure] OAuthLib request verification failed.")
|
||||
logger.info("Valid client:\t%s" % valid_client)
|
||||
logger.info("Valid token:\t%s\t(Required: %s" % (valid_resource_owner, require_resource_owner))
|
||||
logger.info("Valid realm:\t%s\t(Required: %s)" % (valid_realm, require_realm))
|
||||
logger.info("Valid callback:\t%s" % valid_redirect)
|
||||
logger.info("Valid verifier:\t%s\t(Required: %s)" % (valid_verifier, require_verifier))
|
||||
logger.info("Valid signature:\t%s" % valid_signature)
|
||||
return v
|
||||
@@ -1,134 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
"""
|
||||
oauthlib.parameters
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec.
|
||||
|
||||
.. _`section 3.5`: http://tools.ietf.org/html/rfc5849#section-3.5
|
||||
"""
|
||||
|
||||
from urlparse import urlparse, urlunparse
|
||||
from . import utils
|
||||
from oauthlib.common import extract_params, urlencode
|
||||
|
||||
|
||||
# TODO: do we need filter_params now that oauth_params are handled by Request?
|
||||
# We can easily pass in just oauth protocol params.
|
||||
@utils.filter_params
|
||||
def prepare_headers(oauth_params, headers=None, realm=None):
|
||||
"""**Prepare the Authorization header.**
|
||||
Per `section 3.5.1`_ of the spec.
|
||||
|
||||
Protocol parameters can be transmitted using the HTTP "Authorization"
|
||||
header field as defined by `RFC2617`_ with the auth-scheme name set to
|
||||
"OAuth" (case insensitive).
|
||||
|
||||
For example::
|
||||
|
||||
Authorization: OAuth realm="Example",
|
||||
oauth_consumer_key="0685bd9184jfhq22",
|
||||
oauth_token="ad180jjd733klru7",
|
||||
oauth_signature_method="HMAC-SHA1",
|
||||
oauth_signature="wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
|
||||
oauth_timestamp="137131200",
|
||||
oauth_nonce="4572616e48616d6d65724c61686176",
|
||||
oauth_version="1.0"
|
||||
|
||||
|
||||
.. _`section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
|
||||
.. _`RFC2617`: http://tools.ietf.org/html/rfc2617
|
||||
"""
|
||||
headers = headers or {}
|
||||
|
||||
# Protocol parameters SHALL be included in the "Authorization" header
|
||||
# field as follows:
|
||||
authorization_header_parameters_parts = []
|
||||
for oauth_parameter_name, value in oauth_params:
|
||||
# 1. Parameter names and values are encoded per Parameter Encoding
|
||||
# (`Section 3.6`_)
|
||||
#
|
||||
# .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
|
||||
escaped_name = utils.escape(oauth_parameter_name)
|
||||
escaped_value = utils.escape(value)
|
||||
|
||||
# 2. Each parameter's name is immediately followed by an "=" character
|
||||
# (ASCII code 61), a """ character (ASCII code 34), the parameter
|
||||
# value (MAY be empty), and another """ character (ASCII code 34).
|
||||
part = u'{0}="{1}"'.format(escaped_name, escaped_value)
|
||||
|
||||
authorization_header_parameters_parts.append(part)
|
||||
|
||||
# 3. Parameters are separated by a "," character (ASCII code 44) and
|
||||
# OPTIONAL linear whitespace per `RFC2617`_.
|
||||
#
|
||||
# .. _`RFC2617`: http://tools.ietf.org/html/rfc2617
|
||||
authorization_header_parameters = ', '.join(
|
||||
authorization_header_parameters_parts)
|
||||
|
||||
# 4. The OPTIONAL "realm" parameter MAY be added and interpreted per
|
||||
# `RFC2617 section 1.2`_.
|
||||
#
|
||||
# .. _`RFC2617 section 1.2`: http://tools.ietf.org/html/rfc2617#section-1.2
|
||||
if realm:
|
||||
# NOTE: realm should *not* be escaped
|
||||
authorization_header_parameters = (u'realm="%s", ' % realm +
|
||||
authorization_header_parameters)
|
||||
|
||||
# the auth-scheme name set to "OAuth" (case insensitive).
|
||||
authorization_header = u'OAuth %s' % authorization_header_parameters
|
||||
|
||||
# contribute the Authorization header to the given headers
|
||||
full_headers = {}
|
||||
full_headers.update(headers)
|
||||
full_headers[u'Authorization'] = authorization_header
|
||||
return full_headers
|
||||
|
||||
|
||||
def _append_params(oauth_params, params):
|
||||
"""Append OAuth params to an existing set of parameters.
|
||||
|
||||
Both params and oauth_params is must be lists of 2-tuples.
|
||||
|
||||
Per `section 3.5.2`_ and `3.5.3`_ of the spec.
|
||||
|
||||
.. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
|
||||
.. _`3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
|
||||
|
||||
"""
|
||||
merged = list(params)
|
||||
merged.extend(oauth_params)
|
||||
# The request URI / entity-body MAY include other request-specific
|
||||
# parameters, in which case, the protocol parameters SHOULD be appended
|
||||
# following the request-specific parameters, properly separated by an "&"
|
||||
# character (ASCII code 38)
|
||||
merged.sort(key=lambda i: i[0].startswith('oauth_'))
|
||||
return merged
|
||||
|
||||
|
||||
def prepare_form_encoded_body(oauth_params, body):
|
||||
"""Prepare the Form-Encoded Body.
|
||||
|
||||
Per `section 3.5.2`_ of the spec.
|
||||
|
||||
.. _`section 3.5.2`: http://tools.ietf.org/html/rfc5849#section-3.5.2
|
||||
|
||||
"""
|
||||
# append OAuth params to the existing body
|
||||
return _append_params(oauth_params, body)
|
||||
|
||||
|
||||
def prepare_request_uri_query(oauth_params, uri):
|
||||
"""Prepare the Request URI Query.
|
||||
|
||||
Per `section 3.5.3`_ of the spec.
|
||||
|
||||
.. _`section 3.5.3`: http://tools.ietf.org/html/rfc5849#section-3.5.3
|
||||
|
||||
"""
|
||||
# append OAuth params to the existing set of query components
|
||||
sch, net, path, par, query, fra = urlparse(uri)
|
||||
query = urlencode(_append_params(oauth_params, extract_params(query) or []))
|
||||
return urlunparse((sch, net, path, par, query, fra))
|
||||
@@ -1,551 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849.signature
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of `section 3.4`_ of the spec.
|
||||
|
||||
Terminology:
|
||||
* Client: software interfacing with an OAuth API
|
||||
* Server: the API provider
|
||||
* Resource Owner: the user who is granting authorization to the client
|
||||
|
||||
Steps for signing a request:
|
||||
|
||||
1. Collect parameters from the uri query, auth header, & body
|
||||
2. Normalize those parameters
|
||||
3. Normalize the uri
|
||||
4. Pass the normalized uri, normalized parameters, and http method to
|
||||
construct the base string
|
||||
5. Pass the base string and any keys needed to a signing function
|
||||
|
||||
.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
|
||||
"""
|
||||
import binascii
|
||||
import hashlib
|
||||
import hmac
|
||||
import urlparse
|
||||
from . import utils
|
||||
from oauthlib.common import extract_params, safe_string_equals
|
||||
|
||||
|
||||
def construct_base_string(http_method, base_string_uri,
|
||||
normalized_encoded_request_parameters):
|
||||
"""**String Construction**
|
||||
Per `section 3.4.1.1`_ of the spec.
|
||||
|
||||
For example, the HTTP request::
|
||||
|
||||
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
|
||||
Host: example.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Authorization: OAuth realm="Example",
|
||||
oauth_consumer_key="9djdj82h48djs9d2",
|
||||
oauth_token="kkk9d7dh3k39sjv7",
|
||||
oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="137131201",
|
||||
oauth_nonce="7d8f3e4a",
|
||||
oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
|
||||
|
||||
c2&a3=2+q
|
||||
|
||||
is represented by the following signature base string (line breaks
|
||||
are for display purposes only)::
|
||||
|
||||
POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
|
||||
%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
|
||||
key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
|
||||
ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
|
||||
9d7dh3k39sjv7
|
||||
|
||||
.. _`section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
|
||||
"""
|
||||
|
||||
# The signature base string is constructed by concatenating together,
|
||||
# in order, the following HTTP request elements:
|
||||
|
||||
# 1. The HTTP request method in uppercase. For example: "HEAD",
|
||||
# "GET", "POST", etc. If the request uses a custom HTTP method, it
|
||||
# MUST be encoded (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
|
||||
base_string = utils.escape(http_method.upper())
|
||||
|
||||
# 2. An "&" character (ASCII code 38).
|
||||
base_string += u'&'
|
||||
|
||||
# 3. The base string URI from `Section 3.4.1.2`_, after being encoded
|
||||
# (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
||||
# .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
|
||||
base_string += utils.escape(base_string_uri)
|
||||
|
||||
# 4. An "&" character (ASCII code 38).
|
||||
base_string += u'&'
|
||||
|
||||
# 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after
|
||||
# being encoded (`Section 3.6`).
|
||||
#
|
||||
# .. _`Section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
|
||||
# .. _`Section 3.4.6`: http://tools.ietf.org/html/rfc5849#section-3.4.6
|
||||
base_string += utils.escape(normalized_encoded_request_parameters)
|
||||
|
||||
return base_string
|
||||
|
||||
|
||||
def normalize_base_string_uri(uri):
|
||||
"""**Base String URI**
|
||||
Per `section 3.4.1.2`_ of the spec.
|
||||
|
||||
For example, the HTTP request::
|
||||
|
||||
GET /r%20v/X?id=123 HTTP/1.1
|
||||
Host: EXAMPLE.COM:80
|
||||
|
||||
is represented by the base string URI: "http://example.com/r%20v/X".
|
||||
|
||||
In another example, the HTTPS request::
|
||||
|
||||
GET /?q=1 HTTP/1.1
|
||||
Host: www.example.net:8080
|
||||
|
||||
is represented by the base string URI: "https://www.example.net:8080/".
|
||||
|
||||
.. _`section 3.4.1.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
||||
"""
|
||||
if not isinstance(uri, unicode):
|
||||
raise ValueError('uri must be a unicode object.')
|
||||
|
||||
# FIXME: urlparse does not support unicode
|
||||
scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
|
||||
|
||||
# The scheme, authority, and path of the request resource URI `RFC3986`
|
||||
# are included by constructing an "http" or "https" URI representing
|
||||
# the request resource (without the query or fragment) as follows:
|
||||
#
|
||||
# .. _`RFC2616`: http://tools.ietf.org/html/rfc3986
|
||||
|
||||
# 1. The scheme and host MUST be in lowercase.
|
||||
scheme = scheme.lower()
|
||||
netloc = netloc.lower()
|
||||
|
||||
# 2. The host and port values MUST match the content of the HTTP
|
||||
# request "Host" header field.
|
||||
# TODO: enforce this constraint
|
||||
|
||||
# 3. The port MUST be included if it is not the default port for the
|
||||
# scheme, and MUST be excluded if it is the default. Specifically,
|
||||
# the port MUST be excluded when making an HTTP request `RFC2616`_
|
||||
# to port 80 or when making an HTTPS request `RFC2818`_ to port 443.
|
||||
# All other non-default port numbers MUST be included.
|
||||
#
|
||||
# .. _`RFC2616`: http://tools.ietf.org/html/rfc2616
|
||||
# .. _`RFC2818`: http://tools.ietf.org/html/rfc2818
|
||||
default_ports = (
|
||||
(u'http', u'80'),
|
||||
(u'https', u'443'),
|
||||
)
|
||||
if u':' in netloc:
|
||||
host, port = netloc.split(u':', 1)
|
||||
if (scheme, port) in default_ports:
|
||||
netloc = host
|
||||
|
||||
return urlparse.urlunparse((scheme, netloc, path, u'', u'', u''))
|
||||
|
||||
|
||||
# ** Request Parameters **
|
||||
#
|
||||
# Per `section 3.4.1.3`_ of the spec.
|
||||
#
|
||||
# In order to guarantee a consistent and reproducible representation of
|
||||
# the request parameters, the parameters are collected and decoded to
|
||||
# their original decoded form. They are then sorted and encoded in a
|
||||
# particular manner that is often different from their original
|
||||
# encoding scheme, and concatenated into a single string.
|
||||
#
|
||||
# .. _`section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
|
||||
|
||||
def collect_parameters(uri_query='', body=[], headers=None,
|
||||
exclude_oauth_signature=True):
|
||||
"""**Parameter Sources**
|
||||
|
||||
Parameters starting with `oauth_` will be unescaped.
|
||||
|
||||
Body parameters must be supplied as a dict, a list of 2-tuples, or a
|
||||
formencoded query string.
|
||||
|
||||
Headers must be supplied as a dict.
|
||||
|
||||
Per `section 3.4.1.3.1`_ of the spec.
|
||||
|
||||
For example, the HTTP request::
|
||||
|
||||
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
|
||||
Host: example.com
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
Authorization: OAuth realm="Example",
|
||||
oauth_consumer_key="9djdj82h48djs9d2",
|
||||
oauth_token="kkk9d7dh3k39sjv7",
|
||||
oauth_signature_method="HMAC-SHA1",
|
||||
oauth_timestamp="137131201",
|
||||
oauth_nonce="7d8f3e4a",
|
||||
oauth_signature="djosJKDKJSD8743243%2Fjdk33klY%3D"
|
||||
|
||||
c2&a3=2+q
|
||||
|
||||
contains the following (fully decoded) parameters used in the
|
||||
signature base sting::
|
||||
|
||||
+------------------------+------------------+
|
||||
| Name | Value |
|
||||
+------------------------+------------------+
|
||||
| b5 | =%3D |
|
||||
| a3 | a |
|
||||
| c@ | |
|
||||
| a2 | r b |
|
||||
| oauth_consumer_key | 9djdj82h48djs9d2 |
|
||||
| oauth_token | kkk9d7dh3k39sjv7 |
|
||||
| oauth_signature_method | HMAC-SHA1 |
|
||||
| oauth_timestamp | 137131201 |
|
||||
| oauth_nonce | 7d8f3e4a |
|
||||
| c2 | |
|
||||
| a3 | 2 q |
|
||||
+------------------------+------------------+
|
||||
|
||||
Note that the value of "b5" is "=%3D" and not "==". Both "c@" and
|
||||
"c2" have empty values. While the encoding rules specified in this
|
||||
specification for the purpose of constructing the signature base
|
||||
string exclude the use of a "+" character (ASCII code 43) to
|
||||
represent an encoded space character (ASCII code 32), this practice
|
||||
is widely used in "application/x-www-form-urlencoded" encoded values,
|
||||
and MUST be properly decoded, as demonstrated by one of the "a3"
|
||||
parameter instances (the "a3" parameter is used twice in this
|
||||
request).
|
||||
|
||||
.. _`section 3.4.1.3.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
|
||||
"""
|
||||
headers = headers or {}
|
||||
params = []
|
||||
|
||||
# The parameters from the following sources are collected into a single
|
||||
# list of name/value pairs:
|
||||
|
||||
# * The query component of the HTTP request URI as defined by
|
||||
# `RFC3986, Section 3.4`_. The query component is parsed into a list
|
||||
# of name/value pairs by treating it as an
|
||||
# "application/x-www-form-urlencoded" string, separating the names
|
||||
# and values and decoding them as defined by
|
||||
# `W3C.REC-html40-19980424`_, Section 17.13.4.
|
||||
#
|
||||
# .. _`RFC3986, Section 3.4`: http://tools.ietf.org/html/rfc3986#section-3.4
|
||||
# .. _`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
|
||||
if uri_query:
|
||||
params.extend(urlparse.parse_qsl(uri_query, keep_blank_values=True))
|
||||
|
||||
# * The OAuth HTTP "Authorization" header field (`Section 3.5.1`_) if
|
||||
# present. The header's content is parsed into a list of name/value
|
||||
# pairs excluding the "realm" parameter if present. The parameter
|
||||
# values are decoded as defined by `Section 3.5.1`_.
|
||||
#
|
||||
# .. _`Section 3.5.1`: http://tools.ietf.org/html/rfc5849#section-3.5.1
|
||||
if headers:
|
||||
headers_lower = dict((k.lower(), v) for k, v in headers.items())
|
||||
authorization_header = headers_lower.get(u'authorization')
|
||||
if authorization_header is not None:
|
||||
params.extend([i for i in utils.parse_authorization_header(
|
||||
authorization_header) if i[0] != u'realm'])
|
||||
|
||||
# * The HTTP request entity-body, but only if all of the following
|
||||
# conditions are met:
|
||||
# * The entity-body is single-part.
|
||||
#
|
||||
# * The entity-body follows the encoding requirements of the
|
||||
# "application/x-www-form-urlencoded" content-type as defined by
|
||||
# `W3C.REC-html40-19980424`_.
|
||||
|
||||
# * The HTTP request entity-header includes the "Content-Type"
|
||||
# header field set to "application/x-www-form-urlencoded".
|
||||
#
|
||||
# .._`W3C.REC-html40-19980424`: http://tools.ietf.org/html/rfc5849#ref-W3C.REC-html40-19980424
|
||||
|
||||
# TODO: enforce header param inclusion conditions
|
||||
bodyparams = extract_params(body) or []
|
||||
params.extend(bodyparams)
|
||||
|
||||
# ensure all oauth params are unescaped
|
||||
unescaped_params = []
|
||||
for k, v in params:
|
||||
if k.startswith(u'oauth_'):
|
||||
v = utils.unescape(v)
|
||||
unescaped_params.append((k, v))
|
||||
|
||||
# The "oauth_signature" parameter MUST be excluded from the signature
|
||||
# base string if present.
|
||||
if exclude_oauth_signature:
|
||||
unescaped_params = filter(lambda i: i[0] != u'oauth_signature',
|
||||
unescaped_params)
|
||||
|
||||
return unescaped_params
|
||||
|
||||
|
||||
def normalize_parameters(params):
|
||||
"""**Parameters Normalization**
|
||||
Per `section 3.4.1.3.2`_ of the spec.
|
||||
|
||||
For example, the list of parameters from the previous section would
|
||||
be normalized as follows:
|
||||
|
||||
Encoded::
|
||||
|
||||
+------------------------+------------------+
|
||||
| Name | Value |
|
||||
+------------------------+------------------+
|
||||
| b5 | %3D%253D |
|
||||
| a3 | a |
|
||||
| c%40 | |
|
||||
| a2 | r%20b |
|
||||
| oauth_consumer_key | 9djdj82h48djs9d2 |
|
||||
| oauth_token | kkk9d7dh3k39sjv7 |
|
||||
| oauth_signature_method | HMAC-SHA1 |
|
||||
| oauth_timestamp | 137131201 |
|
||||
| oauth_nonce | 7d8f3e4a |
|
||||
| c2 | |
|
||||
| a3 | 2%20q |
|
||||
+------------------------+------------------+
|
||||
|
||||
Sorted::
|
||||
|
||||
+------------------------+------------------+
|
||||
| Name | Value |
|
||||
+------------------------+------------------+
|
||||
| a2 | r%20b |
|
||||
| a3 | 2%20q |
|
||||
| a3 | a |
|
||||
| b5 | %3D%253D |
|
||||
| c%40 | |
|
||||
| c2 | |
|
||||
| oauth_consumer_key | 9djdj82h48djs9d2 |
|
||||
| oauth_nonce | 7d8f3e4a |
|
||||
| oauth_signature_method | HMAC-SHA1 |
|
||||
| oauth_timestamp | 137131201 |
|
||||
| oauth_token | kkk9d7dh3k39sjv7 |
|
||||
+------------------------+------------------+
|
||||
|
||||
Concatenated Pairs::
|
||||
|
||||
+-------------------------------------+
|
||||
| Name=Value |
|
||||
+-------------------------------------+
|
||||
| a2=r%20b |
|
||||
| a3=2%20q |
|
||||
| a3=a |
|
||||
| b5=%3D%253D |
|
||||
| c%40= |
|
||||
| c2= |
|
||||
| oauth_consumer_key=9djdj82h48djs9d2 |
|
||||
| oauth_nonce=7d8f3e4a |
|
||||
| oauth_signature_method=HMAC-SHA1 |
|
||||
| oauth_timestamp=137131201 |
|
||||
| oauth_token=kkk9d7dh3k39sjv7 |
|
||||
+-------------------------------------+
|
||||
|
||||
and concatenated together into a single string (line breaks are for
|
||||
display purposes only)::
|
||||
|
||||
a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj
|
||||
dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1
|
||||
&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7
|
||||
|
||||
.. _`section 3.4.1.3.2`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
|
||||
"""
|
||||
|
||||
# The parameters collected in `Section 3.4.1.3`_ are normalized into a
|
||||
# single string as follows:
|
||||
#
|
||||
# .. _`Section 3.4.1.3`: http://tools.ietf.org/html/rfc5849#section-3.4.1.3
|
||||
|
||||
# 1. First, the name and value of each parameter are encoded
|
||||
# (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
|
||||
key_values = [(utils.escape(k), utils.escape(v)) for k, v in params]
|
||||
|
||||
# 2. The parameters are sorted by name, using ascending byte value
|
||||
# ordering. If two or more parameters share the same name, they
|
||||
# are sorted by their value.
|
||||
key_values.sort()
|
||||
|
||||
# 3. The name of each parameter is concatenated to its corresponding
|
||||
# value using an "=" character (ASCII code 61) as a separator, even
|
||||
# if the value is empty.
|
||||
parameter_parts = [u'{0}={1}'.format(k, v) for k, v in key_values]
|
||||
|
||||
# 4. The sorted name/value pairs are concatenated together into a
|
||||
# single string by using an "&" character (ASCII code 38) as
|
||||
# separator.
|
||||
return u'&'.join(parameter_parts)
|
||||
|
||||
|
||||
def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
|
||||
"""**HMAC-SHA1**
|
||||
|
||||
The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature
|
||||
algorithm as defined in `RFC2104`_::
|
||||
|
||||
digest = HMAC-SHA1 (key, text)
|
||||
|
||||
Per `section 3.4.2`_ of the spec.
|
||||
|
||||
.. _`RFC2104`: http://tools.ietf.org/html/rfc2104
|
||||
.. _`section 3.4.2`: http://tools.ietf.org/html/rfc5849#section-3.4.2
|
||||
"""
|
||||
|
||||
# The HMAC-SHA1 function variables are used in following way:
|
||||
|
||||
# text is set to the value of the signature base string from
|
||||
# `Section 3.4.1.1`_.
|
||||
#
|
||||
# .. _`Section 3.4.1.1`: http://tools.ietf.org/html/rfc5849#section-3.4.1.1
|
||||
text = base_string
|
||||
|
||||
# key is set to the concatenated values of:
|
||||
# 1. The client shared-secret, after being encoded (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
|
||||
key = utils.escape(client_secret or u'')
|
||||
|
||||
# 2. An "&" character (ASCII code 38), which MUST be included
|
||||
# even when either secret is empty.
|
||||
key += u'&'
|
||||
|
||||
# 3. The token shared-secret, after being encoded (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
|
||||
key += utils.escape(resource_owner_secret or u'')
|
||||
|
||||
# FIXME: HMAC does not support unicode!
|
||||
key_utf8 = key.encode('utf-8')
|
||||
text_utf8 = text.encode('utf-8')
|
||||
signature = hmac.new(key_utf8, text_utf8, hashlib.sha1)
|
||||
|
||||
# digest is used to set the value of the "oauth_signature" protocol
|
||||
# parameter, after the result octet string is base64-encoded
|
||||
# per `RFC2045, Section 6.8`.
|
||||
#
|
||||
# .. _`RFC2045, Section 6.8`: http://tools.ietf.org/html/rfc2045#section-6.8
|
||||
return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
|
||||
|
||||
|
||||
def sign_rsa_sha1(base_string, rsa_private_key):
|
||||
"""**RSA-SHA1**
|
||||
|
||||
Per `section 3.4.3`_ of the spec.
|
||||
|
||||
The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature
|
||||
algorithm as defined in `RFC3447, Section 8.2`_ (also known as
|
||||
PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5. To
|
||||
use this method, the client MUST have established client credentials
|
||||
with the server that included its RSA public key (in a manner that is
|
||||
beyond the scope of this specification).
|
||||
|
||||
NOTE: this method requires the python-rsa library.
|
||||
|
||||
.. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
|
||||
.. _`RFC3447, Section 8.2`: http://tools.ietf.org/html/rfc3447#section-8.2
|
||||
|
||||
"""
|
||||
# TODO: finish RSA documentation
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Hash import SHA
|
||||
key = RSA.importKey(rsa_private_key)
|
||||
h = SHA.new(base_string)
|
||||
p = PKCS1_v1_5.new(key)
|
||||
return binascii.b2a_base64(p.sign(h))[:-1].decode('utf-8')
|
||||
|
||||
|
||||
def sign_plaintext(client_secret, resource_owner_secret):
|
||||
"""Sign a request using plaintext.
|
||||
|
||||
Per `section 3.4.4`_ of the spec.
|
||||
|
||||
The "PLAINTEXT" method does not employ a signature algorithm. It
|
||||
MUST be used with a transport-layer mechanism such as TLS or SSL (or
|
||||
sent over a secure channel with equivalent protections). It does not
|
||||
utilize the signature base string or the "oauth_timestamp" and
|
||||
"oauth_nonce" parameters.
|
||||
|
||||
.. _`section 3.4.4`: http://tools.ietf.org/html/rfc5849#section-3.4.4
|
||||
|
||||
"""
|
||||
|
||||
# The "oauth_signature" protocol parameter is set to the concatenated
|
||||
# value of:
|
||||
|
||||
# 1. The client shared-secret, after being encoded (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
|
||||
signature = utils.escape(client_secret or u'')
|
||||
|
||||
# 2. An "&" character (ASCII code 38), which MUST be included even
|
||||
# when either secret is empty.
|
||||
signature += u'&'
|
||||
|
||||
# 3. The token shared-secret, after being encoded (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
|
||||
signature += utils.escape(resource_owner_secret or u'')
|
||||
|
||||
return signature
|
||||
|
||||
|
||||
def verify_hmac_sha1(request, client_secret=None,
|
||||
resource_owner_secret=None):
|
||||
"""Verify a HMAC-SHA1 signature.
|
||||
|
||||
Per `section 3.4`_ of the spec.
|
||||
|
||||
.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
|
||||
"""
|
||||
norm_params = normalize_parameters(request.params)
|
||||
uri = normalize_base_string_uri(request.uri)
|
||||
base_string = construct_base_string(request.http_method, uri, norm_params)
|
||||
signature = sign_hmac_sha1(base_string, client_secret,
|
||||
resource_owner_secret)
|
||||
return safe_string_equals(signature, request.signature)
|
||||
|
||||
|
||||
def verify_rsa_sha1(request, rsa_public_key):
|
||||
"""Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature.
|
||||
|
||||
Per `section 3.4.3`_ of the spec.
|
||||
|
||||
Note this method requires the PyCrypto library.
|
||||
|
||||
.. _`section 3.4.3`: http://tools.ietf.org/html/rfc5849#section-3.4.3
|
||||
|
||||
"""
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Hash import SHA
|
||||
key = RSA.importKey(rsa_public_key)
|
||||
norm_params = normalize_parameters(request.params)
|
||||
uri = normalize_base_string_uri(request.uri)
|
||||
message = construct_base_string(request.http_method, uri, norm_params)
|
||||
h = SHA.new(message)
|
||||
p = PKCS1_v1_5.new(key)
|
||||
sig = binascii.a2b_base64(request.signature)
|
||||
return p.verify(h, sig)
|
||||
|
||||
|
||||
def verify_plaintext(request, client_secret=None, resource_owner_secret=None):
|
||||
"""Verify a PLAINTEXT signature.
|
||||
|
||||
Per `section 3.4`_ of the spec.
|
||||
|
||||
.. _`section 3.4`: http://tools.ietf.org/html/rfc5849#section-3.4
|
||||
"""
|
||||
signature = sign_plaintext(client_secret, resource_owner_secret)
|
||||
return safe_string_equals(signature, request.signature)
|
||||
@@ -1,99 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
oauthlib.utils
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module contains utility methods used by various parts of the OAuth
|
||||
spec.
|
||||
"""
|
||||
|
||||
import string
|
||||
import urllib2
|
||||
|
||||
from oauthlib.common import quote, unquote
|
||||
|
||||
UNICODE_ASCII_CHARACTER_SET = (string.ascii_letters.decode('ascii') +
|
||||
string.digits.decode('ascii'))
|
||||
|
||||
|
||||
def filter_params(target):
|
||||
"""Decorator which filters params to remove non-oauth_* parameters
|
||||
|
||||
Assumes the decorated method takes a params dict or list of tuples as its
|
||||
first argument.
|
||||
"""
|
||||
def wrapper(params, *args, **kwargs):
|
||||
params = filter_oauth_params(params)
|
||||
return target(params, *args, **kwargs)
|
||||
|
||||
wrapper.__doc__ = target.__doc__
|
||||
return wrapper
|
||||
|
||||
|
||||
def filter_oauth_params(params):
|
||||
"""Removes all non oauth parameters from a dict or a list of params."""
|
||||
is_oauth = lambda kv: kv[0].startswith(u"oauth_")
|
||||
if isinstance(params, dict):
|
||||
return filter(is_oauth, params.items())
|
||||
else:
|
||||
return filter(is_oauth, params)
|
||||
|
||||
|
||||
def escape(u):
|
||||
"""Escape a unicode string in an OAuth-compatible fashion.
|
||||
|
||||
Per `section 3.6`_ of the spec.
|
||||
|
||||
.. _`section 3.6`: http://tools.ietf.org/html/rfc5849#section-3.6
|
||||
|
||||
"""
|
||||
if not isinstance(u, unicode):
|
||||
raise ValueError('Only unicode objects are escapable.')
|
||||
# Letters, digits, and the characters '_.-' are already treated as safe
|
||||
# by urllib.quote(). We need to add '~' to fully support rfc5849.
|
||||
return quote(u, safe='~')
|
||||
|
||||
|
||||
def unescape(u):
|
||||
if not isinstance(u, unicode):
|
||||
raise ValueError('Only unicode objects are unescapable.')
|
||||
return unquote(u)
|
||||
|
||||
|
||||
def urlencode(query):
|
||||
"""Encode a sequence of two-element tuples or dictionary into a URL query string.
|
||||
|
||||
Operates using an OAuth-safe escape() method, in contrast to urllib.urlencode.
|
||||
"""
|
||||
# Convert dictionaries to list of tuples
|
||||
if isinstance(query, dict):
|
||||
query = query.items()
|
||||
return u"&".join([u'='.join([escape(k), escape(v)]) for k, v in query])
|
||||
|
||||
|
||||
def parse_keqv_list(l):
|
||||
"""A unicode-safe version of urllib2.parse_keqv_list"""
|
||||
encoded_list = [u.encode('utf-8') for u in l]
|
||||
encoded_parsed = urllib2.parse_keqv_list(encoded_list)
|
||||
return dict((k.decode('utf-8'),
|
||||
v.decode('utf-8')) for k, v in encoded_parsed.items())
|
||||
|
||||
|
||||
def parse_http_list(u):
|
||||
"""A unicode-safe version of urllib2.parse_http_list"""
|
||||
encoded_str = u.encode('utf-8')
|
||||
encoded_list = urllib2.parse_http_list(encoded_str)
|
||||
return [s.decode('utf-8') for s in encoded_list]
|
||||
|
||||
|
||||
def parse_authorization_header(authorization_header):
|
||||
"""Parse an OAuth authorization header into a list of 2-tuples"""
|
||||
auth_scheme = u'OAuth '
|
||||
if authorization_header.startswith(auth_scheme):
|
||||
authorization_header = authorization_header.replace(auth_scheme, u'', 1)
|
||||
items = parse_http_list(authorization_header)
|
||||
try:
|
||||
return parse_keqv_list(items).items()
|
||||
except ValueError:
|
||||
raise ValueError('Malformed authorization header')
|
||||
@@ -1,13 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import
|
||||
|
||||
"""
|
||||
oauthlib.oauth2
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is a wrapper for the most recent implementation of OAuth 2.0 Client
|
||||
and Server classes.
|
||||
"""
|
||||
|
||||
from .draft25 import Client, Server
|
||||
|
||||
@@ -1,497 +0,0 @@
|
||||
"""
|
||||
oauthlib.oauth2.draft_25
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for signing and checking OAuth 2.0 draft 25 requests.
|
||||
"""
|
||||
from tokens import prepare_bearer_uri, prepare_bearer_headers
|
||||
from tokens import prepare_bearer_body, prepare_mac_header
|
||||
from parameters import prepare_grant_uri, prepare_token_request
|
||||
from parameters import parse_authorization_code_response
|
||||
from parameters import parse_implicit_response, parse_token_response
|
||||
|
||||
|
||||
AUTH_HEADER = u'auth_header'
|
||||
URI_QUERY = u'query'
|
||||
BODY = u'body'
|
||||
|
||||
|
||||
class Client(object):
|
||||
|
||||
def __init__(self, client_id,
|
||||
default_redirect_uri=None,
|
||||
token_type=None,
|
||||
access_token=None,
|
||||
refresh_token=None):
|
||||
"""Initialize a client with commonly used attributes."""
|
||||
|
||||
self.client_id = client_id
|
||||
self.default_redirect_uri = default_redirect_uri
|
||||
self.token_type = token_type
|
||||
self.access_token = access_token
|
||||
self.refresh_token = refresh_token
|
||||
self.token_types = {
|
||||
u'bearer': self._add_bearer_token,
|
||||
u'mac': self._add_mac_token
|
||||
}
|
||||
|
||||
def add_token(self, uri, http_method=u'GET', body=None, headers=None,
|
||||
token_placement=AUTH_HEADER):
|
||||
"""Add token to the request uri, body or authorization header.
|
||||
|
||||
The access token type provides the client with the information
|
||||
required to successfully utilize the access token to make a protected
|
||||
resource request (along with type-specific attributes). The client
|
||||
MUST NOT use an access token if it does not understand the token
|
||||
type.
|
||||
|
||||
For example, the "bearer" token type defined in
|
||||
[I-D.ietf-oauth-v2-bearer] is utilized by simply including the access
|
||||
token string in the request:
|
||||
|
||||
GET /resource/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Authorization: Bearer mF_9.B5f-4.1JqM
|
||||
|
||||
while the "mac" token type defined in [I-D.ietf-oauth-v2-http-mac] is
|
||||
utilized by issuing a MAC key together with the access token which is
|
||||
used to sign certain components of the HTTP requests:
|
||||
|
||||
GET /resource/1 HTTP/1.1
|
||||
Host: example.com
|
||||
Authorization: MAC id="h480djs93hd8",
|
||||
nonce="274312:dj83hs9s",
|
||||
mac="kDZvddkndxvhGRXZhvuDjEWhGeE="
|
||||
|
||||
.. _`I-D.ietf-oauth-v2-bearer`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#ref-I-D.ietf-oauth-v2-bearer
|
||||
.. _`I-D.ietf-oauth-v2-http-mac`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#ref-I-D.ietf-oauth-v2-http-mac
|
||||
"""
|
||||
return self.token_types[self.token_type](uri, http_method, body,
|
||||
headers, token_placement)
|
||||
|
||||
def prepare_refresh_body(self, body=u'', refresh_token=None, scope=None):
|
||||
"""Prepare an access token request, using a refresh token.
|
||||
|
||||
If the authorization server issued a refresh token to the client, the
|
||||
client makes a refresh request to the token endpoint by adding the
|
||||
following parameters using the "application/x-www-form-urlencoded"
|
||||
format in the HTTP request entity-body:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "refresh_token".
|
||||
refresh_token
|
||||
REQUIRED. The refresh token issued to the client.
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3. The requested scope MUST NOT include any scope
|
||||
not originally granted by the resource owner, and if omitted is
|
||||
treated as equal to the scope originally granted by the
|
||||
resource owner.
|
||||
"""
|
||||
refresh_token = refresh_token or self.refresh_token
|
||||
return prepare_token_request(u'refresh_token', body=body, scope=scope,
|
||||
refresh_token=refresh_token)
|
||||
|
||||
def _add_bearer_token(self, uri, http_method=u'GET', body=None,
|
||||
headers=None, token_placement=AUTH_HEADER):
|
||||
"""Add a bearer token to the request uri, body or authorization header."""
|
||||
if token_placement == AUTH_HEADER:
|
||||
headers = prepare_bearer_headers(self.token, headers)
|
||||
|
||||
if token_placement == URI_QUERY:
|
||||
uri = prepare_bearer_uri(self.token, uri)
|
||||
|
||||
if token_placement == BODY:
|
||||
body = prepare_bearer_body(self.token, body)
|
||||
|
||||
return uri, headers, body
|
||||
|
||||
def _add_mac_token(self, uri, http_method=u'GET', body=None,
|
||||
headers=None, token_placement=AUTH_HEADER):
|
||||
"""Add a MAC token to the request authorization header."""
|
||||
headers = prepare_mac_header(self.token, uri, self.key, http_method,
|
||||
headers=headers, body=body, ext=self.ext,
|
||||
hash_algorithm=self.hash_algorithm)
|
||||
return uri, headers, body
|
||||
|
||||
def _populate_attributes(self, response):
|
||||
"""Add commonly used values such as access_token to self."""
|
||||
|
||||
if u'access_token' in response:
|
||||
self.access_token = response.get(u'access_token')
|
||||
|
||||
if u'refresh_token' in response:
|
||||
self.refresh_token = response.get(u'refresh_token')
|
||||
|
||||
if u'token_type' in response:
|
||||
self.token_type = response.get(u'token_type')
|
||||
|
||||
if u'expires_in' in response:
|
||||
self.expires_in = response.get(u'expires_in')
|
||||
|
||||
if u'code' in response:
|
||||
self.code = response.get(u'code')
|
||||
|
||||
def prepare_request_uri(self, *args, **kwargs):
|
||||
"""Abstract method used to create request URIs."""
|
||||
raise NotImplementedError("Must be implemented by inheriting classes.")
|
||||
|
||||
def prepare_request_body(self, *args, **kwargs):
|
||||
"""Abstract method used to create request bodies."""
|
||||
raise NotImplementedError("Must be implemented by inheriting classes.")
|
||||
|
||||
def parse_request_uri_response(self, *args, **kwargs):
|
||||
"""Abstract method used to parse redirection responses."""
|
||||
|
||||
def parse_request_body_response(self, *args, **kwargs):
|
||||
"""Abstract method used to parse JSON responses."""
|
||||
|
||||
|
||||
class WebApplicationClient(Client):
|
||||
"""A client utilizing the authorization code grant workflow.
|
||||
|
||||
A web application is a confidential client running on a web
|
||||
server. Resource owners access the client via an HTML user
|
||||
interface rendered in a user-agent on the device used by the
|
||||
resource owner. The client credentials as well as any access
|
||||
token issued to the client are stored on the web server and are
|
||||
not exposed to or accessible by the resource owner.
|
||||
|
||||
The authorization code grant type is used to obtain both access
|
||||
tokens and refresh tokens and is optimized for confidential clients.
|
||||
As a redirection-based flow, the client must be capable of
|
||||
interacting with the resource owner's user-agent (typically a web
|
||||
browser) and capable of receiving incoming requests (via redirection)
|
||||
from the authorization server.
|
||||
"""
|
||||
|
||||
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
|
||||
state=None, **kwargs):
|
||||
"""Prepare the authorization code request URI
|
||||
|
||||
The client constructs the request URI by adding the following
|
||||
parameters to the query component of the authorization endpoint URI
|
||||
using the "application/x-www-form-urlencoded" format as defined by
|
||||
[`W3C.REC-html401-19991224`_]:
|
||||
|
||||
response_type
|
||||
REQUIRED. Value MUST be set to "code".
|
||||
client_id
|
||||
REQUIRED. The client identifier as described in `Section 2.2`_.
|
||||
redirect_uri
|
||||
OPTIONAL. As described in `Section 3.1.2`_.
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
`Section 3.3`_.
|
||||
state
|
||||
RECOMMENDED. An opaque value used by the client to maintain
|
||||
state between the request and callback. The authorization
|
||||
server includes this value when redirecting the user-agent back
|
||||
to the client. The parameter SHOULD be used for preventing
|
||||
cross-site request forgery as described in `Section 10.12`_.
|
||||
|
||||
.. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#ref-W3C.REC-html401-19991224
|
||||
.. _`Section 2.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-2.2
|
||||
.. _`Section 3.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.1.2
|
||||
.. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3
|
||||
.. _`Section 10.12`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-10.12
|
||||
"""
|
||||
redirect_uri = redirect_uri or self.default_redirect_uri
|
||||
return prepare_grant_uri(uri, self.client_id, u'code',
|
||||
redirect_uri=redirect_uri, scope=scope, state=state, **kwargs)
|
||||
|
||||
def prepare_request_body(self, code, body=u'', redirect_uri=None, **kwargs):
|
||||
"""Prepare the access token request body.
|
||||
|
||||
The client makes a request to the token endpoint by adding the
|
||||
following parameters using the "application/x-www-form-urlencoded"
|
||||
format in the HTTP request entity-body:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "authorization_code".
|
||||
code
|
||||
REQUIRED. The authorization code received from the
|
||||
authorization server.
|
||||
redirect_uri
|
||||
REQUIRED, if the "redirect_uri" parameter was included in the
|
||||
authorization request as described in Section 4.1.1, and their
|
||||
values MUST be identical.
|
||||
|
||||
.. _`Section 4.1.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-4.1.1
|
||||
"""
|
||||
redirect_uri = redirect_uri or self.default_redirect_uri
|
||||
code = code or self.code
|
||||
return prepare_token_request(u'authorization_code', code=code, body=body,
|
||||
redirect_uri=redirect_uri, **kwargs)
|
||||
|
||||
def parse_request_uri_response(self, uri, state=None):
|
||||
"""Parse the URI query for code and state.
|
||||
|
||||
If the resource owner grants the access request, the authorization
|
||||
server issues an authorization code and delivers it to the client by
|
||||
adding the following parameters to the query component of the
|
||||
redirection URI using the "application/x-www-form-urlencoded" format:
|
||||
|
||||
code
|
||||
REQUIRED. The authorization code generated by the
|
||||
authorization server. The authorization code MUST expire
|
||||
shortly after it is issued to mitigate the risk of leaks. A
|
||||
maximum authorization code lifetime of 10 minutes is
|
||||
RECOMMENDED. The client MUST NOT use the authorization code
|
||||
more than once. If an authorization code is used more than
|
||||
once, the authorization server MUST deny the request and SHOULD
|
||||
revoke (when possible) all tokens previously issued based on
|
||||
that authorization code. The authorization code is bound to
|
||||
the client identifier and redirection URI.
|
||||
state
|
||||
REQUIRED if the "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
"""
|
||||
response = parse_authorization_code_response(uri, state=state)
|
||||
self._populate_attributes(response)
|
||||
return response
|
||||
|
||||
def parse_request_body_response(self, body, scope=None):
|
||||
"""Parse the JSON response body.
|
||||
|
||||
If the access token request is valid and authorized, the
|
||||
authorization server issues an access token and optional refresh
|
||||
token as described in `Section 5.1`_. If the request client
|
||||
authentication failed or is invalid, the authorization server returns
|
||||
an error response as described in `Section 5.2`_.
|
||||
|
||||
.. `Section 5.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.1
|
||||
.. `Section 5.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.2
|
||||
"""
|
||||
response = parse_token_response(body, scope=scope)
|
||||
self._populate_attributes(response)
|
||||
return response
|
||||
|
||||
|
||||
class UserAgentClient(Client):
|
||||
"""A public client utilizing the implicit code grant workflow.
|
||||
|
||||
A user-agent-based application is a public client in which the
|
||||
client code is downloaded from a web server and executes within a
|
||||
user-agent (e.g. web browser) on the device used by the resource
|
||||
owner. Protocol data and credentials are easily accessible (and
|
||||
often visible) to the resource owner. Since such applications
|
||||
reside within the user-agent, they can make seamless use of the
|
||||
user-agent capabilities when requesting authorization.
|
||||
|
||||
The implicit grant type is used to obtain access tokens (it does not
|
||||
support the issuance of refresh tokens) and is optimized for public
|
||||
clients known to operate a particular redirection URI. These clients
|
||||
are typically implemented in a browser using a scripting language
|
||||
such as JavaScript.
|
||||
|
||||
As a redirection-based flow, the client must be capable of
|
||||
interacting with the resource owner's user-agent (typically a web
|
||||
browser) and capable of receiving incoming requests (via redirection)
|
||||
from the authorization server.
|
||||
|
||||
Unlike the authorization code grant type in which the client makes
|
||||
separate requests for authorization and access token, the client
|
||||
receives the access token as the result of the authorization request.
|
||||
|
||||
The implicit grant type does not include client authentication, and
|
||||
relies on the presence of the resource owner and the registration of
|
||||
the redirection URI. Because the access token is encoded into the
|
||||
redirection URI, it may be exposed to the resource owner and other
|
||||
applications residing on the same device.
|
||||
"""
|
||||
|
||||
def prepare_request_uri(self, uri, redirect_uri=None, scope=None,
|
||||
state=None, **kwargs):
|
||||
"""Prepare the implicit grant request URI.
|
||||
|
||||
The client constructs the request URI by adding the following
|
||||
parameters to the query component of the authorization endpoint URI
|
||||
using the "application/x-www-form-urlencoded" format:
|
||||
|
||||
response_type
|
||||
REQUIRED. Value MUST be set to "token".
|
||||
client_id
|
||||
REQUIRED. The client identifier as described in Section 2.2.
|
||||
redirect_uri
|
||||
OPTIONAL. As described in Section 3.1.2.
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
Section 3.3.
|
||||
state
|
||||
RECOMMENDED. An opaque value used by the client to maintain
|
||||
state between the request and callback. The authorization
|
||||
server includes this value when redirecting the user-agent back
|
||||
to the client. The parameter SHOULD be used for preventing
|
||||
cross-site request forgery as described in Section 10.12.
|
||||
"""
|
||||
redirect_uri = redirect_uri or self.default_redirect_uri
|
||||
return prepare_grant_uri(uri, self.client_id, u'token',
|
||||
redirect_uri=redirect_uri, state=state, scope=scope, **kwargs)
|
||||
|
||||
def parse_request_uri_response(self, uri, state=None, scope=None):
|
||||
"""Parse the response URI fragment.
|
||||
|
||||
If the resource owner grants the access request, the authorization
|
||||
server issues an access token and delivers it to the client by adding
|
||||
the following parameters to the fragment component of the redirection
|
||||
URI using the "application/x-www-form-urlencoded" format:
|
||||
|
||||
access_token
|
||||
REQUIRED. The access token issued by the authorization server.
|
||||
token_type
|
||||
REQUIRED. The type of the token issued as described in
|
||||
`Section 7.1`_. Value is case insensitive.
|
||||
expires_in
|
||||
RECOMMENDED. The lifetime in seconds of the access token. For
|
||||
example, the value "3600" denotes that the access token will
|
||||
expire in one hour from the time the response was generated.
|
||||
If omitted, the authorization server SHOULD provide the
|
||||
expiration time via other means or document the default value.
|
||||
scope
|
||||
OPTIONAL, if identical to the scope requested by the client,
|
||||
otherwise REQUIRED. The scope of the access token as described
|
||||
by `Section 3.3`_.
|
||||
state
|
||||
REQUIRED if the "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
|
||||
.. _`Section 7.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-7.1
|
||||
.. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3
|
||||
"""
|
||||
response = parse_implicit_response(uri, state=state, scope=scope)
|
||||
self._populate_attributes(response)
|
||||
return response
|
||||
|
||||
|
||||
class NativeApplicationClient(Client):
|
||||
"""A public client utilizing the client credentials grant workflow.
|
||||
|
||||
A native application is a public client installed and executed on
|
||||
the device used by the resource owner. Protocol data and
|
||||
credentials are accessible to the resource owner. It is assumed
|
||||
that any client authentication credentials included in the
|
||||
application can be extracted. On the other hand, dynamically
|
||||
issued credentials such as access tokens or refresh tokens can
|
||||
receive an acceptable level of protection. At a minimum, these
|
||||
credentials are protected from hostile servers with which the
|
||||
application may interact with. On some platforms these
|
||||
credentials might be protected from other applications residing on
|
||||
the same device.
|
||||
|
||||
The client can request an access token using only its client
|
||||
credentials (or other supported means of authentication) when the
|
||||
client is requesting access to the protected resources under its
|
||||
control, or those of another resource owner which has been previously
|
||||
arranged with the authorization server (the method of which is beyond
|
||||
the scope of this specification).
|
||||
|
||||
The client credentials grant type MUST only be used by confidential
|
||||
clients.
|
||||
|
||||
Since the client authentication is used as the authorization grant,
|
||||
no additional authorization request is needed.
|
||||
"""
|
||||
|
||||
def prepare_request_body(self, body=u'', scope=None, **kwargs):
|
||||
"""Add the client credentials to the request body.
|
||||
|
||||
The client makes a request to the token endpoint by adding the
|
||||
following parameters using the "application/x-www-form-urlencoded"
|
||||
format in the HTTP request entity-body:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "client_credentials".
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
`Section 3.3`_.
|
||||
|
||||
.. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3
|
||||
"""
|
||||
return prepare_token_request(u'client_credentials', body=body,
|
||||
scope=scope, **kwargs)
|
||||
|
||||
def parse_request_body_response(self, body, scope=None):
|
||||
"""Parse the JSON response body.
|
||||
|
||||
If the access token request is valid and authorized, the
|
||||
authorization server issues an access token as described in
|
||||
`Section 5.1`_. A refresh token SHOULD NOT be included. If the request
|
||||
failed client authentication or is invalid, the authorization server
|
||||
returns an error response as described in `Section 5.2`_.
|
||||
|
||||
.. `Section 5.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.1
|
||||
.. `Section 5.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.2
|
||||
"""
|
||||
response = parse_token_response(body, scope=scope)
|
||||
self._populate_attributes(response)
|
||||
return response
|
||||
|
||||
|
||||
class PasswordCredentialsClient(Client):
|
||||
"""A public client using the resource owner password and username directly.
|
||||
|
||||
The resource owner password credentials grant type is suitable in
|
||||
cases where the resource owner has a trust relationship with the
|
||||
client, such as the device operating system or a highly privileged
|
||||
application. The authorization server should take special care when
|
||||
enabling this grant type, and only allow it when other flows are not
|
||||
viable.
|
||||
|
||||
The grant type is suitable for clients capable of obtaining the
|
||||
resource owner's credentials (username and password, typically using
|
||||
an interactive form). It is also used to migrate existing clients
|
||||
using direct authentication schemes such as HTTP Basic or Digest
|
||||
authentication to OAuth by converting the stored credentials to an
|
||||
access token.
|
||||
|
||||
The method through which the client obtains the resource owner
|
||||
credentials is beyond the scope of this specification. The client
|
||||
MUST discard the credentials once an access token has been obtained.
|
||||
"""
|
||||
|
||||
def prepare_request_body(self, username, password, body=u'', scope=None,
|
||||
**kwargs):
|
||||
"""Add the resource owner password and username to the request body.
|
||||
|
||||
The client makes a request to the token endpoint by adding the
|
||||
following parameters using the "application/x-www-form-urlencoded"
|
||||
format in the HTTP request entity-body:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "password".
|
||||
username
|
||||
REQUIRED. The resource owner username.
|
||||
password
|
||||
REQUIRED. The resource owner password.
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
`Section 3.3`_.
|
||||
|
||||
.. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3
|
||||
"""
|
||||
return prepare_token_request(u'password', body=body, username=username,
|
||||
password=password, scope=scope, **kwargs)
|
||||
|
||||
def parse_request_body_response(self, body, scope=None):
|
||||
"""Parse the JSON response body.
|
||||
|
||||
If the access token request is valid and authorized, the
|
||||
authorization server issues an access token and optional refresh
|
||||
token as described in `Section 5.1`_. If the request failed client
|
||||
authentication or is invalid, the authorization server returns an
|
||||
error response as described in `Section 5.2`_.
|
||||
|
||||
.. `Section 5.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.1
|
||||
.. `Section 5.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-5.2
|
||||
"""
|
||||
response = parse_token_response(body, scope=scope)
|
||||
self._populate_attributes(response)
|
||||
return response
|
||||
|
||||
|
||||
class Server(object):
|
||||
pass
|
||||
@@ -1,256 +0,0 @@
|
||||
"""
|
||||
oauthlib.oauth2_draft28.parameters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains methods related to `Section 4`_ of the OAuth 2 draft.
|
||||
|
||||
.. _`Section 4`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-4
|
||||
"""
|
||||
|
||||
import json
|
||||
import urlparse
|
||||
from oauthlib.common import add_params_to_uri, add_params_to_qs
|
||||
|
||||
|
||||
def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None,
|
||||
scope=None, state=None, **kwargs):
|
||||
"""Prepare the authorization grant request URI.
|
||||
|
||||
The client constructs the request URI by adding the following
|
||||
parameters to the query component of the authorization endpoint URI
|
||||
using the "application/x-www-form-urlencoded" format as defined by
|
||||
[W3C.REC-html401-19991224]:
|
||||
|
||||
response_type
|
||||
REQUIRED. Value MUST be set to "code".
|
||||
client_id
|
||||
REQUIRED. The client identifier as described in `Section 2.2`_.
|
||||
redirect_uri
|
||||
OPTIONAL. As described in `Section 3.1.2`_.
|
||||
scope
|
||||
OPTIONAL. The scope of the access request as described by
|
||||
`Section 3.3`_.
|
||||
state
|
||||
RECOMMENDED. An opaque value used by the client to maintain
|
||||
state between the request and callback. The authorization
|
||||
server includes this value when redirecting the user-agent back
|
||||
to the client. The parameter SHOULD be used for preventing
|
||||
cross-site request forgery as described in `Section 10.12`_.
|
||||
|
||||
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb HTTP/1.1
|
||||
Host: server.example.com
|
||||
|
||||
.. _`W3C.REC-html401-19991224`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#ref-W3C.REC-html401-19991224
|
||||
.. _`Section 2.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-2.2
|
||||
.. _`Section 3.1.2`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.1.2
|
||||
.. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3
|
||||
.. _`section 10.12`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-10.12
|
||||
"""
|
||||
params = [((u'response_type', response_type)),
|
||||
((u'client_id', client_id))]
|
||||
|
||||
if redirect_uri:
|
||||
params.append((u'redirect_uri', redirect_uri))
|
||||
if scope:
|
||||
params.append((u'scope', scope))
|
||||
if state:
|
||||
params.append((u'state', state))
|
||||
|
||||
for k in kwargs:
|
||||
params.append((unicode(k), kwargs[k]))
|
||||
|
||||
return add_params_to_uri(uri, params)
|
||||
|
||||
|
||||
def prepare_token_request(grant_type, body=u'', **kwargs):
|
||||
"""Prepare the access token request.
|
||||
|
||||
The client makes a request to the token endpoint by adding the
|
||||
following parameters using the "application/x-www-form-urlencoded"
|
||||
format in the HTTP request entity-body:
|
||||
|
||||
grant_type
|
||||
REQUIRED. Value MUST be set to "authorization_code".
|
||||
code
|
||||
REQUIRED. The authorization code received from the
|
||||
authorization server.
|
||||
redirect_uri
|
||||
REQUIRED, if the "redirect_uri" parameter was included in the
|
||||
authorization request as described in `Section 4.1.1`_, and their
|
||||
values MUST be identical.
|
||||
|
||||
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA
|
||||
&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
|
||||
|
||||
.. _`Section 4.1.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-4.1.1
|
||||
"""
|
||||
params = [(u'grant_type', grant_type)]
|
||||
for k in kwargs:
|
||||
params.append((unicode(k), kwargs[k]))
|
||||
|
||||
return add_params_to_qs(body, params)
|
||||
|
||||
|
||||
def parse_authorization_code_response(uri, state=None):
|
||||
"""Parse authorization grant response URI into a dict.
|
||||
|
||||
If the resource owner grants the access request, the authorization
|
||||
server issues an authorization code and delivers it to the client by
|
||||
adding the following parameters to the query component of the
|
||||
redirection URI using the "application/x-www-form-urlencoded" format:
|
||||
|
||||
code
|
||||
REQUIRED. The authorization code generated by the
|
||||
authorization server. The authorization code MUST expire
|
||||
shortly after it is issued to mitigate the risk of leaks. A
|
||||
maximum authorization code lifetime of 10 minutes is
|
||||
RECOMMENDED. The client MUST NOT use the authorization code
|
||||
more than once. If an authorization code is used more than
|
||||
once, the authorization server MUST deny the request and SHOULD
|
||||
revoke (when possible) all tokens previously issued based on
|
||||
that authorization code. The authorization code is bound to
|
||||
the client identifier and redirection URI.
|
||||
state
|
||||
REQUIRED if the "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
|
||||
For example, the authorization server redirects the user-agent by
|
||||
sending the following HTTP response:
|
||||
|
||||
HTTP/1.1 302 Found
|
||||
Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA
|
||||
&state=xyz
|
||||
|
||||
"""
|
||||
query = urlparse.urlparse(uri).query
|
||||
params = dict(urlparse.parse_qsl(query))
|
||||
|
||||
if not u'code' in params:
|
||||
raise KeyError("Missing code parameter in response.")
|
||||
|
||||
if state and params.get(u'state', None) != state:
|
||||
raise ValueError("Mismatching or missing state in response.")
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def parse_implicit_response(uri, state=None, scope=None):
|
||||
"""Parse the implicit token response URI into a dict.
|
||||
|
||||
If the resource owner grants the access request, the authorization
|
||||
server issues an access token and delivers it to the client by adding
|
||||
the following parameters to the fragment component of the redirection
|
||||
URI using the "application/x-www-form-urlencoded" format:
|
||||
|
||||
access_token
|
||||
REQUIRED. The access token issued by the authorization server.
|
||||
token_type
|
||||
REQUIRED. The type of the token issued as described in
|
||||
Section 7.1. Value is case insensitive.
|
||||
expires_in
|
||||
RECOMMENDED. The lifetime in seconds of the access token. For
|
||||
example, the value "3600" denotes that the access token will
|
||||
expire in one hour from the time the response was generated.
|
||||
If omitted, the authorization server SHOULD provide the
|
||||
expiration time via other means or document the default value.
|
||||
scope
|
||||
OPTIONAL, if identical to the scope requested by the client,
|
||||
otherwise REQUIRED. The scope of the access token as described
|
||||
by Section 3.3.
|
||||
state
|
||||
REQUIRED if the "state" parameter was present in the client
|
||||
authorization request. The exact value received from the
|
||||
client.
|
||||
|
||||
HTTP/1.1 302 Found
|
||||
Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA
|
||||
&state=xyz&token_type=example&expires_in=3600
|
||||
"""
|
||||
fragment = urlparse.urlparse(uri).fragment
|
||||
params = dict(urlparse.parse_qsl(fragment, keep_blank_values=True))
|
||||
validate_token_parameters(params, scope)
|
||||
|
||||
if state and params.get(u'state', None) != state:
|
||||
raise ValueError("Mismatching or missing state in params.")
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def parse_token_response(body, scope=None):
|
||||
"""Parse the JSON token response body into a dict.
|
||||
|
||||
The authorization server issues an access token and optional refresh
|
||||
token, and constructs the response by adding the following parameters
|
||||
to the entity body of the HTTP response with a 200 (OK) status code:
|
||||
|
||||
access_token
|
||||
REQUIRED. The access token issued by the authorization server.
|
||||
token_type
|
||||
REQUIRED. The type of the token issued as described in
|
||||
`Section 7.1`_. Value is case insensitive.
|
||||
expires_in
|
||||
RECOMMENDED. The lifetime in seconds of the access token. For
|
||||
example, the value "3600" denotes that the access token will
|
||||
expire in one hour from the time the response was generated.
|
||||
If omitted, the authorization server SHOULD provide the
|
||||
expiration time via other means or document the default value.
|
||||
refresh_token
|
||||
OPTIONAL. The refresh token which can be used to obtain new
|
||||
access tokens using the same authorization grant as described
|
||||
in `Section 6`_.
|
||||
scope
|
||||
OPTIONAL, if identical to the scope requested by the client,
|
||||
otherwise REQUIRED. The scope of the access token as described
|
||||
by `Section 3.3`_.
|
||||
|
||||
The parameters are included in the entity body of the HTTP response
|
||||
using the "application/json" media type as defined by [`RFC4627`_]. The
|
||||
parameters are serialized into a JSON structure by adding each
|
||||
parameter at the highest structure level. Parameter names and string
|
||||
values are included as JSON strings. Numerical values are included
|
||||
as JSON numbers. The order of parameters does not matter and can
|
||||
vary.
|
||||
|
||||
For example:
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json;charset=UTF-8
|
||||
Cache-Control: no-store
|
||||
Pragma: no-cache
|
||||
|
||||
{
|
||||
"access_token":"2YotnFZFEjr1zCsicMWpAA",
|
||||
"token_type":"example",
|
||||
"expires_in":3600,
|
||||
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
|
||||
"example_parameter":"example_value"
|
||||
}
|
||||
|
||||
.. _`Section 7.1`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-7.1
|
||||
.. _`Section 6`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-6
|
||||
.. _`Section 3.3`: http://tools.ietf.org/html/draft-ietf-oauth-v2-28#section-3.3
|
||||
.. _`RFC4627`: http://tools.ietf.org/html/rfc4627
|
||||
"""
|
||||
params = json.loads(body)
|
||||
validate_token_parameters(params, scope)
|
||||
return params
|
||||
|
||||
|
||||
def validate_token_parameters(params, scope=None):
|
||||
"""Ensures token precence, token type, expiration and scope in params."""
|
||||
|
||||
if not u'access_token' in params:
|
||||
raise KeyError("Missing access token parameter.")
|
||||
|
||||
if not u'token_type' in params:
|
||||
raise KeyError("Missing token type parameter.")
|
||||
|
||||
# If the issued access token scope is different from the one requested by
|
||||
# the client, the authorization server MUST include the "scope" response
|
||||
# parameter to inform the client of the actual scope granted.
|
||||
# http://tools.ietf.org/html/draft-ietf-oauth-v2-25#section-3.3
|
||||
new_scope = params.get(u'scope', None)
|
||||
if scope and new_scope and scope != new_scope:
|
||||
raise Warning("Scope has changed to %s." % new_scope)
|
||||
@@ -1,132 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
"""
|
||||
oauthlib.oauth2.draft25.tokens
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains methods for adding two types of access tokens to requests.
|
||||
|
||||
- Bearer http://tools.ietf.org/html/draft-ietf-oauth-saml2-bearer-08
|
||||
- MAC http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-00
|
||||
|
||||
"""
|
||||
from binascii import b2a_base64
|
||||
import hashlib
|
||||
import hmac
|
||||
from urlparse import urlparse
|
||||
|
||||
from oauthlib.common import add_params_to_uri, add_params_to_qs
|
||||
from . import utils
|
||||
|
||||
|
||||
def prepare_mac_header(token, uri, key, http_method, nonce=None, headers=None,
|
||||
body=None, ext=u'', hash_algorithm=u'hmac-sha-1'):
|
||||
"""Add an `MAC Access Authentication`_ signature to headers.
|
||||
|
||||
Unlike OAuth 1, this HMAC signature does not require inclusion of the request
|
||||
payload/body, neither does it use a combination of client_secret and
|
||||
token_secret but rather a mac_key provided together with the access token.
|
||||
|
||||
Currently two algorithms are supported, "hmac-sha-1" and "hmac-sha-256",
|
||||
`extension algorithms`_ are not supported.
|
||||
|
||||
Example MAC Authorization header, linebreaks added for clarity
|
||||
|
||||
Authorization: MAC id="h480djs93hd8",
|
||||
nonce="1336363200:dj83hs9s",
|
||||
mac="bhCQXTVyfj5cmA9uKkPFx1zeOXM="
|
||||
|
||||
.. _`MAC Access Authentication`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01
|
||||
.. _`extension algorithms`: http://tools.ietf.org/html/draft-ietf-oauth-v2-http-mac-01#section-7.1
|
||||
|
||||
:param uri: Request URI.
|
||||
:param headers: Request headers as a dictionary.
|
||||
:param http_method: HTTP Request method.
|
||||
:param key: MAC given provided by token endpoint.
|
||||
:param algorithm: HMAC algorithm provided by token endpoint.
|
||||
:return: headers dictionary with the authorization field added.
|
||||
"""
|
||||
http_method = http_method.upper()
|
||||
host, port = utils.host_from_uri(uri)
|
||||
|
||||
if hash_algorithm.lower() == u'hmac-sha-1':
|
||||
h = hashlib.sha1
|
||||
else:
|
||||
h = hashlib.sha256
|
||||
|
||||
nonce = nonce or u'{0}:{1}'.format(utils.generate_nonce(), utils.generate_timestamp())
|
||||
sch, net, path, par, query, fra = urlparse(uri)
|
||||
|
||||
if query:
|
||||
request_uri = path + u'?' + query
|
||||
else:
|
||||
request_uri = path
|
||||
|
||||
# Hash the body/payload
|
||||
if body is not None:
|
||||
bodyhash = b2a_base64(h(body).digest())[:-1].decode('utf-8')
|
||||
else:
|
||||
bodyhash = u''
|
||||
|
||||
# Create the normalized base string
|
||||
base = []
|
||||
base.append(nonce)
|
||||
base.append(http_method.upper())
|
||||
base.append(request_uri)
|
||||
base.append(host)
|
||||
base.append(port)
|
||||
base.append(bodyhash)
|
||||
base.append(ext)
|
||||
base_string = '\n'.join(base) + u'\n'
|
||||
|
||||
# hmac struggles with unicode strings - http://bugs.python.org/issue5285
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
sign = hmac.new(key, base_string, h)
|
||||
sign = b2a_base64(sign.digest())[:-1].decode('utf-8')
|
||||
|
||||
header = []
|
||||
header.append(u'MAC id="%s"' % token)
|
||||
header.append(u'nonce="%s"' % nonce)
|
||||
if bodyhash:
|
||||
header.append(u'bodyhash="%s"' % bodyhash)
|
||||
if ext:
|
||||
header.append(u'ext="%s"' % ext)
|
||||
header.append(u'mac="%s"' % sign)
|
||||
|
||||
headers = headers or {}
|
||||
headers[u'Authorization'] = u', '.join(header)
|
||||
return headers
|
||||
|
||||
|
||||
def prepare_bearer_uri(token, uri):
|
||||
"""Add a `Bearer Token`_ to the request URI.
|
||||
Not recommended, use only if client can't use authorization header or body.
|
||||
|
||||
http://www.example.com/path?access_token=h480djs93hd8
|
||||
|
||||
.. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18
|
||||
"""
|
||||
return add_params_to_uri(uri, [((u'access_token', token))])
|
||||
|
||||
|
||||
def prepare_bearer_headers(token, headers=None):
|
||||
"""Add a `Bearer Token`_ to the request URI.
|
||||
Recommended method of passing bearer tokens.
|
||||
|
||||
Authorization: Bearer h480djs93hd8
|
||||
|
||||
.. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18
|
||||
"""
|
||||
headers = headers or {}
|
||||
headers[u'Authorization'] = u'Bearer %s' % token
|
||||
return headers
|
||||
|
||||
|
||||
def prepare_bearer_body(token, body=u''):
|
||||
"""Add a `Bearer Token`_ to the request body.
|
||||
|
||||
access_token=h480djs93hd8
|
||||
|
||||
.. _`Bearer Token`: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-18
|
||||
"""
|
||||
return add_params_to_qs(body, [((u'access_token', token))])
|
||||
@@ -1,39 +0,0 @@
|
||||
"""
|
||||
oauthlib.utils
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module contains utility methods used by various parts of the OAuth 2 spec.
|
||||
"""
|
||||
|
||||
import urllib
|
||||
import urlparse
|
||||
|
||||
|
||||
def host_from_uri(uri):
|
||||
"""Extract hostname and port from URI.
|
||||
|
||||
Will use default port for HTTP and HTTPS if none is present in the URI.
|
||||
"""
|
||||
default_ports = {
|
||||
u'HTTP': u'80',
|
||||
u'HTTPS': u'443',
|
||||
}
|
||||
|
||||
sch, netloc, path, par, query, fra = urlparse.urlparse(uri)
|
||||
if u':' in netloc:
|
||||
netloc, port = netloc.split(u':', 1)
|
||||
else:
|
||||
port = default_ports.get(sch.upper())
|
||||
|
||||
return netloc, port
|
||||
|
||||
|
||||
def escape(u):
|
||||
"""Escape a string in an OAuth-compatible fashion.
|
||||
|
||||
TODO: verify whether this can in fact be used for OAuth 2
|
||||
|
||||
"""
|
||||
if not isinstance(u, unicode):
|
||||
raise ValueError('Only unicode objects are escapable.')
|
||||
return urllib.quote(u.encode('utf-8'), safe='~')
|
||||
Reference in New Issue
Block a user