venv added, updated

This commit is contained in:
Norbert
2024-09-13 09:46:28 +02:00
parent 577596d9f3
commit 82af8c809a
4812 changed files with 640223 additions and 2 deletions

View File

@@ -0,0 +1,34 @@
from .rfc5849 import (
OAuth1Request,
ClientAuth,
SIGNATURE_HMAC_SHA1,
SIGNATURE_RSA_SHA1,
SIGNATURE_PLAINTEXT,
SIGNATURE_TYPE_HEADER,
SIGNATURE_TYPE_QUERY,
SIGNATURE_TYPE_BODY,
ClientMixin,
TemporaryCredentialMixin,
TokenCredentialMixin,
TemporaryCredential,
AuthorizationServer,
ResourceProtector,
)
__all__ = [
'OAuth1Request',
'ClientAuth',
'SIGNATURE_HMAC_SHA1',
'SIGNATURE_RSA_SHA1',
'SIGNATURE_PLAINTEXT',
'SIGNATURE_TYPE_HEADER',
'SIGNATURE_TYPE_QUERY',
'SIGNATURE_TYPE_BODY',
'ClientMixin',
'TemporaryCredentialMixin',
'TokenCredentialMixin',
'TemporaryCredential',
'AuthorizationServer',
'ResourceProtector',
]

View File

@@ -0,0 +1,172 @@
from authlib.common.urls import (
url_decode,
add_params_to_uri,
urlparse,
)
from authlib.common.encoding import json_loads
from .rfc5849 import (
SIGNATURE_HMAC_SHA1,
SIGNATURE_TYPE_HEADER,
ClientAuth,
)
class OAuth1Client:
auth_class = ClientAuth
def __init__(self, session, client_id, client_secret=None,
token=None, token_secret=None,
redirect_uri=None, rsa_key=None, verifier=None,
signature_method=SIGNATURE_HMAC_SHA1,
signature_type=SIGNATURE_TYPE_HEADER,
force_include_body=False, realm=None, **kwargs):
if not client_id:
raise ValueError('Missing "client_id"')
self.session = session
self.auth = self.auth_class(
client_id, client_secret=client_secret,
token=token, token_secret=token_secret,
redirect_uri=redirect_uri,
signature_method=signature_method,
signature_type=signature_type,
rsa_key=rsa_key,
verifier=verifier,
realm=realm,
force_include_body=force_include_body
)
self._kwargs = kwargs
@property
def redirect_uri(self):
return self.auth.redirect_uri
@redirect_uri.setter
def redirect_uri(self, uri):
self.auth.redirect_uri = uri
@property
def token(self):
return dict(
oauth_token=self.auth.token,
oauth_token_secret=self.auth.token_secret,
oauth_verifier=self.auth.verifier
)
@token.setter
def token(self, token):
"""This token setter is designed for an easy integration for
OAuthClient. Make sure both OAuth1Session and OAuth2Session
have token setters.
"""
if token is None:
self.auth.token = None
self.auth.token_secret = None
self.auth.verifier = None
elif 'oauth_token' in token:
self.auth.token = token['oauth_token']
if 'oauth_token_secret' in token:
self.auth.token_secret = token['oauth_token_secret']
if 'oauth_verifier' in token:
self.auth.verifier = token['oauth_verifier']
else:
message = f'oauth_token is missing: {token!r}'
self.handle_error('missing_token', message)
def create_authorization_url(self, url, request_token=None, **kwargs):
"""Create an authorization URL by appending request_token and optional
kwargs to url.
This is the second step in the OAuth 1 workflow. The user should be
redirected to this authorization URL, grant access to you, and then
be redirected back to you. The redirection back can either be specified
during client registration or by supplying a callback URI per request.
:param url: The authorization endpoint URL.
:param request_token: The previously obtained request token.
:param kwargs: Optional parameters to append to the URL.
:returns: The authorization URL with new parameters embedded.
"""
kwargs['oauth_token'] = request_token or self.auth.token
if self.auth.redirect_uri:
kwargs['oauth_callback'] = self.auth.redirect_uri
return add_params_to_uri(url, kwargs.items())
def fetch_request_token(self, url, **kwargs):
"""Method for fetching an access token from the token endpoint.
This is the first step in the OAuth 1 workflow. A request token is
obtained by making a signed post request to url. The token is then
parsed from the application/x-www-form-urlencoded response and ready
to be used to construct an authorization url.
:param url: Request Token endpoint.
:param kwargs: Extra parameters to include for fetching token.
:return: A Request Token dict.
"""
return self._fetch_token(url, **kwargs)
def fetch_access_token(self, url, verifier=None, **kwargs):
"""Method for fetching an access token from the token endpoint.
This is the final step in the OAuth 1 workflow. An access token is
obtained using all previously obtained credentials, including the
verifier from the authorization step.
:param url: Access Token endpoint.
:param verifier: A verifier string to prove authorization was granted.
:param kwargs: Extra parameters to include for fetching access token.
:return: A token dict.
"""
if verifier:
self.auth.verifier = verifier
if not self.auth.verifier:
self.handle_error('missing_verifier', 'Missing "verifier" value')
return self._fetch_token(url, **kwargs)
def parse_authorization_response(self, url):
"""Extract parameters from the post authorization redirect
response URL.
:param url: The full URL that resulted from the user being redirected
back from the OAuth provider to you, the client.
:returns: A dict of parameters extracted from the URL.
"""
token = dict(url_decode(urlparse.urlparse(url).query))
self.token = token
return token
def _fetch_token(self, url, **kwargs):
resp = self.session.post(url, auth=self.auth, **kwargs)
token = self.parse_response_token(resp.status_code, resp.text)
self.token = token
self.auth.verifier = None
return token
def parse_response_token(self, status_code, text):
if status_code >= 400:
message = (
"Token request failed with code {}, "
"response was '{}'."
).format(status_code, text)
self.handle_error('fetch_token_denied', message)
try:
text = text.strip()
if text.startswith('{'):
token = json_loads(text)
else:
token = dict(url_decode(text))
except (TypeError, ValueError) as e:
error = (
"Unable to decode token from token response. "
"This is commonly caused by an unsuccessful request where"
" a non urlencoded error message is returned. "
"The decoding error was {}"
).format(e)
raise ValueError(error)
return token
@staticmethod
def handle_error(error_type, error_description):
raise ValueError(f'{error_type}: {error_description}')

