mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 02:31:09 -05:00
first commit
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
"""
|
||||
oauthlib.oauth1
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is a wrapper for the most recent implementation of OAuth 1.0 Client
|
||||
and Server classes.
|
||||
"""
|
||||
from .rfc5849 import (
|
||||
SIGNATURE_HMAC, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256,
|
||||
SIGNATURE_HMAC_SHA512, SIGNATURE_PLAINTEXT, SIGNATURE_RSA,
|
||||
SIGNATURE_RSA_SHA1, SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512,
|
||||
SIGNATURE_TYPE_AUTH_HEADER, SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY,
|
||||
Client,
|
||||
)
|
||||
from .rfc5849.endpoints import (
|
||||
AccessTokenEndpoint, AuthorizationEndpoint, RequestTokenEndpoint,
|
||||
ResourceEndpoint, SignatureOnlyEndpoint, WebApplicationServer,
|
||||
)
|
||||
from .rfc5849.errors import (
|
||||
InsecureTransportError, InvalidClientError, InvalidRequestError,
|
||||
InvalidSignatureMethodError, OAuth1Error,
|
||||
)
|
||||
from .rfc5849.request_validator import RequestValidator
|
||||
Binary file not shown.
@@ -0,0 +1,365 @@
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for signing and checking OAuth 1.0 RFC 5849 requests.
|
||||
|
||||
It supports all three standard signature methods defined in RFC 5849:
|
||||
|
||||
- HMAC-SHA1
|
||||
- RSA-SHA1
|
||||
- PLAINTEXT
|
||||
|
||||
It also supports signature methods that are not defined in RFC 5849. These are
|
||||
based on the standard ones but replace SHA-1 with the more secure SHA-256:
|
||||
|
||||
- HMAC-SHA256
|
||||
- RSA-SHA256
|
||||
|
||||
"""
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import urllib.parse as urlparse
|
||||
|
||||
from oauthlib.common import (
|
||||
Request, generate_nonce, generate_timestamp, to_unicode, urlencode,
|
||||
)
|
||||
|
||||
from . import parameters, signature
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Available signature methods
|
||||
#
|
||||
# Note: SIGNATURE_HMAC and SIGNATURE_RSA are kept for backward compatibility
|
||||
# with previous versions of this library, when it the only HMAC-based and
|
||||
# RSA-based signature methods were HMAC-SHA1 and RSA-SHA1. But now that it
|
||||
# supports other hashing algorithms besides SHA1, explicitly identifying which
|
||||
# hashing algorithm is being used is recommended.
|
||||
#
|
||||
# Note: if additional values are defined here, don't forget to update the
|
||||
# imports in "../__init__.py" so they are available outside this module.
|
||||
|
||||
SIGNATURE_HMAC_SHA1 = "HMAC-SHA1"
|
||||
SIGNATURE_HMAC_SHA256 = "HMAC-SHA256"
|
||||
SIGNATURE_HMAC_SHA512 = "HMAC-SHA512"
|
||||
SIGNATURE_HMAC = SIGNATURE_HMAC_SHA1 # deprecated variable for HMAC-SHA1
|
||||
|
||||
SIGNATURE_RSA_SHA1 = "RSA-SHA1"
|
||||
SIGNATURE_RSA_SHA256 = "RSA-SHA256"
|
||||
SIGNATURE_RSA_SHA512 = "RSA-SHA512"
|
||||
SIGNATURE_RSA = SIGNATURE_RSA_SHA1 # deprecated variable for RSA-SHA1
|
||||
|
||||
SIGNATURE_PLAINTEXT = "PLAINTEXT"
|
||||
|
||||
SIGNATURE_METHODS = (
|
||||
SIGNATURE_HMAC_SHA1,
|
||||
SIGNATURE_HMAC_SHA256,
|
||||
SIGNATURE_HMAC_SHA512,
|
||||
SIGNATURE_RSA_SHA1,
|
||||
SIGNATURE_RSA_SHA256,
|
||||
SIGNATURE_RSA_SHA512,
|
||||
SIGNATURE_PLAINTEXT
|
||||
)
|
||||
|
||||
SIGNATURE_TYPE_AUTH_HEADER = 'AUTH_HEADER'
|
||||
SIGNATURE_TYPE_QUERY = 'QUERY'
|
||||
SIGNATURE_TYPE_BODY = 'BODY'
|
||||
|
||||
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
|
||||
|
||||
|
||||
class Client:
|
||||
|
||||
"""A client used to sign OAuth 1.0 RFC 5849 requests."""
|
||||
SIGNATURE_METHODS = {
|
||||
SIGNATURE_HMAC_SHA1: signature.sign_hmac_sha1_with_client,
|
||||
SIGNATURE_HMAC_SHA256: signature.sign_hmac_sha256_with_client,
|
||||
SIGNATURE_HMAC_SHA512: signature.sign_hmac_sha512_with_client,
|
||||
SIGNATURE_RSA_SHA1: signature.sign_rsa_sha1_with_client,
|
||||
SIGNATURE_RSA_SHA256: signature.sign_rsa_sha256_with_client,
|
||||
SIGNATURE_RSA_SHA512: signature.sign_rsa_sha512_with_client,
|
||||
SIGNATURE_PLAINTEXT: signature.sign_plaintext_with_client
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def register_signature_method(cls, method_name, method_callback):
|
||||
cls.SIGNATURE_METHODS[method_name] = method_callback
|
||||
|
||||
def __init__(self, client_key,
|
||||
client_secret=None,
|
||||
resource_owner_key=None,
|
||||
resource_owner_secret=None,
|
||||
callback_uri=None,
|
||||
signature_method=SIGNATURE_HMAC_SHA1,
|
||||
signature_type=SIGNATURE_TYPE_AUTH_HEADER,
|
||||
rsa_key=None, verifier=None, realm=None,
|
||||
encoding='utf-8', decoding=None,
|
||||
nonce=None, timestamp=None):
|
||||
"""Create an OAuth 1 client.
|
||||
|
||||
:param client_key: Client key (consumer key), mandatory.
|
||||
:param resource_owner_key: Resource owner key (oauth token).
|
||||
:param resource_owner_secret: Resource owner secret (oauth token secret).
|
||||
:param callback_uri: Callback used when obtaining request token.
|
||||
:param signature_method: SIGNATURE_HMAC, SIGNATURE_RSA or SIGNATURE_PLAINTEXT.
|
||||
:param signature_type: SIGNATURE_TYPE_AUTH_HEADER (default),
|
||||
SIGNATURE_TYPE_QUERY or SIGNATURE_TYPE_BODY
|
||||
depending on where you want to embed the oauth
|
||||
credentials.
|
||||
:param rsa_key: RSA key used with SIGNATURE_RSA.
|
||||
:param verifier: Verifier used when obtaining an access token.
|
||||
:param realm: Realm (scope) to which access is being requested.
|
||||
:param encoding: If you provide non-unicode input you may use this
|
||||
to have oauthlib automatically convert.
|
||||
:param decoding: If you wish that the returned uri, headers and body
|
||||
from sign be encoded back from unicode, then set
|
||||
decoding to your preferred encoding, i.e. utf-8.
|
||||
:param nonce: Use this nonce instead of generating one. (Mainly for testing)
|
||||
:param timestamp: Use this timestamp instead of using current. (Mainly for testing)
|
||||
"""
|
||||
# Convert to unicode using encoding if given, else assume unicode
|
||||
encode = lambda x: to_unicode(x, encoding) if encoding else x
|
||||
|
||||
self.client_key = encode(client_key)
|
||||
self.client_secret = encode(client_secret)
|
||||
self.resource_owner_key = encode(resource_owner_key)
|
||||
self.resource_owner_secret = encode(resource_owner_secret)
|
||||
self.signature_method = encode(signature_method)
|
||||
self.signature_type = encode(signature_type)
|
||||
self.callback_uri = encode(callback_uri)
|
||||
self.rsa_key = encode(rsa_key)
|
||||
self.verifier = encode(verifier)
|
||||
self.realm = encode(realm)
|
||||
self.encoding = encode(encoding)
|
||||
self.decoding = encode(decoding)
|
||||
self.nonce = encode(nonce)
|
||||
self.timestamp = encode(timestamp)
|
||||
|
||||
def __repr__(self):
|
||||
attrs = vars(self).copy()
|
||||
attrs['client_secret'] = '****' if attrs['client_secret'] else None
|
||||
attrs['rsa_key'] = '****' if attrs['rsa_key'] else None
|
||||
attrs[
|
||||
'resource_owner_secret'] = '****' if attrs['resource_owner_secret'] else None
|
||||
attribute_str = ', '.join('{}={}'.format(k, v) for k, v in attrs.items())
|
||||
return '<{} {}>'.format(self.__class__.__name__, attribute_str)
|
||||
|
||||
def get_oauth_signature(self, request):
|
||||
"""Get an OAuth signature to be used in signing a request
|
||||
|
||||
To satisfy `section 3.4.1.2`_ item 2, if the request argument's
|
||||
headers dict attribute contains a Host item, its value will
|
||||
replace any netloc part of the request argument's uri attribute
|
||||
value.
|
||||
|
||||
.. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
||||
"""
|
||||
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)
|
||||
log.debug("Collected params: {}".format(collected_params))
|
||||
|
||||
normalized_params = signature.normalize_parameters(collected_params)
|
||||
normalized_uri = signature.base_string_uri(uri, headers.get('Host', None))
|
||||
log.debug("Normalized params: {}".format(normalized_params))
|
||||
log.debug("Normalized URI: {}".format(normalized_uri))
|
||||
|
||||
base_string = signature.signature_base_string(request.http_method,
|
||||
normalized_uri, normalized_params)
|
||||
|
||||
log.debug("Signing: signature base string: {}".format(base_string))
|
||||
|
||||
if self.signature_method not in self.SIGNATURE_METHODS:
|
||||
raise ValueError('Invalid signature method.')
|
||||
|
||||
sig = self.SIGNATURE_METHODS[self.signature_method](base_string, self)
|
||||
|
||||
log.debug("Signature: {}".format(sig))
|
||||
return sig
|
||||
|
||||
def get_oauth_params(self, request):
|
||||
"""Get the basic OAuth parameters to be used in generating a signature.
|
||||
"""
|
||||
nonce = (generate_nonce()
|
||||
if self.nonce is None else self.nonce)
|
||||
timestamp = (generate_timestamp()
|
||||
if self.timestamp is None else self.timestamp)
|
||||
params = [
|
||||
('oauth_nonce', nonce),
|
||||
('oauth_timestamp', timestamp),
|
||||
('oauth_version', '1.0'),
|
||||
('oauth_signature_method', self.signature_method),
|
||||
('oauth_consumer_key', self.client_key),
|
||||
]
|
||||
if self.resource_owner_key:
|
||||
params.append(('oauth_token', self.resource_owner_key))
|
||||
if self.callback_uri:
|
||||
params.append(('oauth_callback', self.callback_uri))
|
||||
if self.verifier:
|
||||
params.append(('oauth_verifier', self.verifier))
|
||||
|
||||
# providing body hash for requests other than x-www-form-urlencoded
|
||||
# as described in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-4.1.1
|
||||
# 4.1.1. When to include the body hash
|
||||
# * [...] MUST NOT include an oauth_body_hash parameter on requests with form-encoded request bodies
|
||||
# * [...] SHOULD include the oauth_body_hash parameter on all other requests.
|
||||
# Note that SHA-1 is vulnerable. The spec acknowledges that in https://tools.ietf.org/html/draft-eaton-oauth-bodyhash-00#section-6.2
|
||||
# At this time, no further effort has been made to replace SHA-1 for the OAuth Request Body Hash extension.
|
||||
content_type = request.headers.get('Content-Type', None)
|
||||
content_type_eligible = content_type and content_type.find('application/x-www-form-urlencoded') < 0
|
||||
if request.body is not None and content_type_eligible:
|
||||
params.append(('oauth_body_hash', base64.b64encode(hashlib.sha1(request.body.encode('utf-8')).digest()).decode('utf-8')))
|
||||
|
||||
return params
|
||||
|
||||
def _render(self, request, formencode=False, realm=None):
|
||||
"""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, realm=realm)
|
||||
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'] = '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='GET', body=None, headers=None, realm=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. Also worth noting is that duplicate parameters
|
||||
will be included in the signature, regardless of where they are
|
||||
specified (query, body).
|
||||
|
||||
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.
|
||||
|
||||
Body may not be included if the http_method is either GET or HEAD as
|
||||
this changes the semantic meaning of the request.
|
||||
|
||||
All string data MUST be unicode or be encoded with the same encoding
|
||||
scheme supplied to the Client constructor, default utf-8. This includes
|
||||
strings inside body dicts, for example.
|
||||
"""
|
||||
# normalize request data
|
||||
request = Request(uri, http_method, body, headers,
|
||||
encoding=self.encoding)
|
||||
|
||||
# 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 {} "
|
||||
"instead of {}".format(content_type or "not set",
|
||||
CONTENT_TYPE_FORM_URLENCODED))
|
||||
|
||||
# 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')
|
||||
|
||||
# We amend https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
|
||||
# with the clause that parameters from body should only be included
|
||||
# in non GET or HEAD requests. Extracting the request body parameters
|
||||
# and including them in the signature base string would give semantic
|
||||
# meaning to the body, which it should not have according to the
|
||||
# HTTP 1.1 spec.
|
||||
elif http_method.upper() in ('GET', 'HEAD') and has_params:
|
||||
raise ValueError('GET/HEAD requests should not include body.')
|
||||
|
||||
# generate the basic OAuth parameters
|
||||
request.oauth_params = self.get_oauth_params(request)
|
||||
|
||||
# generate the signature
|
||||
request.oauth_params.append(
|
||||
('oauth_signature', self.get_oauth_signature(request)))
|
||||
|
||||
# render the signed request and return it
|
||||
uri, headers, body = self._render(request, formencode=True,
|
||||
realm=(realm or self.realm))
|
||||
|
||||
if self.decoding:
|
||||
log.debug('Encoding URI, headers and body to %s.', self.decoding)
|
||||
uri = uri.encode(self.decoding)
|
||||
body = body.encode(self.decoding) if body else body
|
||||
new_headers = {}
|
||||
for k, v in headers.items():
|
||||
new_headers[k.encode(self.decoding)] = v.encode(self.decoding)
|
||||
headers = new_headers
|
||||
return uri, headers, body
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
from .access_token import AccessTokenEndpoint
|
||||
from .authorization import AuthorizationEndpoint
|
||||
from .base import BaseEndpoint
|
||||
from .request_token import RequestTokenEndpoint
|
||||
from .resource import ResourceEndpoint
|
||||
from .signature_only import SignatureOnlyEndpoint
|
||||
|
||||
from .pre_configured import WebApplicationServer # isort:skip
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,215 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849.endpoints.access_token
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of the access token provider logic of
|
||||
OAuth 1.0 RFC 5849. It validates the correctness of access token requests,
|
||||
creates and persists tokens as well as create the proper response to be
|
||||
returned to the client.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from oauthlib.common import urlencode
|
||||
|
||||
from .. import errors
|
||||
from .base import BaseEndpoint
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AccessTokenEndpoint(BaseEndpoint):
|
||||
|
||||
"""An endpoint responsible for providing OAuth 1 access tokens.
|
||||
|
||||
Typical use is to instantiate with a request validator and invoke the
|
||||
``create_access_token_response`` from a view function. The tuple returned
|
||||
has all information necessary (body, status, headers) to quickly form
|
||||
and return a proper response. See :doc:`/oauth1/validator` for details on which
|
||||
validator methods to implement for this endpoint.
|
||||
"""
|
||||
|
||||
def create_access_token(self, request, credentials):
|
||||
"""Create and save a new access token.
|
||||
|
||||
Similar to OAuth 2, indication of granted scopes will be included as a
|
||||
space separated list in ``oauth_authorized_realms``.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: The token as an urlencoded string.
|
||||
"""
|
||||
request.realms = self.request_validator.get_realms(
|
||||
request.resource_owner_key, request)
|
||||
token = {
|
||||
'oauth_token': self.token_generator(),
|
||||
'oauth_token_secret': self.token_generator(),
|
||||
# Backport the authorized scopes indication used in OAuth2
|
||||
'oauth_authorized_realms': ' '.join(request.realms)
|
||||
}
|
||||
token.update(credentials)
|
||||
self.request_validator.save_access_token(token, request)
|
||||
return urlencode(token.items())
|
||||
|
||||
def create_access_token_response(self, uri, http_method='GET', body=None,
|
||||
headers=None, credentials=None):
|
||||
"""Create an access token response, with a new request token if valid.
|
||||
|
||||
:param uri: The full URI of the token request.
|
||||
:param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
|
||||
:param body: The request body as a string.
|
||||
:param headers: The request headers as a dict.
|
||||
:param credentials: A list of extra credentials to include in the token.
|
||||
:returns: A tuple of 3 elements.
|
||||
1. A dict of headers to set on the response.
|
||||
2. The response body as a string.
|
||||
3. The response status code as an integer.
|
||||
|
||||
An example of a valid request::
|
||||
|
||||
>>> from your_validator import your_validator
|
||||
>>> from oauthlib.oauth1 import AccessTokenEndpoint
|
||||
>>> endpoint = AccessTokenEndpoint(your_validator)
|
||||
>>> h, b, s = endpoint.create_access_token_response(
|
||||
... 'https://your.provider/access_token?foo=bar',
|
||||
... headers={
|
||||
... 'Authorization': 'OAuth oauth_token=234lsdkf....'
|
||||
... },
|
||||
... credentials={
|
||||
... 'my_specific': 'argument',
|
||||
... })
|
||||
>>> h
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
>>> b
|
||||
'oauth_token=lsdkfol23w54jlksdef&oauth_token_secret=qwe089234lkjsdf&oauth_authorized_realms=movies+pics&my_specific=argument'
|
||||
>>> s
|
||||
200
|
||||
|
||||
An response to invalid request would have a different body and status::
|
||||
|
||||
>>> b
|
||||
'error=invalid_request&description=missing+resource+owner+key'
|
||||
>>> s
|
||||
400
|
||||
|
||||
The same goes for an an unauthorized request:
|
||||
|
||||
>>> b
|
||||
''
|
||||
>>> s
|
||||
401
|
||||
"""
|
||||
resp_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
try:
|
||||
request = self._create_request(uri, http_method, body, headers)
|
||||
valid, processed_request = self.validate_access_token_request(
|
||||
request)
|
||||
if valid:
|
||||
token = self.create_access_token(request, credentials or {})
|
||||
self.request_validator.invalidate_request_token(
|
||||
request.client_key,
|
||||
request.resource_owner_key,
|
||||
request)
|
||||
return resp_headers, token, 200
|
||||
else:
|
||||
return {}, None, 401
|
||||
except errors.OAuth1Error as e:
|
||||
return resp_headers, e.urlencoded, e.status_code
|
||||
|
||||
def validate_access_token_request(self, request):
|
||||
"""Validate an access token request.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:raises: OAuth1Error if the request is invalid.
|
||||
:returns: A tuple of 2 elements.
|
||||
1. The validation result (True or False).
|
||||
2. The request object.
|
||||
"""
|
||||
self._check_transport_security(request)
|
||||
self._check_mandatory_parameters(request)
|
||||
|
||||
if not request.resource_owner_key:
|
||||
raise errors.InvalidRequestError(
|
||||
description='Missing resource owner.')
|
||||
|
||||
if not self.request_validator.check_request_token(
|
||||
request.resource_owner_key):
|
||||
raise errors.InvalidRequestError(
|
||||
description='Invalid resource owner key format.')
|
||||
|
||||
if not request.verifier:
|
||||
raise errors.InvalidRequestError(
|
||||
description='Missing verifier.')
|
||||
|
||||
if not self.request_validator.check_verifier(request.verifier):
|
||||
raise errors.InvalidRequestError(
|
||||
description='Invalid verifier format.')
|
||||
|
||||
if not self.request_validator.validate_timestamp_and_nonce(
|
||||
request.client_key, request.timestamp, request.nonce, request,
|
||||
request_token=request.resource_owner_key):
|
||||
return False, request
|
||||
|
||||
# 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.request_validator.validate_client_key(
|
||||
request.client_key, request)
|
||||
if not valid_client:
|
||||
request.client_key = self.request_validator.dummy_client
|
||||
|
||||
# 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
|
||||
valid_resource_owner = self.request_validator.validate_request_token(
|
||||
request.client_key, request.resource_owner_key, request)
|
||||
if not valid_resource_owner:
|
||||
request.resource_owner_key = self.request_validator.dummy_request_token
|
||||
|
||||
# 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`: https://tools.ietf.org/html/rfc5849#section-3.2
|
||||
#
|
||||
# Note that early exit would enable resource owner authorization
|
||||
# verifier enumertion.
|
||||
valid_verifier = self.request_validator.validate_verifier(
|
||||
request.client_key,
|
||||
request.resource_owner_key,
|
||||
request.verifier,
|
||||
request)
|
||||
|
||||
valid_signature = self._check_signature(request, is_token_request=True)
|
||||
|
||||
# log the results to the validator_log
|
||||
# this lets us handle internal reporting and analysis
|
||||
request.validator_log['client'] = valid_client
|
||||
request.validator_log['resource_owner'] = valid_resource_owner
|
||||
request.validator_log['verifier'] = valid_verifier
|
||||
request.validator_log['signature'] = valid_signature
|
||||
|
||||
# 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_verifier,
|
||||
valid_signature))
|
||||
if not v:
|
||||
log.info("[Failure] request verification failed.")
|
||||
log.info("Valid client:, %s", valid_client)
|
||||
log.info("Valid token:, %s", valid_resource_owner)
|
||||
log.info("Valid verifier:, %s", valid_verifier)
|
||||
log.info("Valid signature:, %s", valid_signature)
|
||||
return v, request
|
||||
@@ -0,0 +1,158 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849.endpoints.authorization
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for signing and checking OAuth 1.0 RFC 5849 requests.
|
||||
"""
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from oauthlib.common import add_params_to_uri
|
||||
|
||||
from .. import errors
|
||||
from .base import BaseEndpoint
|
||||
|
||||
|
||||
class AuthorizationEndpoint(BaseEndpoint):
|
||||
|
||||
"""An endpoint responsible for letting authenticated users authorize access
|
||||
to their protected resources to a client.
|
||||
|
||||
Typical use would be to have two views, one for displaying the authorization
|
||||
form and one to process said form on submission.
|
||||
|
||||
The first view will want to utilize ``get_realms_and_credentials`` to fetch
|
||||
requested realms and useful client credentials, such as name and
|
||||
description, to be used when creating the authorization form.
|
||||
|
||||
During form processing you can use ``create_authorization_response`` to
|
||||
validate the request, create a verifier as well as prepare the final
|
||||
redirection URI used to send the user back to the client.
|
||||
|
||||
See :doc:`/oauth1/validator` for details on which validator methods to implement
|
||||
for this endpoint.
|
||||
"""
|
||||
|
||||
def create_verifier(self, request, credentials):
|
||||
"""Create and save a new request token.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param credentials: A dict of extra token credentials.
|
||||
:returns: The verifier as a dict.
|
||||
"""
|
||||
verifier = {
|
||||
'oauth_token': request.resource_owner_key,
|
||||
'oauth_verifier': self.token_generator(),
|
||||
}
|
||||
verifier.update(credentials)
|
||||
self.request_validator.save_verifier(
|
||||
request.resource_owner_key, verifier, request)
|
||||
return verifier
|
||||
|
||||
def create_authorization_response(self, uri, http_method='GET', body=None,
|
||||
headers=None, realms=None, credentials=None):
|
||||
"""Create an authorization response, with a new request token if valid.
|
||||
|
||||
:param uri: The full URI of the token request.
|
||||
:param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
|
||||
:param body: The request body as a string.
|
||||
:param headers: The request headers as a dict.
|
||||
:param credentials: A list of credentials to include in the verifier.
|
||||
:returns: A tuple of 3 elements.
|
||||
1. A dict of headers to set on the response.
|
||||
2. The response body as a string.
|
||||
3. The response status code as an integer.
|
||||
|
||||
If the callback URI tied to the current token is "oob", a response with
|
||||
a 200 status code will be returned. In this case, it may be desirable to
|
||||
modify the response to better display the verifier to the client.
|
||||
|
||||
An example of an authorization request::
|
||||
|
||||
>>> from your_validator import your_validator
|
||||
>>> from oauthlib.oauth1 import AuthorizationEndpoint
|
||||
>>> endpoint = AuthorizationEndpoint(your_validator)
|
||||
>>> h, b, s = endpoint.create_authorization_response(
|
||||
... 'https://your.provider/authorize?oauth_token=...',
|
||||
... credentials={
|
||||
... 'extra': 'argument',
|
||||
... })
|
||||
>>> h
|
||||
{'Location': 'https://the.client/callback?oauth_verifier=...&extra=argument'}
|
||||
>>> b
|
||||
None
|
||||
>>> s
|
||||
302
|
||||
|
||||
An example of a request with an "oob" callback::
|
||||
|
||||
>>> from your_validator import your_validator
|
||||
>>> from oauthlib.oauth1 import AuthorizationEndpoint
|
||||
>>> endpoint = AuthorizationEndpoint(your_validator)
|
||||
>>> h, b, s = endpoint.create_authorization_response(
|
||||
... 'https://your.provider/authorize?foo=bar',
|
||||
... credentials={
|
||||
... 'extra': 'argument',
|
||||
... })
|
||||
>>> h
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
>>> b
|
||||
'oauth_verifier=...&extra=argument'
|
||||
>>> s
|
||||
200
|
||||
"""
|
||||
request = self._create_request(uri, http_method=http_method, body=body,
|
||||
headers=headers)
|
||||
|
||||
if not request.resource_owner_key:
|
||||
raise errors.InvalidRequestError(
|
||||
'Missing mandatory parameter oauth_token.')
|
||||
if not self.request_validator.verify_request_token(
|
||||
request.resource_owner_key, request):
|
||||
raise errors.InvalidClientError()
|
||||
|
||||
request.realms = realms
|
||||
if (request.realms and not self.request_validator.verify_realms(
|
||||
request.resource_owner_key, request.realms, request)):
|
||||
raise errors.InvalidRequestError(
|
||||
description=('User granted access to realms outside of '
|
||||
'what the client may request.'))
|
||||
|
||||
verifier = self.create_verifier(request, credentials or {})
|
||||
redirect_uri = self.request_validator.get_redirect_uri(
|
||||
request.resource_owner_key, request)
|
||||
if redirect_uri == 'oob':
|
||||
response_headers = {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
response_body = urlencode(verifier)
|
||||
return response_headers, response_body, 200
|
||||
else:
|
||||
populated_redirect = add_params_to_uri(
|
||||
redirect_uri, verifier.items())
|
||||
return {'Location': populated_redirect}, None, 302
|
||||
|
||||
def get_realms_and_credentials(self, uri, http_method='GET', body=None,
|
||||
headers=None):
|
||||
"""Fetch realms and credentials for the presented request token.
|
||||
|
||||
:param uri: The full URI of the token request.
|
||||
:param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
|
||||
:param body: The request body as a string.
|
||||
:param headers: The request headers as a dict.
|
||||
:returns: A tuple of 2 elements.
|
||||
1. A list of request realms.
|
||||
2. A dict of credentials which may be useful in creating the
|
||||
authorization form.
|
||||
"""
|
||||
request = self._create_request(uri, http_method=http_method, body=body,
|
||||
headers=headers)
|
||||
|
||||
if not self.request_validator.verify_request_token(
|
||||
request.resource_owner_key, request):
|
||||
raise errors.InvalidClientError()
|
||||
|
||||
realms = self.request_validator.get_realms(
|
||||
request.resource_owner_key, request)
|
||||
return realms, {'resource_owner_key': request.resource_owner_key}
|
||||
@@ -0,0 +1,244 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849.endpoints.base
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for signing and checking OAuth 1.0 RFC 5849 requests.
|
||||
"""
|
||||
import time
|
||||
|
||||
from oauthlib.common import CaseInsensitiveDict, Request, generate_token
|
||||
|
||||
from .. import (
|
||||
CONTENT_TYPE_FORM_URLENCODED, SIGNATURE_HMAC_SHA1, SIGNATURE_HMAC_SHA256,
|
||||
SIGNATURE_HMAC_SHA512, SIGNATURE_PLAINTEXT, SIGNATURE_RSA_SHA1,
|
||||
SIGNATURE_RSA_SHA256, SIGNATURE_RSA_SHA512, SIGNATURE_TYPE_AUTH_HEADER,
|
||||
SIGNATURE_TYPE_BODY, SIGNATURE_TYPE_QUERY, errors, signature, utils,
|
||||
)
|
||||
|
||||
|
||||
class BaseEndpoint:
|
||||
|
||||
def __init__(self, request_validator, token_generator=None):
|
||||
self.request_validator = request_validator
|
||||
self.token_generator = token_generator or generate_token
|
||||
|
||||
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.
|
||||
"""
|
||||
# Per RFC5849, only the Authorization header may contain the 'realm'
|
||||
# optional parameter.
|
||||
header_params = signature.collect_parameters(headers=request.headers,
|
||||
exclude_oauth_signature=False, with_realm=True)
|
||||
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 = list(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:
|
||||
found_types = [s[0] for s in signature_types_with_oauth_params]
|
||||
raise errors.InvalidRequestError(
|
||||
description=('oauth_ params must come from only 1 signature'
|
||||
'type but were found in %s',
|
||||
', '.join(found_types)))
|
||||
|
||||
try:
|
||||
signature_type, params, oauth_params = signature_types_with_oauth_params[
|
||||
0]
|
||||
except IndexError:
|
||||
raise errors.InvalidRequestError(
|
||||
description='Missing mandatory OAuth parameters.')
|
||||
|
||||
return signature_type, params, oauth_params
|
||||
|
||||
def _create_request(self, uri, http_method, body, headers):
|
||||
# Only include body data from x-www-form-urlencoded requests
|
||||
headers = CaseInsensitiveDict(headers or {})
|
||||
if ("Content-Type" in headers and
|
||||
CONTENT_TYPE_FORM_URLENCODED in headers["Content-Type"]):
|
||||
request = Request(uri, http_method, body, headers)
|
||||
else:
|
||||
request = Request(uri, http_method, '', headers)
|
||||
|
||||
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 errors.InvalidRequestError(
|
||||
description='Duplicate OAuth1 entries.')
|
||||
|
||||
oauth_params = dict(oauth_params)
|
||||
request.signature = oauth_params.get('oauth_signature')
|
||||
request.client_key = oauth_params.get('oauth_consumer_key')
|
||||
request.resource_owner_key = oauth_params.get('oauth_token')
|
||||
request.nonce = oauth_params.get('oauth_nonce')
|
||||
request.timestamp = oauth_params.get('oauth_timestamp')
|
||||
request.redirect_uri = oauth_params.get('oauth_callback')
|
||||
request.verifier = oauth_params.get('oauth_verifier')
|
||||
request.signature_method = oauth_params.get('oauth_signature_method')
|
||||
request.realm = dict(params).get('realm')
|
||||
request.oauth_params = oauth_params
|
||||
|
||||
# Parameters to Client depend on signature method which may vary
|
||||
# for each request. Note that HMAC-SHA1 and PLAINTEXT share parameters
|
||||
request.params = [(k, v) for k, v in params if k != "oauth_signature"]
|
||||
|
||||
if 'realm' in request.headers.get('Authorization', ''):
|
||||
request.params = [(k, v)
|
||||
for k, v in request.params if k != "realm"]
|
||||
|
||||
return request
|
||||
|
||||
def _check_transport_security(self, request):
|
||||
# TODO: move into oauthlib.common from oauth2.utils
|
||||
if (self.request_validator.enforce_ssl and
|
||||
not request.uri.lower().startswith("https://")):
|
||||
raise errors.InsecureTransportError()
|
||||
|
||||
def _check_mandatory_parameters(self, request):
|
||||
# The server SHOULD return a 400 (Bad Request) status code when
|
||||
# receiving a request with missing parameters.
|
||||
if not all((request.signature, request.client_key,
|
||||
request.nonce, request.timestamp,
|
||||
request.signature_method)):
|
||||
raise errors.InvalidRequestError(
|
||||
description='Missing mandatory 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`: https://tools.ietf.org/html/rfc5849#section-4
|
||||
if (not request.signature_method in
|
||||
self.request_validator.allowed_signature_methods):
|
||||
raise errors.InvalidSignatureMethodError(
|
||||
description="Invalid signature, {} not in {!r}.".format(
|
||||
request.signature_method,
|
||||
self.request_validator.allowed_signature_methods))
|
||||
|
||||
# Servers receiving an authenticated request MUST validate it by:
|
||||
# If the "oauth_version" parameter is present, ensuring its value is
|
||||
# "1.0".
|
||||
if ('oauth_version' in request.oauth_params and
|
||||
request.oauth_params['oauth_version'] != '1.0'):
|
||||
raise errors.InvalidRequestError(
|
||||
description='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(request.timestamp) != 10:
|
||||
raise errors.InvalidRequestError(
|
||||
description='Invalid timestamp size')
|
||||
|
||||
try:
|
||||
ts = int(request.timestamp)
|
||||
|
||||
except ValueError:
|
||||
raise errors.InvalidRequestError(
|
||||
description='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 abs(time.time() - ts) > self.request_validator.timestamp_lifetime:
|
||||
raise errors.InvalidRequestError(
|
||||
description=('Timestamp given is invalid, differ from '
|
||||
'allowed by over %s seconds.' % (
|
||||
self.request_validator.timestamp_lifetime)))
|
||||
|
||||
# Provider specific validation of parameters, used to enforce
|
||||
# restrictions such as character set and length.
|
||||
if not self.request_validator.check_client_key(request.client_key):
|
||||
raise errors.InvalidRequestError(
|
||||
description='Invalid client key format.')
|
||||
|
||||
if not self.request_validator.check_nonce(request.nonce):
|
||||
raise errors.InvalidRequestError(
|
||||
description='Invalid nonce format.')
|
||||
|
||||
def _check_signature(self, request, is_token_request=False):
|
||||
# ---- RSA Signature verification ----
|
||||
if request.signature_method == SIGNATURE_RSA_SHA1 or \
|
||||
request.signature_method == SIGNATURE_RSA_SHA256 or \
|
||||
request.signature_method == SIGNATURE_RSA_SHA512:
|
||||
# RSA-based signature method
|
||||
|
||||
# The server verifies the signature per `[RFC3447] section 8.2.2`_
|
||||
# .. _`[RFC3447] section 8.2.2`: https://tools.ietf.org/html/rfc3447#section-8.2.1
|
||||
|
||||
rsa_key = self.request_validator.get_rsa_key(
|
||||
request.client_key, request)
|
||||
|
||||
if request.signature_method == SIGNATURE_RSA_SHA1:
|
||||
valid_signature = signature.verify_rsa_sha1(request, rsa_key)
|
||||
elif request.signature_method == SIGNATURE_RSA_SHA256:
|
||||
valid_signature = signature.verify_rsa_sha256(request, rsa_key)
|
||||
elif request.signature_method == SIGNATURE_RSA_SHA512:
|
||||
valid_signature = signature.verify_rsa_sha512(request, rsa_key)
|
||||
else:
|
||||
valid_signature = False
|
||||
|
||||
# ---- HMAC or Plaintext Signature verification ----
|
||||
else:
|
||||
# Non-RSA based signature method
|
||||
|
||||
# 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`: https://tools.ietf.org/html/rfc5849#section-3.4
|
||||
|
||||
client_secret = self.request_validator.get_client_secret(
|
||||
request.client_key, request)
|
||||
|
||||
resource_owner_secret = None
|
||||
if request.resource_owner_key:
|
||||
if is_token_request:
|
||||
resource_owner_secret = \
|
||||
self.request_validator.get_request_token_secret(
|
||||
request.client_key, request.resource_owner_key,
|
||||
request)
|
||||
else:
|
||||
resource_owner_secret = \
|
||||
self.request_validator.get_access_token_secret(
|
||||
request.client_key, request.resource_owner_key,
|
||||
request)
|
||||
|
||||
if request.signature_method == SIGNATURE_HMAC_SHA1:
|
||||
valid_signature = signature.verify_hmac_sha1(
|
||||
request, client_secret, resource_owner_secret)
|
||||
elif request.signature_method == SIGNATURE_HMAC_SHA256:
|
||||
valid_signature = signature.verify_hmac_sha256(
|
||||
request, client_secret, resource_owner_secret)
|
||||
elif request.signature_method == SIGNATURE_HMAC_SHA512:
|
||||
valid_signature = signature.verify_hmac_sha512(
|
||||
request, client_secret, resource_owner_secret)
|
||||
elif request.signature_method == SIGNATURE_PLAINTEXT:
|
||||
valid_signature = signature.verify_plaintext(
|
||||
request, client_secret, resource_owner_secret)
|
||||
else:
|
||||
valid_signature = False
|
||||
|
||||
return valid_signature
|
||||
@@ -0,0 +1,14 @@
|
||||
from . import (
|
||||
AccessTokenEndpoint, AuthorizationEndpoint, RequestTokenEndpoint,
|
||||
ResourceEndpoint,
|
||||
)
|
||||
|
||||
|
||||
class WebApplicationServer(RequestTokenEndpoint, AuthorizationEndpoint,
|
||||
AccessTokenEndpoint, ResourceEndpoint):
|
||||
|
||||
def __init__(self, request_validator):
|
||||
RequestTokenEndpoint.__init__(self, request_validator)
|
||||
AuthorizationEndpoint.__init__(self, request_validator)
|
||||
AccessTokenEndpoint.__init__(self, request_validator)
|
||||
ResourceEndpoint.__init__(self, request_validator)
|
||||
@@ -0,0 +1,209 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849.endpoints.request_token
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of the request token provider logic of
|
||||
OAuth 1.0 RFC 5849. It validates the correctness of request token requests,
|
||||
creates and persists tokens as well as create the proper response to be
|
||||
returned to the client.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from oauthlib.common import urlencode
|
||||
|
||||
from .. import errors
|
||||
from .base import BaseEndpoint
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestTokenEndpoint(BaseEndpoint):
|
||||
|
||||
"""An endpoint responsible for providing OAuth 1 request tokens.
|
||||
|
||||
Typical use is to instantiate with a request validator and invoke the
|
||||
``create_request_token_response`` from a view function. The tuple returned
|
||||
has all information necessary (body, status, headers) to quickly form
|
||||
and return a proper response. See :doc:`/oauth1/validator` for details on which
|
||||
validator methods to implement for this endpoint.
|
||||
"""
|
||||
|
||||
def create_request_token(self, request, credentials):
|
||||
"""Create and save a new request token.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param credentials: A dict of extra token credentials.
|
||||
:returns: The token as an urlencoded string.
|
||||
"""
|
||||
token = {
|
||||
'oauth_token': self.token_generator(),
|
||||
'oauth_token_secret': self.token_generator(),
|
||||
'oauth_callback_confirmed': 'true'
|
||||
}
|
||||
token.update(credentials)
|
||||
self.request_validator.save_request_token(token, request)
|
||||
return urlencode(token.items())
|
||||
|
||||
def create_request_token_response(self, uri, http_method='GET', body=None,
|
||||
headers=None, credentials=None):
|
||||
"""Create a request token response, with a new request token if valid.
|
||||
|
||||
:param uri: The full URI of the token request.
|
||||
:param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
|
||||
:param body: The request body as a string.
|
||||
:param headers: The request headers as a dict.
|
||||
:param credentials: A list of extra credentials to include in the token.
|
||||
:returns: A tuple of 3 elements.
|
||||
1. A dict of headers to set on the response.
|
||||
2. The response body as a string.
|
||||
3. The response status code as an integer.
|
||||
|
||||
An example of a valid request::
|
||||
|
||||
>>> from your_validator import your_validator
|
||||
>>> from oauthlib.oauth1 import RequestTokenEndpoint
|
||||
>>> endpoint = RequestTokenEndpoint(your_validator)
|
||||
>>> h, b, s = endpoint.create_request_token_response(
|
||||
... 'https://your.provider/request_token?foo=bar',
|
||||
... headers={
|
||||
... 'Authorization': 'OAuth realm=movies user, oauth_....'
|
||||
... },
|
||||
... credentials={
|
||||
... 'my_specific': 'argument',
|
||||
... })
|
||||
>>> h
|
||||
{'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
>>> b
|
||||
'oauth_token=lsdkfol23w54jlksdef&oauth_token_secret=qwe089234lkjsdf&oauth_callback_confirmed=true&my_specific=argument'
|
||||
>>> s
|
||||
200
|
||||
|
||||
An response to invalid request would have a different body and status::
|
||||
|
||||
>>> b
|
||||
'error=invalid_request&description=missing+callback+uri'
|
||||
>>> s
|
||||
400
|
||||
|
||||
The same goes for an an unauthorized request:
|
||||
|
||||
>>> b
|
||||
''
|
||||
>>> s
|
||||
401
|
||||
"""
|
||||
resp_headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||
try:
|
||||
request = self._create_request(uri, http_method, body, headers)
|
||||
valid, processed_request = self.validate_request_token_request(
|
||||
request)
|
||||
if valid:
|
||||
token = self.create_request_token(request, credentials or {})
|
||||
return resp_headers, token, 200
|
||||
else:
|
||||
return {}, None, 401
|
||||
except errors.OAuth1Error as e:
|
||||
return resp_headers, e.urlencoded, e.status_code
|
||||
|
||||
def validate_request_token_request(self, request):
|
||||
"""Validate a request token request.
|
||||
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:raises: OAuth1Error if the request is invalid.
|
||||
:returns: A tuple of 2 elements.
|
||||
1. The validation result (True or False).
|
||||
2. The request object.
|
||||
"""
|
||||
self._check_transport_security(request)
|
||||
self._check_mandatory_parameters(request)
|
||||
|
||||
if request.realm:
|
||||
request.realms = request.realm.split(' ')
|
||||
else:
|
||||
request.realms = self.request_validator.get_default_realms(
|
||||
request.client_key, request)
|
||||
if not self.request_validator.check_realms(request.realms):
|
||||
raise errors.InvalidRequestError(
|
||||
description='Invalid realm {}. Allowed are {!r}.'.format(
|
||||
request.realms, self.request_validator.realms))
|
||||
|
||||
if not request.redirect_uri:
|
||||
raise errors.InvalidRequestError(
|
||||
description='Missing callback URI.')
|
||||
|
||||
if not self.request_validator.validate_timestamp_and_nonce(
|
||||
request.client_key, request.timestamp, request.nonce, request,
|
||||
request_token=request.resource_owner_key):
|
||||
return False, request
|
||||
|
||||
# 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.request_validator.validate_client_key(
|
||||
request.client_key, request)
|
||||
if not valid_client:
|
||||
request.client_key = self.request_validator.dummy_client
|
||||
|
||||
# Note that `realm`_ is only used in authorization headers and how
|
||||
# it should be interpreted 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`: https://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.
|
||||
# This first step (obtaining request token) need not require a realm
|
||||
# and can then be identified by checking the require_resource_owner
|
||||
# flag and absence of 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.
|
||||
valid_realm = self.request_validator.validate_requested_realms(
|
||||
request.client_key, request.realms, request)
|
||||
|
||||
# Callback is normally never required, except for requests for
|
||||
# a Temporary Credential as described in `Section 2.1`_
|
||||
# .._`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1
|
||||
valid_redirect = self.request_validator.validate_redirect_uri(
|
||||
request.client_key, request.redirect_uri, request)
|
||||
if not request.redirect_uri:
|
||||
raise NotImplementedError('Redirect URI must either be provided '
|
||||
'or set to a default during validation.')
|
||||
|
||||
valid_signature = self._check_signature(request)
|
||||
|
||||
# log the results to the validator_log
|
||||
# this lets us handle internal reporting and analysis
|
||||
request.validator_log['client'] = valid_client
|
||||
request.validator_log['realm'] = valid_realm
|
||||
request.validator_log['callback'] = valid_redirect
|
||||
request.validator_log['signature'] = valid_signature
|
||||
|
||||
# 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_realm, valid_redirect, valid_signature))
|
||||
if not v:
|
||||
log.info("[Failure] request verification failed.")
|
||||
log.info("Valid client: %s.", valid_client)
|
||||
log.info("Valid realm: %s.", valid_realm)
|
||||
log.info("Valid callback: %s.", valid_redirect)
|
||||
log.info("Valid signature: %s.", valid_signature)
|
||||
return v, request
|
||||
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849.endpoints.resource
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of the resource protection provider logic of
|
||||
OAuth 1.0 RFC 5849.
|
||||
"""
|
||||
import logging
|
||||
|
||||
from .. import errors
|
||||
from .base import BaseEndpoint
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResourceEndpoint(BaseEndpoint):
|
||||
|
||||
"""An endpoint responsible for protecting resources.
|
||||
|
||||
Typical use is to instantiate with a request validator and invoke the
|
||||
``validate_protected_resource_request`` in a decorator around a view
|
||||
function. If the request is valid, invoke and return the response of the
|
||||
view. If invalid create and return an error response directly from the
|
||||
decorator.
|
||||
|
||||
See :doc:`/oauth1/validator` for details on which validator methods to implement
|
||||
for this endpoint.
|
||||
|
||||
An example decorator::
|
||||
|
||||
from functools import wraps
|
||||
from your_validator import your_validator
|
||||
from oauthlib.oauth1 import ResourceEndpoint
|
||||
endpoint = ResourceEndpoint(your_validator)
|
||||
|
||||
def require_oauth(realms=None):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
v, r = provider.validate_protected_resource_request(
|
||||
request.url,
|
||||
http_method=request.method,
|
||||
body=request.data,
|
||||
headers=request.headers,
|
||||
realms=realms or [])
|
||||
if v:
|
||||
return f(*args, **kwargs)
|
||||
else:
|
||||
return abort(403)
|
||||
"""
|
||||
|
||||
def validate_protected_resource_request(self, uri, http_method='GET',
|
||||
body=None, headers=None, realms=None):
|
||||
"""Create a request token response, with a new request token if valid.
|
||||
|
||||
:param uri: The full URI of the token request.
|
||||
:param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
|
||||
:param body: The request body as a string.
|
||||
:param headers: The request headers as a dict.
|
||||
:param realms: A list of realms the resource is protected under.
|
||||
This will be supplied to the ``validate_realms``
|
||||
method of the request validator.
|
||||
:returns: A tuple of 2 elements.
|
||||
1. True if valid, False otherwise.
|
||||
2. An oauthlib.common.Request object.
|
||||
"""
|
||||
try:
|
||||
request = self._create_request(uri, http_method, body, headers)
|
||||
except errors.OAuth1Error:
|
||||
return False, None
|
||||
|
||||
try:
|
||||
self._check_transport_security(request)
|
||||
self._check_mandatory_parameters(request)
|
||||
except errors.OAuth1Error:
|
||||
return False, request
|
||||
|
||||
if not request.resource_owner_key:
|
||||
return False, request
|
||||
|
||||
if not self.request_validator.check_access_token(
|
||||
request.resource_owner_key):
|
||||
return False, request
|
||||
|
||||
if not self.request_validator.validate_timestamp_and_nonce(
|
||||
request.client_key, request.timestamp, request.nonce, request,
|
||||
access_token=request.resource_owner_key):
|
||||
return False, request
|
||||
|
||||
# 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.request_validator.validate_client_key(
|
||||
request.client_key, request)
|
||||
if not valid_client:
|
||||
request.client_key = self.request_validator.dummy_client
|
||||
|
||||
# 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
|
||||
valid_resource_owner = self.request_validator.validate_access_token(
|
||||
request.client_key, request.resource_owner_key, request)
|
||||
if not valid_resource_owner:
|
||||
request.resource_owner_key = self.request_validator.dummy_access_token
|
||||
|
||||
# Note that `realm`_ is only used in authorization headers and how
|
||||
# it should be interpreted 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`: https://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.
|
||||
# This first step (obtaining request token) need not require a realm
|
||||
# and can then be identified by checking the require_resource_owner
|
||||
# flag and absence of 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.
|
||||
valid_realm = self.request_validator.validate_realms(request.client_key,
|
||||
request.resource_owner_key, request, uri=request.uri,
|
||||
realms=realms)
|
||||
|
||||
valid_signature = self._check_signature(request)
|
||||
|
||||
# log the results to the validator_log
|
||||
# this lets us handle internal reporting and analysis
|
||||
request.validator_log['client'] = valid_client
|
||||
request.validator_log['resource_owner'] = valid_resource_owner
|
||||
request.validator_log['realm'] = valid_realm
|
||||
request.validator_log['signature'] = valid_signature
|
||||
|
||||
# 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_signature))
|
||||
if not v:
|
||||
log.info("[Failure] request verification failed.")
|
||||
log.info("Valid client: %s", valid_client)
|
||||
log.info("Valid token: %s", valid_resource_owner)
|
||||
log.info("Valid realm: %s", valid_realm)
|
||||
log.info("Valid signature: %s", valid_signature)
|
||||
return v, request
|
||||
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849.endpoints.signature_only
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of the signing logic of OAuth 1.0 RFC 5849.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from .. import errors
|
||||
from .base import BaseEndpoint
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SignatureOnlyEndpoint(BaseEndpoint):
|
||||
|
||||
"""An endpoint only responsible for verifying an oauth signature."""
|
||||
|
||||
def validate_request(self, uri, http_method='GET',
|
||||
body=None, headers=None):
|
||||
"""Validate a signed OAuth request.
|
||||
|
||||
:param uri: The full URI of the token request.
|
||||
:param http_method: A valid HTTP verb, i.e. GET, POST, PUT, HEAD, etc.
|
||||
:param body: The request body as a string.
|
||||
:param headers: The request headers as a dict.
|
||||
:returns: A tuple of 2 elements.
|
||||
1. True if valid, False otherwise.
|
||||
2. An oauthlib.common.Request object.
|
||||
"""
|
||||
try:
|
||||
request = self._create_request(uri, http_method, body, headers)
|
||||
except errors.OAuth1Error as err:
|
||||
log.info(
|
||||
'Exception caught while validating request, %s.' % err)
|
||||
return False, None
|
||||
|
||||
try:
|
||||
self._check_transport_security(request)
|
||||
self._check_mandatory_parameters(request)
|
||||
except errors.OAuth1Error as err:
|
||||
log.info(
|
||||
'Exception caught while validating request, %s.' % err)
|
||||
return False, request
|
||||
|
||||
if not self.request_validator.validate_timestamp_and_nonce(
|
||||
request.client_key, request.timestamp, request.nonce, request):
|
||||
log.debug('[Failure] verification failed: timestamp/nonce')
|
||||
return False, request
|
||||
|
||||
# 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.request_validator.validate_client_key(
|
||||
request.client_key, request)
|
||||
if not valid_client:
|
||||
request.client_key = self.request_validator.dummy_client
|
||||
|
||||
valid_signature = self._check_signature(request)
|
||||
|
||||
# log the results to the validator_log
|
||||
# this lets us handle internal reporting and analysis
|
||||
request.validator_log['client'] = valid_client
|
||||
request.validator_log['signature'] = valid_signature
|
||||
|
||||
# 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_signature))
|
||||
if not v:
|
||||
log.info("[Failure] request verification failed.")
|
||||
log.info("Valid client: %s", valid_client)
|
||||
log.info("Valid signature: %s", valid_signature)
|
||||
return v, request
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849.errors
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Error used both by OAuth 1 clients and provicers to represent the spec
|
||||
defined error responses for all four core grant types.
|
||||
"""
|
||||
from oauthlib.common import add_params_to_uri, urlencode
|
||||
|
||||
|
||||
class OAuth1Error(Exception):
|
||||
error = None
|
||||
description = ''
|
||||
|
||||
def __init__(self, description=None, uri=None, status_code=400,
|
||||
request=None):
|
||||
"""
|
||||
description: A human-readable ASCII [USASCII] text providing
|
||||
additional information, used to assist the client
|
||||
developer in understanding the error that occurred.
|
||||
Values for the "error_description" parameter MUST NOT
|
||||
include characters outside the set
|
||||
x20-21 / x23-5B / x5D-7E.
|
||||
|
||||
uri: A URI identifying a human-readable web page with information
|
||||
about the error, used to provide the client developer with
|
||||
additional information about the error. Values for the
|
||||
"error_uri" parameter MUST conform to the URI- Reference
|
||||
syntax, and thus MUST NOT include characters outside the set
|
||||
x21 / x23-5B / x5D-7E.
|
||||
|
||||
state: A CSRF protection value received from the client.
|
||||
|
||||
request: Oauthlib Request object
|
||||
"""
|
||||
self.description = description or self.description
|
||||
message = '({}) {}'.format(self.error, self.description)
|
||||
if request:
|
||||
message += ' ' + repr(request)
|
||||
super().__init__(message)
|
||||
|
||||
self.uri = uri
|
||||
self.status_code = status_code
|
||||
|
||||
def in_uri(self, uri):
|
||||
return add_params_to_uri(uri, self.twotuples)
|
||||
|
||||
@property
|
||||
def twotuples(self):
|
||||
error = [('error', self.error)]
|
||||
if self.description:
|
||||
error.append(('error_description', self.description))
|
||||
if self.uri:
|
||||
error.append(('error_uri', self.uri))
|
||||
return error
|
||||
|
||||
@property
|
||||
def urlencoded(self):
|
||||
return urlencode(self.twotuples)
|
||||
|
||||
|
||||
class InsecureTransportError(OAuth1Error):
|
||||
error = 'insecure_transport_protocol'
|
||||
description = 'Only HTTPS connections are permitted.'
|
||||
|
||||
|
||||
class InvalidSignatureMethodError(OAuth1Error):
|
||||
error = 'invalid_signature_method'
|
||||
|
||||
|
||||
class InvalidRequestError(OAuth1Error):
|
||||
error = 'invalid_request'
|
||||
|
||||
|
||||
class InvalidClientError(OAuth1Error):
|
||||
error = 'invalid_client'
|
||||
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
oauthlib.parameters
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains methods related to `section 3.5`_ of the OAuth 1.0a spec.
|
||||
|
||||
.. _`section 3.5`: https://tools.ietf.org/html/rfc5849#section-3.5
|
||||
"""
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
from oauthlib.common import extract_params, urlencode
|
||||
|
||||
from . import utils
|
||||
|
||||
|
||||
# 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`: https://tools.ietf.org/html/rfc5849#section-3.5.1
|
||||
.. _`RFC2617`: https://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`: https://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 = '{}="{}"'.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`: https://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`: https://tools.ietf.org/html/rfc2617#section-1.2
|
||||
if realm:
|
||||
# NOTE: realm should *not* be escaped
|
||||
authorization_header_parameters = ('realm="%s", ' % realm +
|
||||
authorization_header_parameters)
|
||||
|
||||
# the auth-scheme name set to "OAuth" (case insensitive).
|
||||
authorization_header = 'OAuth %s' % authorization_header_parameters
|
||||
|
||||
# contribute the Authorization header to the given headers
|
||||
full_headers = {}
|
||||
full_headers.update(headers)
|
||||
full_headers['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`: https://tools.ietf.org/html/rfc5849#section-3.5.2
|
||||
.. _`3.5.3`: https://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`: https://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`: https://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))
|
||||
@@ -0,0 +1,849 @@
|
||||
"""
|
||||
oauthlib.oauth1.rfc5849
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module is an implementation of various logic needed
|
||||
for signing and checking OAuth 1.0 RFC 5849 requests.
|
||||
"""
|
||||
from . import SIGNATURE_METHODS, utils
|
||||
|
||||
|
||||
class RequestValidator:
|
||||
|
||||
"""A validator/datastore interaction base class for OAuth 1 providers.
|
||||
|
||||
OAuth providers should inherit from RequestValidator 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 useful 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_realms
|
||||
|
||||
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/invalidate 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_key
|
||||
- validate_request_token
|
||||
- validate_access_token
|
||||
- validate_timestamp_and_nonce
|
||||
- validate_redirect_uri
|
||||
- validate_requested_realms
|
||||
- validate_realms
|
||||
- validate_verifier
|
||||
- invalidate_request_token
|
||||
|
||||
Methods 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
|
||||
- get_realms
|
||||
- get_default_realms
|
||||
- get_redirect_uri
|
||||
|
||||
Methods used to save credentials.
|
||||
The following methods must be implemented:
|
||||
|
||||
- save_request_token
|
||||
- save_verifier
|
||||
- save_access_token
|
||||
|
||||
Methods used to verify input parameters. This methods are used during
|
||||
authorizing request token by user (AuthorizationEndpoint), to check if
|
||||
parameters are valid. During token authorization request is not signed,
|
||||
thus 'validation' methods can not be used. The following methods must be
|
||||
implemented:
|
||||
|
||||
- verify_realms
|
||||
- verify_request_token
|
||||
|
||||
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
|
||||
|
||||
Example implementations have been provided, note that the database used is
|
||||
a simple dictionary and serves only an illustrative purpose. Use whichever
|
||||
database suits your project and how to access it is entirely up to you.
|
||||
The methods are introduced in an order which should make understanding
|
||||
their use more straightforward and as such it could be worth reading what
|
||||
follows in chronological order.
|
||||
|
||||
.. _`whitelisting or blacklisting`: https://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_realms(self, realms):
|
||||
"""Check that the realm is one of a set allowed realms."""
|
||||
return all(r in self.realms for r in realms)
|
||||
|
||||
def _subclass_must_implement(self, fn):
|
||||
"""
|
||||
Returns a NotImplementedError for a function that should be implemented.
|
||||
:param fn: name of the function
|
||||
"""
|
||||
m = "Missing function implementation in {}: {}".format(type(self), fn)
|
||||
return NotImplementedError(m)
|
||||
|
||||
@property
|
||||
def dummy_client(self):
|
||||
"""Dummy client used when an invalid client key is supplied.
|
||||
|
||||
:returns: The dummy client key string.
|
||||
|
||||
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.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
* RequestTokenEndpoint
|
||||
* ResourceEndpoint
|
||||
* SignatureOnlyEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("dummy_client")
|
||||
|
||||
@property
|
||||
def dummy_request_token(self):
|
||||
"""Dummy request token used when an invalid token was supplied.
|
||||
|
||||
:returns: The dummy request token string.
|
||||
|
||||
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.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("dummy_request_token")
|
||||
|
||||
@property
|
||||
def dummy_access_token(self):
|
||||
"""Dummy access token used when an invalid token was supplied.
|
||||
|
||||
:returns: The dummy access token string.
|
||||
|
||||
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.
|
||||
|
||||
This method is used by
|
||||
|
||||
* ResourceEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("dummy_access_token")
|
||||
|
||||
def get_client_secret(self, client_key, request):
|
||||
"""Retrieves the client secret associated with the client key.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: The client secret as a string.
|
||||
|
||||
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::
|
||||
|
||||
# Unlikely to be near constant time as it uses two database
|
||||
# lookups for a valid client, and only one for an invalid.
|
||||
from your_datastore import ClientSecret
|
||||
if ClientSecret.has(client_key):
|
||||
return ClientSecret.get(client_key)
|
||||
else:
|
||||
return 'dummy'
|
||||
|
||||
# Aim to mimic number of latency inducing operations no matter
|
||||
# whether the client is valid or not.
|
||||
from your_datastore import ClientSecret
|
||||
return ClientSecret.get(client_key, 'dummy')
|
||||
|
||||
Note that the returned key must be in plaintext.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
* RequestTokenEndpoint
|
||||
* ResourceEndpoint
|
||||
* SignatureOnlyEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement('get_client_secret')
|
||||
|
||||
def get_request_token_secret(self, client_key, token, request):
|
||||
"""Retrieves the shared secret associated with the request token.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param token: The request token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: The token secret as a string.
|
||||
|
||||
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::
|
||||
|
||||
# Unlikely to be near constant time as it uses two database
|
||||
# lookups for a valid client, and only one for an invalid.
|
||||
from your_datastore import RequestTokenSecret
|
||||
if RequestTokenSecret.has(client_key):
|
||||
return RequestTokenSecret.get((client_key, request_token))
|
||||
else:
|
||||
return 'dummy'
|
||||
|
||||
# Aim to mimic number of latency inducing operations no matter
|
||||
# whether the client is valid or not.
|
||||
from your_datastore import RequestTokenSecret
|
||||
return ClientSecret.get((client_key, request_token), 'dummy')
|
||||
|
||||
Note that the returned key must be in plaintext.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement('get_request_token_secret')
|
||||
|
||||
def get_access_token_secret(self, client_key, token, request):
|
||||
"""Retrieves the shared secret associated with the access token.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param token: The access token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: The token secret as a string.
|
||||
|
||||
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::
|
||||
|
||||
# Unlikely to be near constant time as it uses two database
|
||||
# lookups for a valid client, and only one for an invalid.
|
||||
from your_datastore import AccessTokenSecret
|
||||
if AccessTokenSecret.has(client_key):
|
||||
return AccessTokenSecret.get((client_key, request_token))
|
||||
else:
|
||||
return 'dummy'
|
||||
|
||||
# Aim to mimic number of latency inducing operations no matter
|
||||
# whether the client is valid or not.
|
||||
from your_datastore import AccessTokenSecret
|
||||
return ClientSecret.get((client_key, request_token), 'dummy')
|
||||
|
||||
Note that the returned key must be in plaintext.
|
||||
|
||||
This method is used by
|
||||
|
||||
* ResourceEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("get_access_token_secret")
|
||||
|
||||
def get_default_realms(self, client_key, request):
|
||||
"""Get the default realms for a client.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: The list of default realms associated with the client.
|
||||
|
||||
The list of default realms will be set during client registration and
|
||||
is outside the scope of OAuthLib.
|
||||
|
||||
This method is used by
|
||||
|
||||
* RequestTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("get_default_realms")
|
||||
|
||||
def get_realms(self, token, request):
|
||||
"""Get realms associated with a request token.
|
||||
|
||||
:param token: The request token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: The list of realms associated with the request token.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AuthorizationEndpoint
|
||||
* AccessTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("get_realms")
|
||||
|
||||
def get_redirect_uri(self, token, request):
|
||||
"""Get the redirect URI associated with a request token.
|
||||
|
||||
:param token: The request token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: The redirect URI associated with the request token.
|
||||
|
||||
It may be desirable to return a custom URI if the redirect is set to "oob".
|
||||
In this case, the user will be redirected to the returned URI and at that
|
||||
endpoint the verifier can be displayed.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AuthorizationEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("get_redirect_uri")
|
||||
|
||||
def get_rsa_key(self, client_key, request):
|
||||
"""Retrieves a previously stored client provided RSA key.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: The rsa public key as a string.
|
||||
|
||||
This method must allow the use of a dummy client_key value. Fetching
|
||||
the rsa key using the dummy key must take the same amount of time
|
||||
as fetching a key for a valid client. The dummy key must also be of
|
||||
the same bit length as client keys.
|
||||
|
||||
Note that the key must be returned in plaintext.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
* RequestTokenEndpoint
|
||||
* ResourceEndpoint
|
||||
* SignatureOnlyEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("get_rsa_key")
|
||||
|
||||
def invalidate_request_token(self, client_key, request_token, request):
|
||||
"""Invalidates a used request token.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param request_token: The request token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: None
|
||||
|
||||
Per `Section 2.3`_ of the spec:
|
||||
|
||||
"The server MUST (...) ensure that the temporary
|
||||
credentials have not expired or been used before."
|
||||
|
||||
.. _`Section 2.3`: https://tools.ietf.org/html/rfc5849#section-2.3
|
||||
|
||||
This method should ensure that provided token won't validate anymore.
|
||||
It can be simply removing RequestToken from storage or setting
|
||||
specific flag that makes it invalid (note that such flag should be
|
||||
also validated during request token validation).
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("invalidate_request_token")
|
||||
|
||||
def validate_client_key(self, client_key, request):
|
||||
"""Validates that supplied client key is a registered and valid client.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
Note that if the dummy client is supplied it should validate in same
|
||||
or nearly the same amount of time as a valid one.
|
||||
|
||||
Ensure latency inducing tasks are mimiced even for dummy clients.
|
||||
For example, use::
|
||||
|
||||
from your_datastore import Client
|
||||
try:
|
||||
return Client.exists(client_key, access_token)
|
||||
except DoesNotExist:
|
||||
return False
|
||||
|
||||
Rather than::
|
||||
|
||||
from your_datastore import Client
|
||||
if access_token == self.dummy_access_token:
|
||||
return False
|
||||
else:
|
||||
return Client.exists(client_key, access_token)
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
* RequestTokenEndpoint
|
||||
* ResourceEndpoint
|
||||
* SignatureOnlyEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("validate_client_key")
|
||||
|
||||
def validate_request_token(self, client_key, token, request):
|
||||
"""Validates that supplied request token is registered and valid.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param token: The request token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
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.
|
||||
|
||||
Ensure latency inducing tasks are mimiced even for dummy clients.
|
||||
For example, use::
|
||||
|
||||
from your_datastore import RequestToken
|
||||
try:
|
||||
return RequestToken.exists(client_key, access_token)
|
||||
except DoesNotExist:
|
||||
return False
|
||||
|
||||
Rather than::
|
||||
|
||||
from your_datastore import RequestToken
|
||||
if access_token == self.dummy_access_token:
|
||||
return False
|
||||
else:
|
||||
return RequestToken.exists(client_key, access_token)
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("validate_request_token")
|
||||
|
||||
def validate_access_token(self, client_key, token, request):
|
||||
"""Validates that supplied access token is registered and valid.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param token: The access token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
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.
|
||||
|
||||
Ensure latency inducing tasks are mimiced even for dummy clients.
|
||||
For example, use::
|
||||
|
||||
from your_datastore import AccessToken
|
||||
try:
|
||||
return AccessToken.exists(client_key, access_token)
|
||||
except DoesNotExist:
|
||||
return False
|
||||
|
||||
Rather than::
|
||||
|
||||
from your_datastore import AccessToken
|
||||
if access_token == self.dummy_access_token:
|
||||
return False
|
||||
else:
|
||||
return AccessToken.exists(client_key, access_token)
|
||||
|
||||
This method is used by
|
||||
|
||||
* ResourceEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("validate_access_token")
|
||||
|
||||
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
|
||||
request, request_token=None, access_token=None):
|
||||
"""Validates that the nonce has not been used before.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param timestamp: The ``oauth_timestamp`` parameter.
|
||||
:param nonce: The ``oauth_nonce`` parameter.
|
||||
:param request_token: Request token string, if any.
|
||||
:param access_token: Access token string, if any.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
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`: https://tools.ietf.org/html/rfc5849#section-3.3
|
||||
|
||||
One of the first validation checks that will be made is for the validity
|
||||
of the nonce and timestamp, which are associated with a client key and
|
||||
possibly a token. If invalid then immediately fail the request
|
||||
by returning False. If the nonce/timestamp pair has been used before and
|
||||
you may just have detected a replay attack. Therefore it is an essential
|
||||
part of OAuth security that you not allow nonce/timestamp reuse.
|
||||
Note that this validation check is done before checking the validity of
|
||||
the client and token.::
|
||||
|
||||
nonces_and_timestamps_database = [
|
||||
(u'foo', 1234567890, u'rannoMstrInghere', u'bar')
|
||||
]
|
||||
|
||||
def validate_timestamp_and_nonce(self, client_key, timestamp, nonce,
|
||||
request_token=None, access_token=None):
|
||||
|
||||
return ((client_key, timestamp, nonce, request_token or access_token)
|
||||
not in self.nonces_and_timestamps_database)
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
* RequestTokenEndpoint
|
||||
* ResourceEndpoint
|
||||
* SignatureOnlyEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("validate_timestamp_and_nonce")
|
||||
|
||||
def validate_redirect_uri(self, client_key, redirect_uri, request):
|
||||
"""Validates the client supplied redirection URI.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param redirect_uri: The URI the client which to redirect back to after
|
||||
authorization is successful.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
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.
|
||||
|
||||
Alternatively per `Section 2.1`_ of the spec:
|
||||
|
||||
"If the client is unable to receive callbacks or a callback URI has
|
||||
been established via other means, the parameter value MUST be set to
|
||||
"oob" (case sensitive), to indicate an out-of-band configuration."
|
||||
|
||||
.. _`CWE-601`: http://cwe.mitre.org/top25/index.html#CWE-601
|
||||
.. _`Section 2.1`: https://tools.ietf.org/html/rfc5849#section-2.1
|
||||
|
||||
This method is used by
|
||||
|
||||
* RequestTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("validate_redirect_uri")
|
||||
|
||||
def validate_requested_realms(self, client_key, realms, request):
|
||||
"""Validates that the client may request access to the realm.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param realms: The list of realms that client is requesting access to.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
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.
|
||||
|
||||
This method is used by
|
||||
|
||||
* RequestTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("validate_requested_realms")
|
||||
|
||||
def validate_realms(self, client_key, token, request, uri=None,
|
||||
realms=None):
|
||||
"""Validates access to the request realm.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param token: A request token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:param uri: The URI the realms is protecting.
|
||||
:param realms: A list of realms that must have been granted to
|
||||
the access token.
|
||||
:returns: True or False
|
||||
|
||||
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".
|
||||
|
||||
realms is a convenience parameter which can be used to provide
|
||||
a per view method pre-defined list of allowed realms.
|
||||
|
||||
Can be as simple as::
|
||||
|
||||
from your_datastore import RequestToken
|
||||
request_token = RequestToken.get(token, None)
|
||||
|
||||
if not request_token:
|
||||
return False
|
||||
return set(request_token.realms).issuperset(set(realms))
|
||||
|
||||
This method is used by
|
||||
|
||||
* ResourceEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("validate_realms")
|
||||
|
||||
def validate_verifier(self, client_key, token, verifier, request):
|
||||
"""Validates a verification code.
|
||||
|
||||
:param client_key: The client/consumer key.
|
||||
:param token: A request token string.
|
||||
:param verifier: The authorization verifier string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
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.
|
||||
|
||||
Verifier validation should be done in near constant time
|
||||
(to avoid verifier enumeration). To achieve this we need a
|
||||
constant time string comparison which is provided by OAuthLib
|
||||
in ``oauthlib.common.safe_string_equals``::
|
||||
|
||||
from your_datastore import Verifier
|
||||
correct_verifier = Verifier.get(client_key, request_token)
|
||||
from oauthlib.common import safe_string_equals
|
||||
return safe_string_equals(verifier, correct_verifier)
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("validate_verifier")
|
||||
|
||||
def verify_request_token(self, token, request):
|
||||
"""Verify that the given OAuth1 request token is valid.
|
||||
|
||||
:param token: A request token string.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
This method is used only in AuthorizationEndpoint to check whether the
|
||||
oauth_token given in the authorization URL is valid or not.
|
||||
This request is not signed and thus similar ``validate_request_token``
|
||||
method can not be used.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AuthorizationEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("verify_request_token")
|
||||
|
||||
def verify_realms(self, token, realms, request):
|
||||
"""Verify authorized realms to see if they match those given to token.
|
||||
|
||||
:param token: An access token string.
|
||||
:param realms: A list of realms the client attempts to access.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
:returns: True or False
|
||||
|
||||
This prevents the list of authorized realms sent by the client during
|
||||
the authorization step to be altered to include realms outside what
|
||||
was bound with the request token.
|
||||
|
||||
Can be as simple as::
|
||||
|
||||
valid_realms = self.get_realms(token)
|
||||
return all((r in valid_realms for r in realms))
|
||||
|
||||
This method is used by
|
||||
|
||||
* AuthorizationEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("verify_realms")
|
||||
|
||||
def save_access_token(self, token, request):
|
||||
"""Save an OAuth1 access token.
|
||||
|
||||
:param token: A dict with token credentials.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
The token dictionary will at minimum include
|
||||
|
||||
* ``oauth_token`` the access token string.
|
||||
* ``oauth_token_secret`` the token specific secret used in signing.
|
||||
* ``oauth_authorized_realms`` a space separated list of realms.
|
||||
|
||||
Client key can be obtained from ``request.client_key``.
|
||||
|
||||
The list of realms (not joined string) can be obtained from
|
||||
``request.realm``.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AccessTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("save_access_token")
|
||||
|
||||
def save_request_token(self, token, request):
|
||||
"""Save an OAuth1 request token.
|
||||
|
||||
:param token: A dict with token credentials.
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
The token dictionary will at minimum include
|
||||
|
||||
* ``oauth_token`` the request token string.
|
||||
* ``oauth_token_secret`` the token specific secret used in signing.
|
||||
* ``oauth_callback_confirmed`` the string ``true``.
|
||||
|
||||
Client key can be obtained from ``request.client_key``.
|
||||
|
||||
This method is used by
|
||||
|
||||
* RequestTokenEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("save_request_token")
|
||||
|
||||
def save_verifier(self, token, verifier, request):
|
||||
"""Associate an authorization verifier with a request token.
|
||||
|
||||
:param token: A request token string.
|
||||
:param verifier: A dictionary containing the oauth_verifier and
|
||||
oauth_token
|
||||
:param request: OAuthlib request.
|
||||
:type request: oauthlib.common.Request
|
||||
|
||||
We need to associate verifiers with tokens for validation during the
|
||||
access token request.
|
||||
|
||||
Note that unlike save_x_token token here is the ``oauth_token`` token
|
||||
string from the request token saved previously.
|
||||
|
||||
This method is used by
|
||||
|
||||
* AuthorizationEndpoint
|
||||
"""
|
||||
raise self._subclass_must_implement("save_verifier")
|
||||
@@ -0,0 +1,852 @@
|
||||
"""
|
||||
This module is an implementation of `section 3.4`_ of RFC 5849.
|
||||
|
||||
**Usage**
|
||||
|
||||
Steps for signing a request:
|
||||
|
||||
1. Collect parameters from the request using ``collect_parameters``.
|
||||
2. Normalize those parameters using ``normalize_parameters``.
|
||||
3. Create the *base string URI* using ``base_string_uri``.
|
||||
4. Create the *signature base string* from the above three components
|
||||
using ``signature_base_string``.
|
||||
5. Pass the *signature base string* and the client credentials to one of the
|
||||
sign-with-client functions. The HMAC-based signing functions needs
|
||||
client credentials with secrets. The RSA-based signing functions needs
|
||||
client credentials with an RSA private key.
|
||||
|
||||
To verify a request, pass the request and credentials to one of the verify
|
||||
functions. The HMAC-based signing functions needs the shared secrets. The
|
||||
RSA-based verify functions needs the RSA public key.
|
||||
|
||||
**Scope**
|
||||
|
||||
All of the functions in this module should be considered internal to OAuthLib,
|
||||
since they are not imported into the "oauthlib.oauth1" module. Programs using
|
||||
OAuthLib should not use directly invoke any of the functions in this module.
|
||||
|
||||
**Deprecated functions**
|
||||
|
||||
The "sign_" methods that are not "_with_client" have been deprecated. They may
|
||||
be removed in a future release. Since they are all internal functions, this
|
||||
should have no impact on properly behaving programs.
|
||||
|
||||
.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import hashlib
|
||||
import hmac
|
||||
import ipaddress
|
||||
import logging
|
||||
import urllib.parse as urlparse
|
||||
import warnings
|
||||
|
||||
from oauthlib.common import extract_params, safe_string_equals, urldecode
|
||||
|
||||
from . import utils
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ==== Common functions ==========================================
|
||||
|
||||
def signature_base_string(
|
||||
http_method: str,
|
||||
base_str_uri: str,
|
||||
normalized_encoded_request_parameters: str) -> str:
|
||||
"""
|
||||
Construct the signature base string.
|
||||
|
||||
The *signature base string* is the value that is calculated and signed by
|
||||
the client. It is also independently calculated by the server to verify
|
||||
the signature, and therefore must produce the exact same value at both
|
||||
ends or the signature won't verify.
|
||||
|
||||
The rules for calculating the *signature base string* are defined in
|
||||
section 3.4.1.1`_ of RFC 5849.
|
||||
|
||||
.. _`section 3.4.1.1`: https://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`: https://tools.ietf.org/html/rfc5849#section-3.6
|
||||
base_string = utils.escape(http_method.upper())
|
||||
|
||||
# 2. An "&" character (ASCII code 38).
|
||||
base_string += '&'
|
||||
|
||||
# 3. The base string URI from `Section 3.4.1.2`_, after being encoded
|
||||
# (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
||||
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
|
||||
base_string += utils.escape(base_str_uri)
|
||||
|
||||
# 4. An "&" character (ASCII code 38).
|
||||
base_string += '&'
|
||||
|
||||
# 5. The request parameters as normalized in `Section 3.4.1.3.2`_, after
|
||||
# being encoded (`Section 3.6`).
|
||||
#
|
||||
# .. _`Sec 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
|
||||
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
|
||||
base_string += utils.escape(normalized_encoded_request_parameters)
|
||||
|
||||
return base_string
|
||||
|
||||
|
||||
def base_string_uri(uri: str, host: str = None) -> str:
|
||||
"""
|
||||
Calculates the _base string URI_.
|
||||
|
||||
The *base string URI* is one of the components that make up the
|
||||
*signature base string*.
|
||||
|
||||
The ``host`` is optional. If provided, it is used to override any host and
|
||||
port values in the ``uri``. The value for ``host`` is usually extracted from
|
||||
the "Host" request header from the HTTP request. Its value may be just the
|
||||
hostname, or the hostname followed by a colon and a TCP/IP port number
|
||||
(hostname:port). If a value for the``host`` is provided but it does not
|
||||
contain a port number, the default port number is used (i.e. if the ``uri``
|
||||
contained a port number, it will be discarded).
|
||||
|
||||
The rules for calculating the *base string URI* are defined in
|
||||
section 3.4.1.2`_ of RFC 5849.
|
||||
|
||||
.. _`section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
|
||||
|
||||
:param uri: URI
|
||||
:param host: hostname with optional port number, separated by a colon
|
||||
:return: base string URI
|
||||
"""
|
||||
|
||||
if not isinstance(uri, str):
|
||||
raise ValueError('uri must be a string.')
|
||||
|
||||
# FIXME: urlparse does not support unicode
|
||||
output = urlparse.urlparse(uri)
|
||||
scheme = output.scheme
|
||||
hostname = output.hostname
|
||||
port = output.port
|
||||
path = output.path
|
||||
params = output.params
|
||||
|
||||
# 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:
|
||||
#
|
||||
# .. _`RFC3986`: https://tools.ietf.org/html/rfc3986
|
||||
|
||||
if not scheme:
|
||||
raise ValueError('missing scheme')
|
||||
|
||||
# Per `RFC 2616 section 5.1.2`_:
|
||||
#
|
||||
# Note that the absolute path cannot be empty; if none is present in
|
||||
# the original URI, it MUST be given as "/" (the server root).
|
||||
#
|
||||
# .. _`RFC 2616 5.1.2`: https://tools.ietf.org/html/rfc2616#section-5.1.2
|
||||
if not path:
|
||||
path = '/'
|
||||
|
||||
# 1. The scheme and host MUST be in lowercase.
|
||||
scheme = scheme.lower()
|
||||
# Note: if ``host`` is used, it will be converted to lowercase below
|
||||
if hostname is not None:
|
||||
hostname = hostname.lower()
|
||||
|
||||
# 2. The host and port values MUST match the content of the HTTP
|
||||
# request "Host" header field.
|
||||
if host is not None:
|
||||
# NOTE: override value in uri with provided host
|
||||
# Host argument is equal to netloc. It means it's missing scheme.
|
||||
# Add it back, before parsing.
|
||||
|
||||
host = host.lower()
|
||||
host = f"{scheme}://{host}"
|
||||
output = urlparse.urlparse(host)
|
||||
hostname = output.hostname
|
||||
port = output.port
|
||||
|
||||
# 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`: https://tools.ietf.org/html/rfc2616
|
||||
# .. _`RFC2818`: https://tools.ietf.org/html/rfc2818
|
||||
|
||||
if hostname is None:
|
||||
raise ValueError('missing host')
|
||||
|
||||
# NOTE: Try guessing if we're dealing with IP or hostname
|
||||
try:
|
||||
hostname = ipaddress.ip_address(hostname)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if isinstance(hostname, ipaddress.IPv6Address):
|
||||
hostname = f"[{hostname}]"
|
||||
elif isinstance(hostname, ipaddress.IPv4Address):
|
||||
hostname = f"{hostname}"
|
||||
|
||||
if port is not None and not (0 < port <= 65535):
|
||||
raise ValueError('port out of range') # 16-bit unsigned ints
|
||||
if (scheme, port) in (('http', 80), ('https', 443)):
|
||||
netloc = hostname # default port for scheme: exclude port num
|
||||
elif port:
|
||||
netloc = f"{hostname}:{port}" # use hostname:port
|
||||
else:
|
||||
netloc = hostname
|
||||
|
||||
v = urlparse.urlunparse((scheme, netloc, path, params, '', ''))
|
||||
|
||||
# RFC 5849 does not specify which characters are encoded in the
|
||||
# "base string URI", nor how they are encoded - which is very bad, since
|
||||
# the signatures won't match if there are any differences. Fortunately,
|
||||
# most URIs only use characters that are clearly not encoded (e.g. digits
|
||||
# and A-Z, a-z), so have avoided any differences between implementations.
|
||||
#
|
||||
# The example from its section 3.4.1.2 illustrates that spaces in
|
||||
# the path are percent encoded. But it provides no guidance as to what other
|
||||
# characters (if any) must be encoded (nor how); nor if characters in the
|
||||
# other components are to be encoded or not.
|
||||
#
|
||||
# This implementation **assumes** that **only** the space is percent-encoded
|
||||
# and it is done to the entire value (not just to spaces in the path).
|
||||
#
|
||||
# This code may need to be changed if it is discovered that other characters
|
||||
# are expected to be encoded.
|
||||
#
|
||||
# Note: the "base string URI" returned by this function will be encoded
|
||||
# again before being concatenated into the "signature base string". So any
|
||||
# spaces in the URI will actually appear in the "signature base string"
|
||||
# as "%2520" (the "%20" further encoded according to section 3.6).
|
||||
|
||||
return v.replace(' ', '%20')
|
||||
|
||||
|
||||
def collect_parameters(uri_query='', body=None, headers=None,
|
||||
exclude_oauth_signature=True, with_realm=False):
|
||||
"""
|
||||
Gather the request parameters from all the parameter sources.
|
||||
|
||||
This function is used to extract all the parameters, which are then passed
|
||||
to ``normalize_parameters`` to produce one of the components that make up
|
||||
the *signature base string*.
|
||||
|
||||
Parameters starting with `oauth_` will be unescaped.
|
||||
|
||||
Body parameters must be supplied as a dict, a list of 2-tuples, or a
|
||||
form encoded query string.
|
||||
|
||||
Headers must be supplied as a dict.
|
||||
|
||||
The rules where the parameters must be sourced from are defined in
|
||||
`section 3.4.1.3.1`_ of RFC 5849.
|
||||
|
||||
.. _`Sec 3.4.1.3.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.1
|
||||
"""
|
||||
if body is None:
|
||||
body = []
|
||||
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
|
||||
# `W3C-HTML-4.0`_, Section 17.13.4.
|
||||
#
|
||||
# .. _`RFC3986, Sec 3.4`: https://tools.ietf.org/html/rfc3986#section-3.4
|
||||
# .. _`W3C-HTML-4.0`: https://www.w3.org/TR/1998/REC-html40-19980424/
|
||||
if uri_query:
|
||||
params.extend(urldecode(uri_query))
|
||||
|
||||
# * 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`: https://tools.ietf.org/html/rfc5849#section-3.5.1
|
||||
if headers:
|
||||
headers_lower = {k.lower(): v for k, v in headers.items()}
|
||||
authorization_header = headers_lower.get('authorization')
|
||||
if authorization_header is not None:
|
||||
params.extend([i for i in utils.parse_authorization_header(
|
||||
authorization_header) if with_realm or i[0] != '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 `W3C-HTML-4.0`_.
|
||||
|
||||
# * The HTTP request entity-header includes the "Content-Type"
|
||||
# header field set to "application/x-www-form-urlencoded".
|
||||
#
|
||||
# .. _`W3C-HTML-4.0`: https://www.w3.org/TR/1998/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('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 = list(filter(lambda i: i[0] != 'oauth_signature',
|
||||
unescaped_params))
|
||||
|
||||
return unescaped_params
|
||||
|
||||
|
||||
def normalize_parameters(params) -> str:
|
||||
"""
|
||||
Calculate the normalized request parameters.
|
||||
|
||||
The *normalized request parameters* is one of the components that make up
|
||||
the *signature base string*.
|
||||
|
||||
The rules for parameter normalization are defined in `section 3.4.1.3.2`_ of
|
||||
RFC 5849.
|
||||
|
||||
.. _`Sec 3.4.1.3.2`: https://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`: https://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`: https://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 = ['{}={}'.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 '&'.join(parameter_parts)
|
||||
|
||||
|
||||
# ==== Common functions for HMAC-based signature methods =========
|
||||
|
||||
def _sign_hmac(hash_algorithm_name: str,
|
||||
sig_base_str: str,
|
||||
client_secret: str,
|
||||
resource_owner_secret: str):
|
||||
"""
|
||||
**HMAC-SHA256**
|
||||
|
||||
The "HMAC-SHA256" signature method uses the HMAC-SHA256 signature
|
||||
algorithm as defined in `RFC4634`_::
|
||||
|
||||
digest = HMAC-SHA256 (key, text)
|
||||
|
||||
Per `section 3.4.2`_ of the spec.
|
||||
|
||||
.. _`RFC4634`: https://tools.ietf.org/html/rfc4634
|
||||
.. _`section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
|
||||
"""
|
||||
|
||||
# The HMAC-SHA256 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`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
|
||||
text = sig_base_str
|
||||
|
||||
# key is set to the concatenated values of:
|
||||
# 1. The client shared-secret, after being encoded (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
|
||||
key = utils.escape(client_secret or '')
|
||||
|
||||
# 2. An "&" character (ASCII code 38), which MUST be included
|
||||
# even when either secret is empty.
|
||||
key += '&'
|
||||
|
||||
# 3. The token shared-secret, after being encoded (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
|
||||
key += utils.escape(resource_owner_secret or '')
|
||||
|
||||
# Get the hashing algorithm to use
|
||||
|
||||
m = {
|
||||
'SHA-1': hashlib.sha1,
|
||||
'SHA-256': hashlib.sha256,
|
||||
'SHA-512': hashlib.sha512,
|
||||
}
|
||||
hash_alg = m[hash_algorithm_name]
|
||||
|
||||
# Calculate the signature
|
||||
|
||||
# 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, hash_alg)
|
||||
|
||||
# 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, Sec 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
|
||||
return binascii.b2a_base64(signature.digest())[:-1].decode('utf-8')
|
||||
|
||||
|
||||
def _verify_hmac(hash_algorithm_name: str,
|
||||
request,
|
||||
client_secret=None,
|
||||
resource_owner_secret=None):
|
||||
"""Verify a HMAC-SHA1 signature.
|
||||
|
||||
Per `section 3.4`_ of the spec.
|
||||
|
||||
.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
|
||||
|
||||
To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
|
||||
attribute MUST be an absolute URI whose netloc part identifies the
|
||||
origin server or gateway on which the resource resides. Any Host
|
||||
item of the request argument's headers dict attribute will be
|
||||
ignored.
|
||||
|
||||
.. _`RFC2616 section 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
|
||||
|
||||
"""
|
||||
norm_params = normalize_parameters(request.params)
|
||||
bs_uri = base_string_uri(request.uri)
|
||||
sig_base_str = signature_base_string(request.http_method, bs_uri,
|
||||
norm_params)
|
||||
signature = _sign_hmac(hash_algorithm_name, sig_base_str,
|
||||
client_secret, resource_owner_secret)
|
||||
match = safe_string_equals(signature, request.signature)
|
||||
if not match:
|
||||
log.debug('Verify HMAC failed: signature base string: %s', sig_base_str)
|
||||
return match
|
||||
|
||||
|
||||
# ==== HMAC-SHA1 =================================================
|
||||
|
||||
def sign_hmac_sha1_with_client(sig_base_str, client):
|
||||
return _sign_hmac('SHA-1', sig_base_str,
|
||||
client.client_secret, client.resource_owner_secret)
|
||||
|
||||
|
||||
def verify_hmac_sha1(request, client_secret=None, resource_owner_secret=None):
|
||||
return _verify_hmac('SHA-1', request, client_secret, resource_owner_secret)
|
||||
|
||||
|
||||
def sign_hmac_sha1(base_string, client_secret, resource_owner_secret):
|
||||
"""
|
||||
Deprecated function for calculating a HMAC-SHA1 signature.
|
||||
|
||||
This function has been replaced by invoking ``sign_hmac`` with "SHA-1"
|
||||
as the hash algorithm name.
|
||||
|
||||
This function was invoked by sign_hmac_sha1_with_client and
|
||||
test_signatures.py, but does any application invoke it directly? If not,
|
||||
it can be removed.
|
||||
"""
|
||||
warnings.warn('use sign_hmac_sha1_with_client instead of sign_hmac_sha1',
|
||||
DeprecationWarning)
|
||||
|
||||
# For some unknown reason, the original implementation assumed base_string
|
||||
# could either be bytes or str. The signature base string calculating
|
||||
# function always returned a str, so the new ``sign_rsa`` only expects that.
|
||||
|
||||
base_string = base_string.decode('ascii') \
|
||||
if isinstance(base_string, bytes) else base_string
|
||||
|
||||
return _sign_hmac('SHA-1', base_string,
|
||||
client_secret, resource_owner_secret)
|
||||
|
||||
|
||||
# ==== HMAC-SHA256 ===============================================
|
||||
|
||||
def sign_hmac_sha256_with_client(sig_base_str, client):
|
||||
return _sign_hmac('SHA-256', sig_base_str,
|
||||
client.client_secret, client.resource_owner_secret)
|
||||
|
||||
|
||||
def verify_hmac_sha256(request, client_secret=None, resource_owner_secret=None):
|
||||
return _verify_hmac('SHA-256', request,
|
||||
client_secret, resource_owner_secret)
|
||||
|
||||
|
||||
def sign_hmac_sha256(base_string, client_secret, resource_owner_secret):
|
||||
"""
|
||||
Deprecated function for calculating a HMAC-SHA256 signature.
|
||||
|
||||
This function has been replaced by invoking ``sign_hmac`` with "SHA-256"
|
||||
as the hash algorithm name.
|
||||
|
||||
This function was invoked by sign_hmac_sha256_with_client and
|
||||
test_signatures.py, but does any application invoke it directly? If not,
|
||||
it can be removed.
|
||||
"""
|
||||
warnings.warn(
|
||||
'use sign_hmac_sha256_with_client instead of sign_hmac_sha256',
|
||||
DeprecationWarning)
|
||||
|
||||
# For some unknown reason, the original implementation assumed base_string
|
||||
# could either be bytes or str. The signature base string calculating
|
||||
# function always returned a str, so the new ``sign_rsa`` only expects that.
|
||||
|
||||
base_string = base_string.decode('ascii') \
|
||||
if isinstance(base_string, bytes) else base_string
|
||||
|
||||
return _sign_hmac('SHA-256', base_string,
|
||||
client_secret, resource_owner_secret)
|
||||
|
||||
|
||||
# ==== HMAC-SHA512 ===============================================
|
||||
|
||||
def sign_hmac_sha512_with_client(sig_base_str: str,
|
||||
client):
|
||||
return _sign_hmac('SHA-512', sig_base_str,
|
||||
client.client_secret, client.resource_owner_secret)
|
||||
|
||||
|
||||
def verify_hmac_sha512(request,
|
||||
client_secret: str = None,
|
||||
resource_owner_secret: str = None):
|
||||
return _verify_hmac('SHA-512', request,
|
||||
client_secret, resource_owner_secret)
|
||||
|
||||
|
||||
# ==== Common functions for RSA-based signature methods ==========
|
||||
|
||||
_jwt_rsa = {} # cache of RSA-hash implementations from PyJWT jwt.algorithms
|
||||
|
||||
|
||||
def _get_jwt_rsa_algorithm(hash_algorithm_name: str):
|
||||
"""
|
||||
Obtains an RSAAlgorithm object that implements RSA with the hash algorithm.
|
||||
|
||||
This method maintains the ``_jwt_rsa`` cache.
|
||||
|
||||
Returns a jwt.algorithm.RSAAlgorithm.
|
||||
"""
|
||||
if hash_algorithm_name in _jwt_rsa:
|
||||
# Found in cache: return it
|
||||
return _jwt_rsa[hash_algorithm_name]
|
||||
else:
|
||||
# Not in cache: instantiate a new RSAAlgorithm
|
||||
|
||||
# PyJWT has some nice pycrypto/cryptography abstractions
|
||||
import jwt.algorithms as jwt_algorithms
|
||||
m = {
|
||||
'SHA-1': jwt_algorithms.hashes.SHA1,
|
||||
'SHA-256': jwt_algorithms.hashes.SHA256,
|
||||
'SHA-512': jwt_algorithms.hashes.SHA512,
|
||||
}
|
||||
v = jwt_algorithms.RSAAlgorithm(m[hash_algorithm_name])
|
||||
|
||||
_jwt_rsa[hash_algorithm_name] = v # populate cache
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def _prepare_key_plus(alg, keystr):
|
||||
"""
|
||||
Prepare a PEM encoded key (public or private), by invoking the `prepare_key`
|
||||
method on alg with the keystr.
|
||||
|
||||
The keystr should be a string or bytes. If the keystr is bytes, it is
|
||||
decoded as UTF-8 before being passed to prepare_key. Otherwise, it
|
||||
is passed directly.
|
||||
"""
|
||||
if isinstance(keystr, bytes):
|
||||
keystr = keystr.decode('utf-8')
|
||||
return alg.prepare_key(keystr)
|
||||
|
||||
|
||||
def _sign_rsa(hash_algorithm_name: str,
|
||||
sig_base_str: str,
|
||||
rsa_private_key: str):
|
||||
"""
|
||||
Calculate the signature for an RSA-based signature method.
|
||||
|
||||
The ``alg`` is used to calculate the digest over the signature base string.
|
||||
For the "RSA_SHA1" signature method, the alg must be SHA-1. While OAuth 1.0a
|
||||
only defines the RSA-SHA1 signature method, this function can be used for
|
||||
other non-standard signature methods that only differ from RSA-SHA1 by the
|
||||
digest algorithm.
|
||||
|
||||
Signing for the RSA-SHA1 signature method is defined in
|
||||
`section 3.4.3`_ of RFC 5849.
|
||||
|
||||
The RSASSA-PKCS1-v1_5 signature algorithm used defined by
|
||||
`RFC3447, Section 8.2`_ (also known as PKCS#1), with the `alg` 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).
|
||||
|
||||
.. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
|
||||
.. _`RFC3447, Section 8.2`: https://tools.ietf.org/html/rfc3447#section-8.2
|
||||
"""
|
||||
|
||||
# Get the implementation of RSA-hash
|
||||
|
||||
alg = _get_jwt_rsa_algorithm(hash_algorithm_name)
|
||||
|
||||
# Check private key
|
||||
|
||||
if not rsa_private_key:
|
||||
raise ValueError('rsa_private_key required for RSA with ' +
|
||||
alg.hash_alg.name + ' signature method')
|
||||
|
||||
# Convert the "signature base string" into a sequence of bytes (M)
|
||||
#
|
||||
# The signature base string, by definition, only contain printable US-ASCII
|
||||
# characters. So encoding it as 'ascii' will always work. It will raise a
|
||||
# ``UnicodeError`` if it can't encode the value, which will never happen
|
||||
# if the signature base string was created correctly. Therefore, using
|
||||
# 'ascii' encoding provides an extra level of error checking.
|
||||
|
||||
m = sig_base_str.encode('ascii')
|
||||
|
||||
# Perform signing: S = RSASSA-PKCS1-V1_5-SIGN (K, M)
|
||||
|
||||
key = _prepare_key_plus(alg, rsa_private_key)
|
||||
s = alg.sign(m, key)
|
||||
|
||||
# base64-encoded per RFC2045 section 6.8.
|
||||
#
|
||||
# 1. While b2a_base64 implements base64 defined by RFC 3548. As used here,
|
||||
# it is the same as base64 defined by RFC 2045.
|
||||
# 2. b2a_base64 includes a "\n" at the end of its result ([:-1] removes it)
|
||||
# 3. b2a_base64 produces a binary string. Use decode to produce a str.
|
||||
# It should only contain only printable US-ASCII characters.
|
||||
|
||||
return binascii.b2a_base64(s)[:-1].decode('ascii')
|
||||
|
||||
|
||||
def _verify_rsa(hash_algorithm_name: str,
|
||||
request,
|
||||
rsa_public_key: str):
|
||||
"""
|
||||
Verify a base64 encoded signature for a RSA-based signature method.
|
||||
|
||||
The ``alg`` is used to calculate the digest over the signature base string.
|
||||
For the "RSA_SHA1" signature method, the alg must be SHA-1. While OAuth 1.0a
|
||||
only defines the RSA-SHA1 signature method, this function can be used for
|
||||
other non-standard signature methods that only differ from RSA-SHA1 by the
|
||||
digest algorithm.
|
||||
|
||||
Verification for the RSA-SHA1 signature method is defined in
|
||||
`section 3.4.3`_ of RFC 5849.
|
||||
|
||||
.. _`section 3.4.3`: https://tools.ietf.org/html/rfc5849#section-3.4.3
|
||||
|
||||
To satisfy `RFC2616 section 5.2`_ item 1, the request argument's uri
|
||||
attribute MUST be an absolute URI whose netloc part identifies the
|
||||
origin server or gateway on which the resource resides. Any Host
|
||||
item of the request argument's headers dict attribute will be
|
||||
ignored.
|
||||
|
||||
.. _`RFC2616 Sec 5.2`: https://tools.ietf.org/html/rfc2616#section-5.2
|
||||
"""
|
||||
|
||||
try:
|
||||
# Calculate the *signature base string* of the actual received request
|
||||
|
||||
norm_params = normalize_parameters(request.params)
|
||||
bs_uri = base_string_uri(request.uri)
|
||||
sig_base_str = signature_base_string(
|
||||
request.http_method, bs_uri, norm_params)
|
||||
|
||||
# Obtain the signature that was received in the request
|
||||
|
||||
sig = binascii.a2b_base64(request.signature.encode('ascii'))
|
||||
|
||||
# Get the implementation of RSA-with-hash algorithm to use
|
||||
|
||||
alg = _get_jwt_rsa_algorithm(hash_algorithm_name)
|
||||
|
||||
# Verify the received signature was produced by the private key
|
||||
# corresponding to the `rsa_public_key`, signing exact same
|
||||
# *signature base string*.
|
||||
#
|
||||
# RSASSA-PKCS1-V1_5-VERIFY ((n, e), M, S)
|
||||
|
||||
key = _prepare_key_plus(alg, rsa_public_key)
|
||||
|
||||
# The signature base string only contain printable US-ASCII characters.
|
||||
# The ``encode`` method with the default "strict" error handling will
|
||||
# raise a ``UnicodeError`` if it can't encode the value. So using
|
||||
# "ascii" will always work.
|
||||
|
||||
verify_ok = alg.verify(sig_base_str.encode('ascii'), key, sig)
|
||||
|
||||
if not verify_ok:
|
||||
log.debug('Verify failed: RSA with ' + alg.hash_alg.name +
|
||||
': signature base string=%s' + sig_base_str)
|
||||
return verify_ok
|
||||
|
||||
except UnicodeError:
|
||||
# A properly encoded signature will only contain printable US-ASCII
|
||||
# characters. The ``encode`` method with the default "strict" error
|
||||
# handling will raise a ``UnicodeError`` if it can't decode the value.
|
||||
# So using "ascii" will work with all valid signatures. But an
|
||||
# incorrectly or maliciously produced signature could contain other
|
||||
# bytes.
|
||||
#
|
||||
# This implementation treats that situation as equivalent to the
|
||||
# signature verification having failed.
|
||||
#
|
||||
# Note: simply changing the encode to use 'utf-8' will not remove this
|
||||
# case, since an incorrect or malicious request can contain bytes which
|
||||
# are invalid as UTF-8.
|
||||
return False
|
||||
|
||||
|
||||
# ==== RSA-SHA1 ==================================================
|
||||
|
||||
def sign_rsa_sha1_with_client(sig_base_str, client):
|
||||
# For some reason, this function originally accepts both str and bytes.
|
||||
# This behaviour is preserved here. But won't be done for the newer
|
||||
# sign_rsa_sha256_with_client and sign_rsa_sha512_with_client functions,
|
||||
# which will only accept strings. The function to calculate a
|
||||
# "signature base string" always produces a string, so it is not clear
|
||||
# why support for bytes would ever be needed.
|
||||
sig_base_str = sig_base_str.decode('ascii')\
|
||||
if isinstance(sig_base_str, bytes) else sig_base_str
|
||||
|
||||
return _sign_rsa('SHA-1', sig_base_str, client.rsa_key)
|
||||
|
||||
|
||||
def verify_rsa_sha1(request, rsa_public_key: str):
|
||||
return _verify_rsa('SHA-1', request, rsa_public_key)
|
||||
|
||||
|
||||
def sign_rsa_sha1(base_string, rsa_private_key):
|
||||
"""
|
||||
Deprecated function for calculating a RSA-SHA1 signature.
|
||||
|
||||
This function has been replaced by invoking ``sign_rsa`` with "SHA-1"
|
||||
as the hash algorithm name.
|
||||
|
||||
This function was invoked by sign_rsa_sha1_with_client and
|
||||
test_signatures.py, but does any application invoke it directly? If not,
|
||||
it can be removed.
|
||||
"""
|
||||
warnings.warn('use _sign_rsa("SHA-1", ...) instead of sign_rsa_sha1',
|
||||
DeprecationWarning)
|
||||
|
||||
if isinstance(base_string, bytes):
|
||||
base_string = base_string.decode('ascii')
|
||||
|
||||
return _sign_rsa('SHA-1', base_string, rsa_private_key)
|
||||
|
||||
|
||||
# ==== RSA-SHA256 ================================================
|
||||
|
||||
def sign_rsa_sha256_with_client(sig_base_str: str, client):
|
||||
return _sign_rsa('SHA-256', sig_base_str, client.rsa_key)
|
||||
|
||||
|
||||
def verify_rsa_sha256(request, rsa_public_key: str):
|
||||
return _verify_rsa('SHA-256', request, rsa_public_key)
|
||||
|
||||
|
||||
# ==== RSA-SHA512 ================================================
|
||||
|
||||
def sign_rsa_sha512_with_client(sig_base_str: str, client):
|
||||
return _sign_rsa('SHA-512', sig_base_str, client.rsa_key)
|
||||
|
||||
|
||||
def verify_rsa_sha512(request, rsa_public_key: str):
|
||||
return _verify_rsa('SHA-512', request, rsa_public_key)
|
||||
|
||||
|
||||
# ==== PLAINTEXT =================================================
|
||||
|
||||
def sign_plaintext_with_client(_signature_base_string, client):
|
||||
# _signature_base_string is not used because the signature with PLAINTEXT
|
||||
# is just the secret: it isn't a real signature.
|
||||
return sign_plaintext(client.client_secret, client.resource_owner_secret)
|
||||
|
||||
|
||||
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`: https://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`: https://tools.ietf.org/html/rfc5849#section-3.6
|
||||
signature = utils.escape(client_secret or '')
|
||||
|
||||
# 2. An "&" character (ASCII code 38), which MUST be included even
|
||||
# when either secret is empty.
|
||||
signature += '&'
|
||||
|
||||
# 3. The token shared-secret, after being encoded (`Section 3.6`_).
|
||||
#
|
||||
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
|
||||
signature += utils.escape(resource_owner_secret or '')
|
||||
|
||||
return signature
|
||||
|
||||
|
||||
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`: https://tools.ietf.org/html/rfc5849#section-3.4
|
||||
"""
|
||||
signature = sign_plaintext(client_secret, resource_owner_secret)
|
||||
match = safe_string_equals(signature, request.signature)
|
||||
if not match:
|
||||
log.debug('Verify PLAINTEXT failed')
|
||||
return match
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
oauthlib.utils
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
This module contains utility methods used by various parts of the OAuth
|
||||
spec.
|
||||
"""
|
||||
import urllib.request as urllib2
|
||||
|
||||
from oauthlib.common import quote, unquote
|
||||
|
||||
UNICODE_ASCII_CHARACTER_SET = ('abcdefghijklmnopqrstuvwxyz'
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
'0123456789')
|
||||
|
||||
|
||||
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("oauth_")
|
||||
if isinstance(params, dict):
|
||||
return list(filter(is_oauth, list(params.items())))
|
||||
else:
|
||||
return list(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`: https://tools.ietf.org/html/rfc5849#section-3.6
|
||||
|
||||
"""
|
||||
if not isinstance(u, str):
|
||||
raise ValueError('Only unicode objects are escapable. ' +
|
||||
'Got {!r} of type {}.'.format(u, type(u)))
|
||||
# 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=b'~')
|
||||
|
||||
|
||||
def unescape(u):
|
||||
if not isinstance(u, str):
|
||||
raise ValueError('Only unicode objects are unescapable.')
|
||||
return unquote(u)
|
||||
|
||||
|
||||
def parse_keqv_list(l):
|
||||
"""A unicode-safe version of urllib2.parse_keqv_list"""
|
||||
# With Python 2.6, parse_http_list handles unicode fine
|
||||
return urllib2.parse_keqv_list(l)
|
||||
|
||||
|
||||
def parse_http_list(u):
|
||||
"""A unicode-safe version of urllib2.parse_http_list"""
|
||||
# With Python 2.6, parse_http_list handles unicode fine
|
||||
return urllib2.parse_http_list(u)
|
||||
|
||||
|
||||
def parse_authorization_header(authorization_header):
|
||||
"""Parse an OAuth authorization header into a list of 2-tuples"""
|
||||
auth_scheme = 'OAuth '.lower()
|
||||
if authorization_header[:len(auth_scheme)].lower().startswith(auth_scheme):
|
||||
items = parse_http_list(authorization_header[len(auth_scheme):])
|
||||
try:
|
||||
return list(parse_keqv_list(items).items())
|
||||
except (IndexError, ValueError):
|
||||
pass
|
||||
raise ValueError('Malformed authorization header')
|
||||
Reference in New Issue
Block a user