From 3633c11b0830bbfbbbf0cdda834886dd609220e0 Mon Sep 17 00:00:00 2001 From: Si Young Byun Date: Thu, 8 Jul 2021 11:51:44 -0500 Subject: [PATCH] task: Imports legacy authlib --- .gitignore | 5 +- Pipfile | 5 +- Pipfile.lock | 160 +++++++++++++------ bhjwt/__init__.py | 3 + bhjwt/config/__init__.py | 1 + bhjwt/config/config.py | 28 ++++ bhjwt/decorators/__init__.py | 0 bhjwt/decorators/token_required_decorator.py | 16 ++ bhjwt/providers/__init__.py | 15 ++ bhjwt/providers/brighthive_provider.py | 34 ++++ bhjwt/providers/provider.py | 95 +++++++++++ bhjwt/providers/provider_error.py | 14 ++ bhjwt/providers/provider_factory.py | 46 ++++++ setup.py | 4 +- 14 files changed, 372 insertions(+), 54 deletions(-) create mode 100644 bhjwt/config/__init__.py create mode 100644 bhjwt/config/config.py create mode 100644 bhjwt/decorators/__init__.py create mode 100644 bhjwt/decorators/token_required_decorator.py create mode 100644 bhjwt/providers/__init__.py create mode 100644 bhjwt/providers/brighthive_provider.py create mode 100644 bhjwt/providers/provider.py create mode 100644 bhjwt/providers/provider_error.py create mode 100644 bhjwt/providers/provider_factory.py diff --git a/.gitignore b/.gitignore index 007ec29..d961952 100644 --- a/.gitignore +++ b/.gitignore @@ -138,4 +138,7 @@ dmypy.json cython_debug/ # PyCharm -.idea \ No newline at end of file +.idea + +# vscode +.vscode \ No newline at end of file diff --git a/Pipfile b/Pipfile index 0edb46f..99a1ee8 100644 --- a/Pipfile +++ b/Pipfile @@ -7,11 +7,14 @@ name = "pypi" format = "pipenv run autopep8 . --recursive --in-place --pep8-passes 2000 --verbose" [packages] -brighthive-jwt = {editable = true, path = "."} +requests = "*" +flask = "*" +pyjwt = {extras = ["crypto"], version = "*"} [dev-packages] pytest = "*" autopep8 = "*" +setuptools = "*" [requires] python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index feabccc..d1e2e6d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "50eb3ebf313507d30ce4bf69278fae577fe0d843e5ccb7db174eac5a9f0f8d0b" + "sha256": "8d51351a2f6704a38ce9c72b4743922850ac3bca626de805a90ae3f56c4d1ac7" }, "pipfile-spec": 6, "requires": { @@ -16,25 +16,12 @@ ] }, "default": { - "boto3": { + "certifi": { "hashes": [ - "sha256:2c2f70608934b03f9c08f4cd185de223b5abd18245dd4d4800e1fbc2a2523e31", - "sha256:fccfa81cda69bb2317ed97e7149d7d84d19e6ec3bfbe3f721139e7ac0c407c73" + "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", + "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.17.98" - }, - "botocore": { - "hashes": [ - "sha256:b2a49de4ee04b690142c8e7240f0f5758e3f7673dd39cf398efe893bf5e11c3f", - "sha256:b955b23fe2fbdbbc8e66f37fe2970de6b5d8169f940b200bcf434751709d38f6" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==1.20.98" - }, - "brighthive-jwt": { - "editable": true, - "path": "." + "version": "==2021.5.30" }, "cffi": { "hashes": [ @@ -90,6 +77,22 @@ ], "version": "==1.14.5" }, + "chardet": { + "hashes": [ + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==4.0.0" + }, + "click": { + "hashes": [ + "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a", + "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6" + ], + "markers": "python_version >= '3.6'", + "version": "==8.0.1" + }, "cryptography": { "hashes": [ "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", @@ -107,13 +110,77 @@ ], "version": "==3.4.7" }, - "jmespath": { + "flask": { "hashes": [ - "sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9", - "sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f" + "sha256:1c4c257b1892aec1398784c63791cbaa43062f1f7aeb555c4da961b20ee68f55", + "sha256:a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" ], - "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.10.0" + "index": "pypi", + "version": "==2.0.1" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, + "itsdangerous": { + "hashes": [ + "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c", + "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" + }, + "jinja2": { + "hashes": [ + "sha256:1f06f2da51e7b56b8f238affdd6b4e2c61e39598a378cc49345bc1bd42a978a4", + "sha256:703f484b47a6af502e743c9122595cc812b0271f661722403114f71a79d0f5a4" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.1" + }, + "markupsafe": { + "hashes": [ + "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298", + "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64", + "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", + "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", + "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", + "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", + "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", + "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", + "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", + "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", + "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", + "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", + "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", + "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", + "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", + "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", + "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", + "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", + "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", + "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51", + "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872" + ], + "markers": "python_version >= '3.6'", + "version": "==2.0.1" }, "pycparser": { "hashes": [ @@ -131,39 +198,32 @@ "sha256:934d73fbba91b0483d3857d1aff50e96b2a892384ee2c17417ed3203f173fca1", "sha256:fba44e7898bbca160a2b2b501f492824fc8382485d3a6f11ba5d0c1937ce6130" ], - "markers": "python_version >= '3.6'", + "index": "pypi", "version": "==2.1.0" }, - "python-dateutil": { - "hashes": [ - "sha256:73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", - "sha256:75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.1" - }, - "s3transfer": { + "requests": { "hashes": [ - "sha256:9b3752887a2880690ce628bc263d6d13a3864083aeacff4890c1c9839a5eb0bc", - "sha256:cb022f4b16551edebbb31a377d3f09600dbada7363d8c5db7976e7f47732e1b2" + "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804", + "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e" ], - "version": "==0.4.2" + "index": "pypi", + "version": "==2.25.1" }, - "six": { + "urllib3": { "hashes": [ - "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", - "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4", + "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.16.0" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.26.6" }, - "urllib3": { + "werkzeug": { "hashes": [ - "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c", - "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098" + "sha256:1de1db30d010ff1af14a009224ec49ab2329ad2cde454c8a708130642d579c42", + "sha256:6c1ec500dcdba0baa27600f6a22f6333d8b662d22027ff9f6202e3367413caa8" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.5" + "markers": "python_version >= '3.6'", + "version": "==2.0.1" } }, "develop": { @@ -192,11 +252,11 @@ }, "packaging": { "hashes": [ - "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5", - "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a" + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==20.9" + "markers": "python_version >= '3.6'", + "version": "==21.0" }, "pluggy": { "hashes": [ diff --git a/bhjwt/__init__.py b/bhjwt/__init__.py index da8913a..7f14a3b 100644 --- a/bhjwt/__init__.py +++ b/bhjwt/__init__.py @@ -1 +1,4 @@ from bhjwt.main import create_asserter +from bhjwt.config import AuthLibConfiguration +from bhjwt.providers import BrightHiveProvider, OAuth2ProviderFactory, OAuth2ProviderError +from bhjwt.decorators.token_required_decorator import token_required diff --git a/bhjwt/config/__init__.py b/bhjwt/config/__init__.py new file mode 100644 index 0000000..ba18ec9 --- /dev/null +++ b/bhjwt/config/__init__.py @@ -0,0 +1 @@ +from bhjwt.config.config import AuthLibConfiguration diff --git a/bhjwt/config/config.py b/bhjwt/config/config.py new file mode 100644 index 0000000..d642eba --- /dev/null +++ b/bhjwt/config/config.py @@ -0,0 +1,28 @@ +"""Application configuration. + +""" + + +class AuthLibConfiguration(object): + """Configuration class. + + This class encapsulates all the necessary information needed by + an OAuth 2.0 provider in order to validate a token. + + Args: + provider (str): Name of the OAuth 2.0 provider. + base_url (str): Base URL for the OAuth 2.0 provider. + jwks_url (str): URL for retrieving the application JSON Web Key Set. + algorithms (list): Accepted JWT algorithms. + audience (str): OAuth 2.0 audience parameter. + + """ + + def __init__(self, provider: str = None, base_url: str = None, + jwks_url: str = None, algorithms: list = None, + audience: str = None): + self.provider = provider + self.base_url = base_url + self.jwks_url = jwks_url + self.algorithms = algorithms + self.audience = audience diff --git a/bhjwt/decorators/__init__.py b/bhjwt/decorators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bhjwt/decorators/token_required_decorator.py b/bhjwt/decorators/token_required_decorator.py new file mode 100644 index 0000000..19aa699 --- /dev/null +++ b/bhjwt/decorators/token_required_decorator.py @@ -0,0 +1,16 @@ +"""Access Token Decorator + +This decorator can be used to wrap any endpoint that needs to be protected. + +""" + +from bhjwt.providers import OAuth2Provider + + +def token_required(provider: OAuth2Provider, scopes: list = []): + def wrap(f): + def wrapped_f(*args, **kwargs): + if provider.validate_token(scopes=scopes): + return f(*args, **kwargs) + return wrapped_f + return wrap diff --git a/bhjwt/providers/__init__.py b/bhjwt/providers/__init__.py new file mode 100644 index 0000000..aaca0bb --- /dev/null +++ b/bhjwt/providers/__init__.py @@ -0,0 +1,15 @@ +"""OAuth 2.0 Providers + +This module contains implementation-specific methods for all OAuth 2.0 +providers supported by the library. + +Note: + To add a new provider, ensure that the provider extends the OAuth2Provider + base class. + +""" + +from bhjwt.providers.provider_error import OAuth2ProviderError +from bhjwt.providers.provider import OAuth2Provider +from bhjwt.providers.brighthive_provider import BrightHiveProvider +from bhjwt.providers.provider_factory import OAuth2ProviderFactory diff --git a/bhjwt/providers/brighthive_provider.py b/bhjwt/providers/brighthive_provider.py new file mode 100644 index 0000000..e90a124 --- /dev/null +++ b/bhjwt/providers/brighthive_provider.py @@ -0,0 +1,34 @@ +"""BrightHive OAuth 2.0 Provider. + +Implementation of a BrightHive OAuth 2.0 Provier. + +""" + +import requests +import json +from bhjwt.providers import OAuth2Provider, OAuth2ProviderError + + +class BrightHiveProvider(OAuth2Provider): + """BrightHive OAuth 2.0 Provider.""" + + def __init__(self): + super().__init__() + + def validate_token(self, token=None, scopes=[]): + if not token: + token = self.get_token() + + try: + headers = {'content-type': 'application/json'} + validate_ep = f'{self.base_url}/oauth/validate' + payload = {'token': token} + query = requests.post( + validate_ep, data=json.dumps(payload), headers=headers) + resp = query.json() + if resp['messages']['valid']: + return True + else: + raise OAuth2ProviderError('Access Denied') + except Exception: + raise OAuth2ProviderError('Access Denied') diff --git a/bhjwt/providers/provider.py b/bhjwt/providers/provider.py new file mode 100644 index 0000000..d1300c9 --- /dev/null +++ b/bhjwt/providers/provider.py @@ -0,0 +1,95 @@ +"""OAuth 2.0 Provider Base Class + +This class is the base class for all OAuth 2.0 providers supported by the +libray. + +""" + +import os +import re +from flask import request +from bhjwt.config import AuthLibConfiguration +from bhjwt.providers import OAuth2ProviderError + + +class OAuth2Provider(object): + """OAuth 2.0 Provider Base Class. + + Args: + provider (str): Name of the OAuth 2.0 provider. + base_url (str): Base URL for the OAuth 2.0 provider. + jwks_url (str): URL for retrieving the application JSON Web Key Set. + algorithms (list): Accepted JWT algorithms. + audience (str): OAuth 2.0 audience parameter. + + """ + + __slots__ = ['provider', 'base_url', 'jwks_url', 'algorithms', 'audience'] + + def __init__(self): + self.provider = None + self.base_url = None + self.jwks_url = None + self.algorithms = None + self.audience = None + + def from_object(self, obj: AuthLibConfiguration): + """Configure the library from a configuration object. + + Args: + obj (AuthLibConfiguration): A configuration object to pull configurations from. + + """ + + self.provider = obj.provider + self.base_url = obj.base_url + self.jwks_url = obj.jwks_url + self.algorithms = obj.algorithms + self.audience = obj.audience + + def from_env(self): + """Configure the library from the OS environemt.""" + + self.provider = os.getenv('OAUTH2_PROVIDER', None) + self.base_url = os.getenv('OAUTH2_BASE_URL', None) + self.jwks_url = os.getenv('OAUTH2_JWKS_URL', None) + self.algorithms = os.getenv('OAUTH2_ALGORITHMS', None) + self.audience = os.getenv('OAUTH2_AUDIENCE', None) + + def get_token(self): + """Retrieve the token from the Flask request header. + + Returns: + str: The auth token extracted from the authorization header. + + Raises: + OAuth2ProviderError: If the Authorization header is invalid. + + """ + + auth_token = request.headers.get('Authorization', None) + if not auth_token: + raise OAuth2ProviderError('No authorization header provided.') + + auth_token = re.split('\\s+', auth_token) + if str(auth_token[0]).upper() != 'BEARER': + raise OAuth2ProviderError('Authorization must be a bearer token.') + + if len(auth_token) != 2: + raise OAuth2ProviderError('Invalid token provided.') + + return auth_token[1] + + def validate_token(self, token: str, scopes: list = []): + """Validate the access token provided by the client. + + Args: + token (str): OAuth 2.0 bearer token to validate. + + grants (list): List of OAuth 2.0 grants to validate against. + + Returns: + bool: True if the client is authorized to access the resource, False otherwise. + + """ + pass diff --git a/bhjwt/providers/provider_error.py b/bhjwt/providers/provider_error.py new file mode 100644 index 0000000..3801f29 --- /dev/null +++ b/bhjwt/providers/provider_error.py @@ -0,0 +1,14 @@ +"""Custom OAuth 2.0 Provider Error. + +This class provides a custom OAuth 2.0 Provider Error. + +""" + + +class OAuth2ProviderError(Exception): + """Exception for OAuth2.0 Provider related problems. + + """ + + def __init__(self, message): + super().__init__(message) diff --git a/bhjwt/providers/provider_factory.py b/bhjwt/providers/provider_factory.py new file mode 100644 index 0000000..523f6bc --- /dev/null +++ b/bhjwt/providers/provider_factory.py @@ -0,0 +1,46 @@ +"""An OAuth 2.0 Provider Factory. + +This class implements an OAuth 2.0 factory that is responsible for creating new +providers on demand. + +""" + +from bhjwt.config import AuthLibConfiguration +from bhjwt.providers import BrightHiveProvider, OAuth2ProviderError + + +class OAuth2ProviderFactory(object): + """OAuth 2.0 Provider Factory. + + Methods: + get_provider (OAuth2Provider): + Returns an OAuth2.0 provider. + + """ + + @staticmethod + def get_provider(provider: str = 'BRIGHTHIVE', config: AuthLibConfiguration = None): + """Returns an OAuth 2.0 Provder. + + Params: + provider (str): + The name of the provider (default is BRIGHTHIVE) + config (AuthLibConfiguration): + The configuration object to pass to the newly created provider. + + """ + + if str(provider).upper() == 'BRIGHTHIVE': + provider = BrightHiveProvider() + else: + raise OAuth2ProviderError( + 'Unknown OAuth 2.0 Provider: {}'.format(str(provider).upper())) + + if config is not None: + try: + provider.from_object(config) + except AttributeError: + raise OAuth2ProviderError( + 'Unable to configure library with the provided configuration') + + return provider diff --git a/setup.py b/setup.py index 833e64d..307b5c4 100644 --- a/setup.py +++ b/setup.py @@ -6,11 +6,11 @@ # with open("README.md", "r", encoding="utf-8") as fh: # long_description = fh.read() -REQUIRED_PACKAGES = ["pyjwt[crypto]"] +REQUIRED_PACKAGES = ["pyjwt[crypto]", "requests", "flask"] setup( name="bhjwt", - version="0.0.4-alpha.3", + version="0.0.4-alpha.4", author="Brighthive", # author_email="engineering@brighthive.io", # description="Brighthive Library for JWT",