View File

@@ -0,0 +1,3 @@
# flake8: noqa
from .rfc5849.errors import *

View File

@@ -0,0 +1,45 @@
"""
authlib.oauth1.rfc5849
~~~~~~~~~~~~~~~~~~~~~~
This module represents a direct implementation of The OAuth 1.0 Protocol.
https://tools.ietf.org/html/rfc5849
"""
from .wrapper import OAuth1Request
from .client_auth import ClientAuth
from .signature import (
SIGNATURE_HMAC_SHA1,
SIGNATURE_RSA_SHA1,
SIGNATURE_PLAINTEXT,
SIGNATURE_TYPE_HEADER,
SIGNATURE_TYPE_QUERY,
SIGNATURE_TYPE_BODY,
)
from .models import (
ClientMixin,
TemporaryCredentialMixin,
TokenCredentialMixin,
TemporaryCredential,
)
from .authorization_server import AuthorizationServer
from .resource_protector import ResourceProtector
__all__ = [
'OAuth1Request',
'ClientAuth',
'SIGNATURE_HMAC_SHA1',
'SIGNATURE_RSA_SHA1',
'SIGNATURE_PLAINTEXT',
'SIGNATURE_TYPE_HEADER',
'SIGNATURE_TYPE_QUERY',
'SIGNATURE_TYPE_BODY',
'ClientMixin',
'TemporaryCredentialMixin',
'TokenCredentialMixin',
'TemporaryCredential',
'AuthorizationServer',
'ResourceProtector',
]

View File

