venv added, updated
This commit is contained in:
@@ -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',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
172
myenv/lib/python3.12/site-packages/authlib/oauth1/client.py
Normal file
172
myenv/lib/python3.12/site-packages/authlib/oauth1/client.py
Normal 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}')
|
||||
@@ -0,0 +1,3 @@
|
||||
# flake8: noqa
|
||||
|
||||
from .rfc5849.errors import *
|
||||
@@ -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',
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()))
|
||||
@@ -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
|
||||
@@ -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,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))
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user