mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 05:31:10 -05:00
okay fine
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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 [AWS-SECRET-REMOVED]==
|
||||
|
||||
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 [AWS-SECRET-REMOVED]=========
|
||||
|
||||
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 [AWS-SECRET-REMOVED]=======
|
||||
|
||||
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 [AWS-SECRET-REMOVED]=======
|
||||
|
||||
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 [AWS-SECRET-REMOVED]==========
|
||||
|
||||
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 [AWS-SECRET-REMOVED]========
|
||||
|
||||
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 [AWS-SECRET-REMOVED]========
|
||||
|
||||
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 [AWS-SECRET-REMOVED]=========
|
||||
|
||||
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