@@ -0,0 +1,357 @@
from authlib.common.urls import is_valid_url, add_params_to_uri
from .base_server import BaseServer
from .errors import (
OAuth1Error,
InvalidRequestError,
MissingRequiredParameterError,
InvalidClientError,
InvalidTokenError,
AccessDeniedError,
MethodNotAllowedError,
)
class AuthorizationServer(BaseServer):
TOKEN_RESPONSE_HEADER = [
('Content-Type', 'application/x-www-form-urlencoded'),
('Cache-Control', 'no-store'),
('Pragma', 'no-cache'),
]
TEMPORARY_CREDENTIALS_METHOD = 'POST'
def _get_client(self, request):
client = self.get_client_by_id(request.client_id)
request.client = client
return client
def create_oauth1_request(self, request):
raise NotImplementedError()
def handle_response(self, status_code, payload, headers):
raise NotImplementedError()
def handle_error_response(self, error):
return self.handle_response(
error.status_code,
error.get_body(),
error.get_headers()
)
def validate_temporary_credentials_request(self, request):
"""Validate HTTP request for temporary credentials."""
# The client obtains a set of temporary credentials from the server by
# making an authenticated (Section 3) HTTP "POST" request to the
# Temporary Credential Request endpoint (unless the server advertises
# another HTTP request method for the client to use).
if request.method.upper() != self.TEMPORARY_CREDENTIALS_METHOD:
raise MethodNotAllowedError()
# REQUIRED parameter
if not request.client_id:
raise MissingRequiredParameterError('oauth_consumer_key')
# REQUIRED parameter
oauth_callback = request.redirect_uri
if not request.redirect_uri:
raise MissingRequiredParameterError('oauth_callback')
# An absolute URI or
# other means (the parameter value MUST be set to "oob"
if oauth_callback != 'oob' and not is_valid_url(oauth_callback):
raise InvalidRequestError('Invalid "oauth_callback" value')
client = self._get_client(request)
if not client:
raise InvalidClientError()
self.validate_timestamp_and_nonce(request)
self.validate_oauth_signature(request)
return request
def create_temporary_credentials_response(self, request=None):
"""Validate temporary credentials token request and create response
for temporary credentials token. Assume the endpoint of temporary
credentials request is ``https://photos.example.net/initiate``:
.. code-block:: http
POST /initiate HTTP/1.1
Host: photos.example.net
Authorization: OAuth realm="Photos",
oauth_consumer_key="dpf43f3p2l4k3l03",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="137131200",
oauth_nonce="wIjqoS",
oauth_callback="http%3A%2F%2Fprinter.example.com%2Fready",
oauth_signature="74KNZJeDHnMBp0EMJ9ZHt%2FXKycU%3D"
The server validates the request and replies with a set of temporary
credentials in the body of the HTTP response:
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: application/x-www-form-urlencoded
oauth_token=hh5s93j4hdidpola&oauth_token_secret=hdhd0244k9j7ao03&
oauth_callback_confirmed=true
:param request: OAuth1Request instance.
:returns: (status_code, body, headers)
"""
try:
request = self.create_oauth1_request(request)
self.validate_temporary_credentials_request(request)
except OAuth1Error as error:
return self.handle_error_response(error)
credential = self.create_temporary_credential(request)
payload = [
('oauth_token', credential.get_oauth_token()),
('oauth_token_secret', credential.get_oauth_token_secret()),
('oauth_callback_confirmed', True)
]
return self.handle_response(200, payload, self.TOKEN_RESPONSE_HEADER)
def validate_authorization_request(self, request):
"""Validate the request for resource owner authorization."""
if not request.token:
raise MissingRequiredParameterError('oauth_token')
credential = self.get_temporary_credential(request)
if not credential:
raise InvalidTokenError()
# assign credential for later use
request.credential = credential
return request
def create_authorization_response(self, request, grant_user=None):
"""Validate authorization request and create authorization response.
Assume the endpoint for authorization request is
``https://photos.example.net/authorize``, the client redirects Jane's
user-agent to the server's Resource Owner Authorization endpoint to
obtain Jane's approval for accessing her private photos::
https://photos.example.net/authorize?oauth_token=hh5s93j4hdidpola
The server requests Jane to sign in using her username and password
and if successful, asks her to approve granting 'printer.example.com'
access to her private photos. Jane approves the request and her
user-agent is redirected to the callback URI provided by the client
in the previous request (line breaks are for display purposes only)::
http://printer.example.com/ready?
oauth_token=hh5s93j4hdidpola&oauth_verifier=hfdp7dh39dks9884
:param request: OAuth1Request instance.
:param grant_user: if granted, pass the grant user, otherwise None.
:returns: (status_code, body, headers)
"""
request = self.create_oauth1_request(request)
# authorize endpoint should try catch this error
self.validate_authorization_request(request)
temporary_credentials = request.credential
redirect_uri = temporary_credentials.get_redirect_uri()
if not redirect_uri or redirect_uri == 'oob':
client_id = temporary_credentials.get_client_id()
client = self.get_client_by_id(client_id)
redirect_uri = client.get_default_redirect_uri()
if grant_user is None:
error = AccessDeniedError()
location = add_params_to_uri(redirect_uri, error.get_body())
return self.handle_response(302, '', [('Location', location)])
request.user = grant_user
verifier = self.create_authorization_verifier(request)
params = [
('oauth_token', request.token),
('oauth_verifier', verifier)
]
location = add_params_to_uri(redirect_uri, params)
return self.handle_response(302, '', [('Location', location)])
def validate_token_request(self, request):
"""Validate request for issuing token."""
if not request.client_id:
raise MissingRequiredParameterError('oauth_consumer_key')
client = self._get_client(request)
if not client:
raise InvalidClientError()
if not request.token:
raise MissingRequiredParameterError('oauth_token')
token = self.get_temporary_credential(request)
if not token:
raise InvalidTokenError()
verifier = request.oauth_params.get('oauth_verifier')
if not verifier:
raise MissingRequiredParameterError('oauth_verifier')
if not token.check_verifier(verifier):
raise InvalidRequestError('Invalid "oauth_verifier"')
request.credential = token
self.validate_timestamp_and_nonce(request)
self.validate_oauth_signature(request)
return request
def create_token_response(self, request):
"""Validate token request and create token response. Assuming the
endpoint of token request is ``https://photos.example.net/token``,
the callback request informs the client that Jane completed the
authorization process. The client then requests a set of token
credentials using its temporary credentials (over a secure Transport
Layer Security (TLS) channel):
.. code-block:: http
POST /token HTTP/1.1
Host: photos.example.net
Authorization: OAuth realm="Photos",
oauth_consumer_key="dpf43f3p2l4k3l03",
oauth_token="hh5s93j4hdidpola",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="137131201",
oauth_nonce="walatlh",
oauth_verifier="hfdp7dh39dks9884",
oauth_signature="gKgrFCywp7rO0OXSjdot%2FIHF7IU%3D"
The server validates the request and replies with a set of token
credentials in the body of the HTTP response:
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: application/x-www-form-urlencoded
oauth_token=nnch734d00sl2jdk&oauth_token_secret=pfkkdhi9sl3r4s00
:param request: OAuth1Request instance.
:returns: (status_code, body, headers)
"""
try:
request = self.create_oauth1_request(request)
except OAuth1Error as error:
return self.handle_error_response(error)
try:
self.validate_token_request(request)
except OAuth1Error as error:
self.delete_temporary_credential(request)
return self.handle_error_response(error)
credential = self.create_token_credential(request)
payload = [
('oauth_token', credential.get_oauth_token()),
('oauth_token_secret', credential.get_oauth_token_secret()),
]
self.delete_temporary_credential(request)
return self.handle_response(200, payload, self.TOKEN_RESPONSE_HEADER)
def create_temporary_credential(self, request):
"""Generate and save a temporary credential into database or cache.
A temporary credential is used for exchanging token credential. This
method should be re-implemented::
def create_temporary_credential(self, request):
oauth_token = generate_token(36)
oauth_token_secret = generate_token(48)
temporary_credential = TemporaryCredential(
oauth_token=oauth_token,
oauth_token_secret=oauth_token_secret,
client_id=request.client_id,
redirect_uri=request.redirect_uri,
)
# if the credential has a save method
temporary_credential.save()
return temporary_credential
:param request: OAuth1Request instance
:return: TemporaryCredential instance
"""
raise NotImplementedError()
def get_temporary_credential(self, request):
"""Get the temporary credential from database or cache. A temporary
credential should share the same methods as described in models of
``TemporaryCredentialMixin``::
def get_temporary_credential(self, request):
key = 'a-key-prefix:{}'.format(request.token)
data = cache.get(key)
# TemporaryCredential shares methods from TemporaryCredentialMixin
return TemporaryCredential(data)
:param request: OAuth1Request instance
:return: TemporaryCredential instance
"""
raise NotImplementedError()
def delete_temporary_credential(self, request):
"""Delete temporary credential from database or cache. For instance,
if temporary credential is saved in cache::
def delete_temporary_credential(self, request):
key = 'a-key-prefix:{}'.format(request.token)
cache.delete(key)
:param request: OAuth1Request instance
"""
raise NotImplementedError()
def create_authorization_verifier(self, request):
"""Create and bind ``oauth_verifier`` to temporary credential. It
could be re-implemented in this way::
def create_authorization_verifier(self, request):
verifier = generate_token(36)
temporary_credential = request.credential
user_id = request.user.id
temporary_credential.user_id = user_id
temporary_credential.oauth_verifier = verifier
# if the credential has a save method
temporary_credential.save()
# remember to return the verifier
return verifier
:param request: OAuth1Request instance
:return: A string of ``oauth_verifier``
"""
raise NotImplementedError()
def create_token_credential(self, request):
"""Create and save token credential into database. This method would
be re-implemented like this::
def create_token_credential(self, request):
oauth_token = generate_token(36)
oauth_token_secret = generate_token(48)
temporary_credential = request.credential
token_credential = TokenCredential(
oauth_token=oauth_token,
oauth_token_secret=oauth_token_secret,
client_id=temporary_credential.get_client_id(),
user_id=temporary_credential.get_user_id()
)
# if the credential has a save method
token_credential.save()
return token_credential
:param request: OAuth1Request instance
:return: TokenCredential instance
"""
raise NotImplementedError()

View File

