first commit
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
"""authlib.oauth1.rfc5849.
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module represents a direct implementation of The OAuth 1.0 Protocol.
|
||||
|
||||
https://tools.ietf.org/html/rfc5849
|
||||
"""
|
||||
|
||||
from .authorization_server import AuthorizationServer
|
||||
from .client_auth import ClientAuth
|
||||
from .models import ClientMixin
|
||||
from .models import TemporaryCredential
|
||||
from .models import TemporaryCredentialMixin
|
||||
from .models import TokenCredentialMixin
|
||||
from .resource_protector import ResourceProtector
|
||||
from .signature import SIGNATURE_HMAC_SHA1
|
||||
from .signature import SIGNATURE_PLAINTEXT
|
||||
from .signature import SIGNATURE_RSA_SHA1
|
||||
from .signature import SIGNATURE_TYPE_BODY
|
||||
from .signature import SIGNATURE_TYPE_HEADER
|
||||
from .signature import SIGNATURE_TYPE_QUERY
|
||||
from .wrapper import OAuth1Request
|
||||
|
||||
__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",
|
||||
]
|
||||
@@ -0,0 +1,350 @@
|
||||
from authlib.common.urls import add_params_to_uri
|
||||
from authlib.common.urls import is_valid_url
|
||||
|
||||
from .base_server import BaseServer
|
||||
from .errors import AccessDeniedError
|
||||
from .errors import InvalidClientError
|
||||
from .errors import InvalidRequestError
|
||||
from .errors import InvalidTokenError
|
||||
from .errors import MethodNotAllowedError
|
||||
from .errors import MissingRequiredParameterError
|
||||
from .errors import OAuth1Error
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,115 @@
|
||||
import time
|
||||
|
||||
from .errors import InvalidNonceError
|
||||
from .errors import InvalidRequestError
|
||||
from .errors import InvalidSignatureError
|
||||
from .errors import MissingRequiredParameterError
|
||||
from .errors import UnsupportedSignatureMethodError
|
||||
from .signature import SIGNATURE_HMAC_SHA1
|
||||
from .signature import SIGNATURE_PLAINTEXT
|
||||
from .signature import SIGNATURE_RSA_SHA1
|
||||
from .signature import verify_hmac_sha1
|
||||
from .signature import verify_plaintext
|
||||
from .signature import verify_rsa_sha1
|
||||
|
||||
|
||||
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) as exc:
|
||||
raise InvalidRequestError('Invalid "oauth_timestamp" value') from exc
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,193 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from authlib.common.encoding import to_native
|
||||
from authlib.common.security import generate_token
|
||||
from authlib.common.urls import extract_params
|
||||
|
||||
from .parameters import prepare_form_encoded_body
|
||||
from .parameters import prepare_headers
|
||||
from .parameters import prepare_request_uri_query
|
||||
from .signature import SIGNATURE_HMAC_SHA1
|
||||
from .signature import SIGNATURE_PLAINTEXT
|
||||
from .signature import SIGNATURE_RSA_SHA1
|
||||
from .signature import SIGNATURE_TYPE_BODY
|
||||
from .signature import SIGNATURE_TYPE_HEADER
|
||||
from .signature import SIGNATURE_TYPE_QUERY
|
||||
from .signature import sign_hmac_sha1
|
||||
from .signature import sign_plaintext
|
||||
from .signature import sign_rsa_sha1
|
||||
from .wrapper import OAuth1Request
|
||||
|
||||
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
|
||||
if isinstance(body, bytes):
|
||||
body = body.decode()
|
||||
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()))
|
||||
@@ -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 1 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
|
||||
@@ -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")
|
||||
@@ -0,0 +1,106 @@
|
||||
"""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 extract_params
|
||||
from authlib.common.urls import url_encode
|
||||
from authlib.common.urls import urlparse
|
||||
|
||||
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))
|
||||
@@ -0,0 +1,39 @@
|
||||
from .base_server import BaseServer
|
||||
from .errors import InvalidClientError
|
||||
from .errors import InvalidTokenError
|
||||
from .errors import MissingRequiredParameterError
|
||||
from .wrapper import OAuth1Request
|
||||
|
||||
|
||||
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()
|
||||
@@ -0,0 +1,24 @@
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||
|
||||
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
|
||||
@@ -0,0 +1,387 @@
|
||||
"""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.encoding import to_bytes
|
||||
from authlib.common.encoding import to_unicode
|
||||
from authlib.common.urls import urlparse
|
||||
|
||||
from .util import escape
|
||||
from .util import 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)
|
||||
@@ -0,0 +1,10 @@
|
||||
from authlib.common.urls import quote
|
||||
from authlib.common.urls import unquote
|
||||
|
||||
|
||||
def escape(s):
|
||||
return quote(s, safe=b"~")
|
||||
|
||||
|
||||
def unescape(s):
|
||||
return unquote(s)
|
||||
@@ -0,0 +1,129 @@
|
||||
from urllib.request import parse_http_list
|
||||
from urllib.request import parse_keqv_list
|
||||
|
||||
from authlib.common.urls import extract_params
|
||||
from authlib.common.urls import url_decode
|
||||
from authlib.common.urls import urlparse
|
||||
|
||||
from .errors import DuplicatedOAuthProtocolParameterError
|
||||
from .errors import InsecureTransportError
|
||||
from .signature import SIGNATURE_TYPE_BODY
|
||||
from .signature import SIGNATURE_TYPE_HEADER
|
||||
from .signature import SIGNATURE_TYPE_QUERY
|
||||
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
|
||||
Reference in New Issue
Block a user