@@ -0,0 +1,119 @@
import time
from .signature import (
SIGNATURE_HMAC_SHA1,
SIGNATURE_PLAINTEXT,
SIGNATURE_RSA_SHA1,
)
from .signature import (
verify_hmac_sha1,
verify_plaintext,
verify_rsa_sha1,
)
from .errors import (
InvalidRequestError,
MissingRequiredParameterError,
UnsupportedSignatureMethodError,
InvalidNonceError,
InvalidSignatureError,
)
class BaseServer:
SIGNATURE_METHODS = {
SIGNATURE_HMAC_SHA1: verify_hmac_sha1,
SIGNATURE_RSA_SHA1: verify_rsa_sha1,
SIGNATURE_PLAINTEXT: verify_plaintext,
}
SUPPORTED_SIGNATURE_METHODS = [SIGNATURE_HMAC_SHA1]
EXPIRY_TIME = 300
@classmethod
def register_signature_method(cls, name, verify):
"""Extend signature method verification.
:param name: A string to represent signature method.
:param verify: A function to verify signature.
The ``verify`` method accept ``OAuth1Request`` as parameter::
def verify_custom_method(request):
# verify this request, return True or False
return True
Server.register_signature_method('custom-name', verify_custom_method)
"""
cls.SIGNATURE_METHODS[name] = verify
def validate_timestamp_and_nonce(self, request):
"""Validate ``oauth_timestamp`` and ``oauth_nonce`` in HTTP request.
:param request: OAuth1Request instance
"""
timestamp = request.oauth_params.get('oauth_timestamp')
nonce = request.oauth_params.get('oauth_nonce')
if request.signature_method == SIGNATURE_PLAINTEXT:
# The parameters MAY be omitted when using the "PLAINTEXT"
# signature method
if not timestamp and not nonce:
return
if not timestamp:
raise MissingRequiredParameterError('oauth_timestamp')
try:
# The timestamp value MUST be a positive integer
timestamp = int(timestamp)
if timestamp < 0:
raise InvalidRequestError('Invalid "oauth_timestamp" value')
if self.EXPIRY_TIME and time.time() - timestamp > self.EXPIRY_TIME:
raise InvalidRequestError('Invalid "oauth_timestamp" value')
except (ValueError, TypeError):
raise InvalidRequestError('Invalid "oauth_timestamp" value')
if not nonce:
raise MissingRequiredParameterError('oauth_nonce')
if self.exists_nonce(nonce, request):
raise InvalidNonceError()
def validate_oauth_signature(self, request):
"""Validate ``oauth_signature`` from HTTP request.
:param request: OAuth1Request instance
"""
method = request.signature_method
if not method:
raise MissingRequiredParameterError('oauth_signature_method')
if method not in self.SUPPORTED_SIGNATURE_METHODS:
raise UnsupportedSignatureMethodError()
if not request.signature:
raise MissingRequiredParameterError('oauth_signature')
verify = self.SIGNATURE_METHODS.get(method)
if not verify:
raise UnsupportedSignatureMethodError()
if not verify(request):
raise InvalidSignatureError()
def get_client_by_id(self, client_id):
"""Get client instance with the given ``client_id``.
:param client_id: A string of client_id
:return: Client instance
"""
raise NotImplementedError()
def exists_nonce(self, nonce, request):
"""The nonce value MUST be unique across all requests with the same
timestamp, client credentials, and token combinations.
:param nonce: A string value of ``oauth_nonce``
:param request: OAuth1Request instance
:return: Boolean
"""
raise NotImplementedError()

View File

@@ -0,0 +1,187 @@
import time
import base64
import hashlib
from authlib.common.security import generate_token
from authlib.common.urls import extract_params
from authlib.common.encoding import to_native
from .wrapper import OAuth1Request
from .signature import (
SIGNATURE_HMAC_SHA1,
SIGNATURE_PLAINTEXT,
SIGNATURE_RSA_SHA1,
SIGNATURE_TYPE_HEADER,
SIGNATURE_TYPE_BODY,
SIGNATURE_TYPE_QUERY,
)
from .signature import (
sign_hmac_sha1,
sign_rsa_sha1,
sign_plaintext
)
from .parameters import (
prepare_form_encoded_body,
prepare_headers,
prepare_request_uri_query,
)
CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded'
CONTENT_TYPE_MULTI_PART = 'multipart/form-data'
class ClientAuth:
SIGNATURE_METHODS = {
SIGNATURE_HMAC_SHA1: sign_hmac_sha1,
SIGNATURE_RSA_SHA1: sign_rsa_sha1,
SIGNATURE_PLAINTEXT: sign_plaintext,
}
@classmethod
def register_signature_method(cls, name, sign):
"""Extend client signature methods.
:param name: A string to represent signature method.
:param sign: A function to generate signature.
The ``sign`` method accept 2 parameters::
def custom_sign_method(client, request):
# client is the instance of Client.
return 'your-signed-string'
Client.register_signature_method('custom-name', custom_sign_method)
"""
cls.SIGNATURE_METHODS[name] = sign
def __init__(self, client_id, client_secret=None,
token=None, token_secret=None,
redirect_uri=None, rsa_key=None, verifier=None,
signature_method=SIGNATURE_HMAC_SHA1,
signature_type=SIGNATURE_TYPE_HEADER,
realm=None, force_include_body=False):
self.client_id = client_id
self.client_secret = client_secret
self.token = token
self.token_secret = token_secret
self.redirect_uri = redirect_uri
self.signature_method = signature_method
self.signature_type = signature_type
self.rsa_key = rsa_key
self.verifier = verifier
self.realm = realm
self.force_include_body = force_include_body
def get_oauth_signature(self, method, uri, headers, body):
"""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
"""
sign = self.SIGNATURE_METHODS.get(self.signature_method)
if not sign:
raise ValueError('Invalid signature method.')
request = OAuth1Request(method, uri, body=body, headers=headers)
return sign(self, request)
def get_oauth_params(self, nonce, timestamp):
oauth_params = [
('oauth_nonce', nonce),
('oauth_timestamp', timestamp),
('oauth_version', '1.0'),
('oauth_signature_method', self.signature_method),
('oauth_consumer_key', self.client_id),
]
if self.token:
oauth_params.append(('oauth_token', self.token))
if self.redirect_uri:
oauth_params.append(('oauth_callback', self.redirect_uri))
if self.verifier:
oauth_params.append(('oauth_verifier', self.verifier))
return oauth_params
def _render(self, uri, headers, body, oauth_params):
if self.signature_type == SIGNATURE_TYPE_HEADER:
headers = prepare_headers(oauth_params, headers, realm=self.realm)
elif self.signature_type == SIGNATURE_TYPE_BODY:
if CONTENT_TYPE_FORM_URLENCODED in headers.get('Content-Type', ''):
decoded_body = extract_params(body) or []
body = prepare_form_encoded_body(oauth_params, decoded_body)
headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED
elif self.signature_type == SIGNATURE_TYPE_QUERY:
uri = prepare_request_uri_query(oauth_params, uri)
else:
raise ValueError('Unknown signature type specified.')
return uri, headers, body
def sign(self, method, uri, headers, body):
"""Sign the HTTP request, add OAuth parameters and signature.
:param method: HTTP method of the request.
:param uri: URI of the HTTP request.
:param body: Body payload of the HTTP request.
:param headers: Headers of the HTTP request.
:return: uri, headers, body
"""
nonce = generate_nonce()
timestamp = generate_timestamp()
if body is None:
body = b''
# transform int to str
timestamp = str(timestamp)
if headers is None:
headers = {}
oauth_params = self.get_oauth_params(nonce, timestamp)
# https://datatracker.ietf.org/doc/html/draft-eaton-oauth-bodyhash-00.html
# include oauth_body_hash
if body and headers.get('Content-Type') != CONTENT_TYPE_FORM_URLENCODED:
oauth_body_hash = base64.b64encode(hashlib.sha1(body).digest())
oauth_params.append(('oauth_body_hash', oauth_body_hash.decode('utf-8')))
uri, headers, body = self._render(uri, headers, body, oauth_params)
sig = self.get_oauth_signature(method, uri, headers, body)
oauth_params.append(('oauth_signature', sig))
uri, headers, body = self._render(uri, headers, body, oauth_params)
return uri, headers, body
def prepare(self, method, uri, headers, body):
"""Add OAuth parameters to the request.
Parameters may be included from the body if the content-type is
urlencoded, if no content type is set, a guess is made.
"""
content_type = to_native(headers.get('Content-Type', ''))
if self.signature_type == SIGNATURE_TYPE_BODY:
content_type = CONTENT_TYPE_FORM_URLENCODED
elif not content_type and extract_params(body):
content_type = CONTENT_TYPE_FORM_URLENCODED
if CONTENT_TYPE_FORM_URLENCODED in content_type:
headers['Content-Type'] = CONTENT_TYPE_FORM_URLENCODED
uri, headers, body = self.sign(method, uri, headers, body)
elif self.force_include_body:
# To allow custom clients to work on non form encoded bodies.
uri, headers, body = self.sign(method, uri, headers, body)
else:
# Omit body data in the signing of non form-encoded requests
uri, headers, _ = self.sign(method, uri, headers, b'')
body = b''
return uri, headers, body
def generate_nonce():
return generate_token()
def generate_timestamp():
return str(int(time.time()))

View File

@@ -0,0 +1,89 @@
"""
authlib.oauth1.rfc5849.errors
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
RFC5849 has no definition on errors. This module is designed by
Authlib based on OAuth 1.0a `Section 10`_ with some changes.
.. _`Section 10`: https://oauth.net/core/1.0a/#rfc.section.10
"""
from authlib.common.errors import AuthlibHTTPError
from authlib.common.security import is_secure_transport
class OAuth1Error(AuthlibHTTPError):
def __init__(self, description=None, uri=None, status_code=None):
super().__init__(None, description, uri, status_code)
def get_headers(self):
"""Get a list of headers."""
return [
('Content-Type', 'application/x-www-form-urlencoded'),
('Cache-Control', 'no-store'),
('Pragma', 'no-cache')
]
class InsecureTransportError(OAuth1Error):
error = 'insecure_transport'
description = 'OAuth 2 MUST utilize https.'
@classmethod
def check(cls, uri):
if not is_secure_transport(uri):
raise cls()
class InvalidRequestError(OAuth1Error):
error = 'invalid_request'
class UnsupportedParameterError(OAuth1Error):
error = 'unsupported_parameter'
class UnsupportedSignatureMethodError(OAuth1Error):
error = 'unsupported_signature_method'
class MissingRequiredParameterError(OAuth1Error):
error = 'missing_required_parameter'
def __init__(self, key):
description = f'missing "{key}" in parameters'
super().__init__(description=description)
class DuplicatedOAuthProtocolParameterError(OAuth1Error):
error = 'duplicated_oauth_protocol_parameter'
class InvalidClientError(OAuth1Error):
error = 'invalid_client'
status_code = 401
class InvalidTokenError(OAuth1Error):
error = 'invalid_token'
description = 'Invalid or expired "oauth_token" in parameters'
status_code = 401
class InvalidSignatureError(OAuth1Error):
error = 'invalid_signature'
status_code = 401
class InvalidNonceError(OAuth1Error):
error = 'invalid_nonce'
status_code = 401
class AccessDeniedError(OAuth1Error):
error = 'access_denied'
description = 'The resource owner or authorization server denied the request'
class MethodNotAllowedError(OAuth1Error):
error = 'method_not_allowed'
status_code = 405

View File

@@ -0,0 +1,108 @@
class ClientMixin:
def get_default_redirect_uri(self):
"""A method to get client default redirect_uri. For instance, the
database table for client has a column called ``default_redirect_uri``::
def get_default_redirect_uri(self):
return self.default_redirect_uri
:return: A URL string
"""
raise NotImplementedError()
def get_client_secret(self):
"""A method to return the client_secret of this client. For instance,
the database table has a column called ``client_secret``::
def get_client_secret(self):
return self.client_secret
"""
raise NotImplementedError()
def get_rsa_public_key(self):
"""A method to get the RSA public key for RSA-SHA1 signature method.
For instance, the value is saved on column ``rsa_public_key``::
def get_rsa_public_key(self):
return self.rsa_public_key
"""
raise NotImplementedError()
class TokenCredentialMixin:
def get_oauth_token(self):
"""A method to get the value of ``oauth_token``. For instance, the
database table has a column called ``oauth_token``::
def get_oauth_token(self):
return self.oauth_token
:return: A string
"""
raise NotImplementedError()
def get_oauth_token_secret(self):
"""A method to get the value of ``oauth_token_secret``. For instance,
the database table has a column called ``oauth_token_secret``::
def get_oauth_token_secret(self):
return self.oauth_token_secret
:return: A string
"""
raise NotImplementedError()
class TemporaryCredentialMixin(TokenCredentialMixin):
def get_client_id(self):
"""A method to get the client_id associated with this credential.
For instance, the table in the database has a column ``client_id``::
def get_client_id(self):
return self.client_id
"""
raise NotImplementedError()
def get_redirect_uri(self):
"""A method to get temporary credential's ``oauth_callback``.
For instance, the database table for temporary credential has a
column called ``oauth_callback``::
def get_redirect_uri(self):
return self.oauth_callback
:return: A URL string
"""
raise NotImplementedError()
def check_verifier(self, verifier):
"""A method to check if the given verifier matches this temporary
credential. For instance that this temporary credential has recorded
the value in database as column ``oauth_verifier``::
def check_verifier(self, verifier):
return self.oauth_verifier == verifier
:return: Boolean
"""
raise NotImplementedError()
class TemporaryCredential(dict, TemporaryCredentialMixin):
def get_client_id(self):
return self.get('client_id')
def get_user_id(self):
return self.get('user_id')
def get_redirect_uri(self):
return self.get('oauth_callback')
def check_verifier(self, verifier):
return self.get('oauth_verifier') == verifier
def get_oauth_token(self):
return self.get('oauth_token')
def get_oauth_token_secret(self):
return self.get('oauth_token_secret')

View File

@@ -0,0 +1,101 @@
"""
authlib.spec.rfc5849.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 authlib.common.urls import urlparse, url_encode, extract_params
from .util import escape
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="Photos",
oauth_consumer_key="dpf43f3p2l4k3l03",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="137131200",
oauth_nonce="wIjqoS",
oauth_callback="http%3A%2F%2Fprinter.example.com%2Fready",
oauth_signature="74KNZJeDHnMBp0EMJ9ZHt%2FXKycU%3D",
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 {}
# step 1, 2, 3 in Section 3.5.1
header_parameters = ', '.join([
f'{escape(k)}="{escape(v)}"' for k, v in oauth_params
if k.startswith('oauth_')
])
# 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
header_parameters = f'realm="{realm}", ' + header_parameters
# the auth-scheme name set to "OAuth" (case insensitive).
headers['Authorization'] = f'OAuth {header_parameters}'
return 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 url_encode(_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.urlparse(uri)
query = url_encode(
_append_params(oauth_params, extract_params(query) or []))
return urlparse.urlunparse((sch, net, path, par, query, fra))

View File

@@ -0,0 +1,41 @@
from .base_server import BaseServer
from .wrapper import OAuth1Request
from .errors import (
MissingRequiredParameterError,
InvalidClientError,
InvalidTokenError,
)
class ResourceProtector(BaseServer):
def validate_request(self, method, uri, body, headers):
request = OAuth1Request(method, uri, body, headers)
if not request.client_id:
raise MissingRequiredParameterError('oauth_consumer_key')
client = self.get_client_by_id(request.client_id)
if not client:
raise InvalidClientError()
request.client = client
if not request.token:
raise MissingRequiredParameterError('oauth_token')
token = self.get_token_credential(request)
if not token:
raise InvalidTokenError()
request.credential = token
self.validate_timestamp_and_nonce(request)
self.validate_oauth_signature(request)
return request
def get_token_credential(self, request):
"""Fetch the token credential from data store like a database,
framework should implement this function.
:param request: OAuth1Request instance
:return: Token model instance
"""
raise NotImplementedError()

View File

@@ -0,0 +1,29 @@
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import (
load_pem_private_key, load_pem_public_key
)
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
from authlib.common.encoding import to_bytes
def sign_sha1(msg, rsa_private_key):
key = load_pem_private_key(
to_bytes(rsa_private_key),
password=None,
backend=default_backend()
)
return key.sign(msg, padding.PKCS1v15(), hashes.SHA1())
def verify_sha1(sig, msg, rsa_public_key):
key = load_pem_public_key(
to_bytes(rsa_public_key),
backend=default_backend()
)
try:
key.verify(sig, msg, padding.PKCS1v15(), hashes.SHA1())
return True
except InvalidSignature:
return False

View File

@@ -0,0 +1,386 @@
"""
authlib.oauth1.rfc5849.signature
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This module represents a direct implementation of `section 3.4`_ of the spec.
.. _`section 3.4`: https://tools.ietf.org/html/rfc5849#section-3.4
"""
import binascii
import hashlib
import hmac
from authlib.common.urls import urlparse
from authlib.common.encoding import to_unicode, to_bytes
from .util import escape, unescape
SIGNATURE_HMAC_SHA1 = "HMAC-SHA1"
SIGNATURE_RSA_SHA1 = "RSA-SHA1"
SIGNATURE_PLAINTEXT = "PLAINTEXT"
SIGNATURE_TYPE_HEADER = 'HEADER'
SIGNATURE_TYPE_QUERY = 'QUERY'
SIGNATURE_TYPE_BODY = 'BODY'
def construct_base_string(method, uri, params, host=None):
"""Generate signature base string from request, per `Section 3.4.1`_.
For example, the HTTP request::
POST /request?b5=%3D%253D&a3=a&c%40=&a2=r%20b HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Authorization: OAuth realm="Example",
oauth_consumer_key="9djdj82h48djs9d2",
oauth_token="kkk9d7dh3k39sjv7",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="137131201",
oauth_nonce="7d8f3e4a",
oauth_signature="bYT5CMsGcbgUdFHObYMEfcx6bsw%3D"
c2&a3=2+q
is represented by the following signature base string (line breaks
are for display purposes only)::
POST&http%3A%2F%2Fexample.com%2Frequest&a2%3Dr%2520b%26a3%3D2%2520q
%26a3%3Da%26b5%3D%253D%25253D%26c%2540%3D%26c2%3D%26oauth_consumer_
key%3D9djdj82h48djs9d2%26oauth_nonce%3D7d8f3e4a%26oauth_signature_m
ethod%3DHMAC-SHA1%26oauth_timestamp%3D137131201%26oauth_token%3Dkkk
9d7dh3k39sjv7
.. _`Section 3.4.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1
"""
# Create base string URI per Section 3.4.1.2
base_string_uri = normalize_base_string_uri(uri, host)
# Cleanup parameter sources per Section 3.4.1.3.1
unescaped_params = []
for k, v in params:
# The "oauth_signature" parameter MUST be excluded from the signature
if k in ('oauth_signature', 'realm'):
continue
# ensure oauth params are unescaped
if k.startswith('oauth_'):
v = unescape(v)
unescaped_params.append((k, v))
# Normalize parameters per Section 3.4.1.3.2
normalized_params = normalize_parameters(unescaped_params)
# construct base string
return '&'.join([
escape(method.upper()),
escape(base_string_uri),
escape(normalized_params),
])
def normalize_base_string_uri(uri, host=None):
"""Normalize Base String URI per `Section 3.4.1.2`_.
For example, the HTTP request::
GET /r%20v/X?id=123 HTTP/1.1
Host: EXAMPLE.COM:80
is represented by the base string URI: "http://example.com/r%20v/X".
In another example, the HTTPS request::
GET /?q=1 HTTP/1.1
Host: www.example.net:8080
is represented by the base string URI: "https://www.example.net:8080/".
.. _`Section 3.4.1.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.2
The host argument overrides the netloc part of the uri argument.
"""
uri = to_unicode(uri)
scheme, netloc, path, params, query, fragment = urlparse.urlparse(uri)
# The scheme, authority, and path of the request resource URI `RFC3986`
# are included by constructing an "http" or "https" URI representing
# the request resource (without the query or fragment) as follows:
#
# .. _`RFC3986`: https://tools.ietf.org/html/rfc3986
if not scheme or not netloc:
raise ValueError('uri must include a scheme and netloc')
# 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 section 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()
netloc = netloc.lower()
# 2. The host and port values MUST match the content of the HTTP
# request "Host" header field.
if host is not None:
netloc = host.lower()
# 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
default_ports = (
('http', '80'),
('https', '443'),
)
if ':' in netloc:
host, port = netloc.split(':', 1)
if (scheme, port) in default_ports:
netloc = host
return urlparse.urlunparse((scheme, netloc, path, params, '', ''))
def normalize_parameters(params):
"""Normalize parameters per `Section 3.4.1.3.2`_.
For example, the list of parameters from the previous section would
be normalized as follows:
Encoded::
+------------------------+------------------+
| Name | Value |
+------------------------+------------------+
| b5 | %3D%253D |
| a3 | a |
| c%40 | |
| a2 | r%20b |
| oauth_consumer_key | 9djdj82h48djs9d2 |
| oauth_token | kkk9d7dh3k39sjv7 |
| oauth_signature_method | HMAC-SHA1 |
| oauth_timestamp | 137131201 |
| oauth_nonce | 7d8f3e4a |
| c2 | |
| a3 | 2%20q |
+------------------------+------------------+
Sorted::
+------------------------+------------------+
| Name | Value |
+------------------------+------------------+
| a2 | r%20b |
| a3 | 2%20q |
| a3 | a |
| b5 | %3D%253D |
| c%40 | |
| c2 | |
| oauth_consumer_key | 9djdj82h48djs9d2 |
| oauth_nonce | 7d8f3e4a |
| oauth_signature_method | HMAC-SHA1 |
| oauth_timestamp | 137131201 |
| oauth_token | kkk9d7dh3k39sjv7 |
+------------------------+------------------+
Concatenated Pairs::
+-------------------------------------+
| Name=Value |
+-------------------------------------+
| a2=r%20b |
| a3=2%20q |
| a3=a |
| b5=%3D%253D |
| c%40= |
| c2= |
| oauth_consumer_key=9djdj82h48djs9d2 |
| oauth_nonce=7d8f3e4a |
| oauth_signature_method=HMAC-SHA1 |
| oauth_timestamp=137131201 |
| oauth_token=kkk9d7dh3k39sjv7 |
+-------------------------------------+
and concatenated together into a single string (line breaks are for
display purposes only)::
a2=r%20b&a3=2%20q&a3=a&b5=%3D%253D&c%40=&c2=&oauth_consumer_key=9dj
dj82h48djs9d2&oauth_nonce=7d8f3e4a&oauth_signature_method=HMAC-SHA1
&oauth_timestamp=137131201&oauth_token=kkk9d7dh3k39sjv7
.. _`Section 3.4.1.3.2`: https://tools.ietf.org/html/rfc5849#section-3.4.1.3.2
"""
# 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 = [(escape(k), 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 = [f'{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)
def generate_signature_base_string(request):
"""Generate signature base string from request."""
host = request.headers.get('Host', None)
return construct_base_string(
request.method, request.uri, request.params, host)
def hmac_sha1_signature(base_string, client_secret, token_secret):
"""Generate signature via HMAC-SHA1 method, per `Section 3.4.2`_.
The "HMAC-SHA1" signature method uses the HMAC-SHA1 signature
algorithm as defined in `RFC2104`_::
digest = HMAC-SHA1 (key, text)
.. _`RFC2104`: https://tools.ietf.org/html/rfc2104
.. _`Section 3.4.2`: https://tools.ietf.org/html/rfc5849#section-3.4.2
"""
# The HMAC-SHA1 function variables are used in following way:
# text is set to the value of the signature base string from
# `Section 3.4.1.1`_.
#
# .. _`Section 3.4.1.1`: https://tools.ietf.org/html/rfc5849#section-3.4.1.1
text = base_string
# key is set to the concatenated values of:
# 1. The client shared-secret, after being encoded (`Section 3.6`_).
#
# .. _`Section 3.6`: https://tools.ietf.org/html/rfc5849#section-3.6
key = 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 += escape(token_secret or '')
signature = hmac.new(to_bytes(key), to_bytes(text), hashlib.sha1)
# digest is used to set the value of the "oauth_signature" protocol
# parameter, after the result octet string is base64-encoded
# per `RFC2045, Section 6.8`.
#
# .. _`RFC2045, Section 6.8`: https://tools.ietf.org/html/rfc2045#section-6.8
sig = binascii.b2a_base64(signature.digest())[:-1]
return to_unicode(sig)
def rsa_sha1_signature(base_string, rsa_private_key):
"""Generate signature via RSA-SHA1 method, per `Section 3.4.3`_.
The "RSA-SHA1" signature method uses the RSASSA-PKCS1-v1_5 signature
algorithm as defined in `RFC3447, Section 8.2`_ (also known as
PKCS#1), using SHA-1 as the hash function for EMSA-PKCS1-v1_5. To
use this method, the client MUST have established client credentials
with the server that included its RSA public key (in a manner that is
beyond the scope of this specification).
.. _`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
"""
from .rsa import sign_sha1
base_string = to_bytes(base_string)
s = sign_sha1(to_bytes(base_string), rsa_private_key)
sig = binascii.b2a_base64(s)[:-1]
return to_unicode(sig)
def plaintext_signature(client_secret, token_secret):
"""Generate signature via PLAINTEXT method, per `Section 3.4.4`_.
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 = 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 += escape(token_secret or '')
return signature
def sign_hmac_sha1(client, request):
"""Sign a HMAC-SHA1 signature."""
base_string = generate_signature_base_string(request)
return hmac_sha1_signature(
base_string, client.client_secret, client.token_secret)
def sign_rsa_sha1(client, request):
"""Sign a RSASSA-PKCS #1 v1.5 base64 encoded signature."""
base_string = generate_signature_base_string(request)
return rsa_sha1_signature(base_string, client.rsa_key)
def sign_plaintext(client, request):
"""Sign a PLAINTEXT signature."""
return plaintext_signature(client.client_secret, client.token_secret)
def verify_hmac_sha1(request):
"""Verify a HMAC-SHA1 signature."""
base_string = generate_signature_base_string(request)
sig = hmac_sha1_signature(
base_string, request.client_secret, request.token_secret)
return hmac.compare_digest(sig, request.signature)
def verify_rsa_sha1(request):
"""Verify a RSASSA-PKCS #1 v1.5 base64 encoded signature."""
from .rsa import verify_sha1
base_string = generate_signature_base_string(request)
sig = binascii.a2b_base64(to_bytes(request.signature))
return verify_sha1(sig, to_bytes(base_string), request.rsa_public_key)
def verify_plaintext(request):
"""Verify a PLAINTEXT signature."""
sig = plaintext_signature(request.client_secret, request.token_secret)
return hmac.compare_digest(sig, request.signature)

View File

@@ -0,0 +1,9 @@
from authlib.common.urls import quote, unquote
def escape(s):
return quote(s, safe=b'~')
def unescape(s):
return unquote(s)

View File

@@ -0,0 +1,129 @@
from urllib.request import parse_keqv_list, parse_http_list
from authlib.common.urls import (
urlparse, extract_params, url_decode,
)
from .signature import (
SIGNATURE_TYPE_QUERY,
SIGNATURE_TYPE_BODY,
SIGNATURE_TYPE_HEADER
)
from .errors import (
InsecureTransportError,
DuplicatedOAuthProtocolParameterError
)
from .util import unescape
class OAuth1Request:
def __init__(self, method, uri, body=None, headers=None):
InsecureTransportError.check(uri)
self.method = method
self.uri = uri
self.body = body
self.headers = headers or {}
# states namespaces
self.client = None
self.credential = None
self.user = None
self.query = urlparse.urlparse(uri).query
self.query_params = url_decode(self.query)
self.body_params = extract_params(body) or []
self.auth_params, self.realm = _parse_authorization_header(headers)
self.signature_type, self.oauth_params = _parse_oauth_params(
self.query_params, self.body_params, self.auth_params)
params = []
params.extend(self.query_params)
params.extend(self.body_params)
params.extend(self.auth_params)
self.params = params
@property
def client_id(self):
return self.oauth_params.get('oauth_consumer_key')
@property
def client_secret(self):
if self.client:
return self.client.get_client_secret()
@property
def rsa_public_key(self):
if self.client:
return self.client.get_rsa_public_key()
@property
def timestamp(self):
return self.oauth_params.get('oauth_timestamp')
@property
def redirect_uri(self):
return self.oauth_params.get('oauth_callback')
@property
def signature(self):
return self.oauth_params.get('oauth_signature')
@property
def signature_method(self):
return self.oauth_params.get('oauth_signature_method')
@property
def token(self):
return self.oauth_params.get('oauth_token')
@property
def token_secret(self):
if self.credential:
return self.credential.get_oauth_token_secret()
def _filter_oauth(params):
for k, v in params:
if k.startswith('oauth_'):
yield (k, v)
def _parse_authorization_header(headers):
"""Parse an OAuth authorization header into a list of 2-tuples"""
authorization_header = headers.get('Authorization')
if not authorization_header:
return [], None
auth_scheme = 'oauth '
if authorization_header.lower().startswith(auth_scheme):
items = parse_http_list(authorization_header[len(auth_scheme):])
try:
items = parse_keqv_list(items).items()
auth_params = [(unescape(k), unescape(v)) for k, v in items]
realm = dict(auth_params).get('realm')
return auth_params, realm
except (IndexError, ValueError):
pass
raise ValueError('Malformed authorization header')
def _parse_oauth_params(query_params, body_params, auth_params):
oauth_params_set = [
(SIGNATURE_TYPE_QUERY, list(_filter_oauth(query_params))),
(SIGNATURE_TYPE_BODY, list(_filter_oauth(body_params))),
(SIGNATURE_TYPE_HEADER, list(_filter_oauth(auth_params)))
]
oauth_params_set = [params for params in oauth_params_set if params[1]]
if len(oauth_params_set) > 1:
found_types = [p[0] for p in oauth_params_set]
raise DuplicatedOAuthProtocolParameterError(
'"oauth_" params must come from only 1 signature type '
'but were found in {}'.format(','.join(found_types))
)
if oauth_params_set:
signature_type = oauth_params_set[0][0]
oauth_params = dict(oauth_params_set[0][1])
else:
signature_type = None
oauth_params = {}
return signature_type, oauth_params