From e73eb606c7962088ef290e63f4df5bc7f6aa3130 Mon Sep 17 00:00:00 2001 From: maillol Date: Tue, 28 Jan 2025 08:00:08 +0100 Subject: [PATCH 1/7] JWTIdentity raises common error JWTIdentityError --- aiohttp_security/jwt_identity.py | 16 ++++++++++++++-- tests/test_jwt_identity.py | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/aiohttp_security/jwt_identity.py b/aiohttp_security/jwt_identity.py index 6edcf69e..9f3fab8f 100644 --- a/aiohttp_security/jwt_identity.py +++ b/aiohttp_security/jwt_identity.py @@ -19,6 +19,17 @@ AUTH_SCHEME = 'Bearer ' +if HAS_JWT: + # This class inherits from ValueError to maintain backward compatibility + # with previous versions of aiohttp-security. + class JWTIdentityError(jwt.exceptions.PyJWTError, ValueError): + pass + +else: + class JWTIdentityError(ValueError): + pass + + class JWTIdentityPolicy(AbstractIdentityPolicy): def __init__(self, secret: str, algorithm: str = "HS256", key: str = "login"): if not HAS_JWT: @@ -34,14 +45,15 @@ async def identify(self, request: web.Request) -> Optional[str]: return None if not header_identity.startswith(AUTH_SCHEME): - raise ValueError("Invalid authorization scheme. " - + "Should be `{}`".format(AUTH_SCHEME)) + raise JWTIdentityError("Invalid authorization scheme. " + + "Should be `{}`".format(AUTH_SCHEME)) token = header_identity.split(' ')[1].strip() identity = jwt.decode(token, self.secret, algorithms=[self.algorithm]) + return identity.get(self.key) # type: ignore[no-any-return] async def remember(self, request: web.Request, response: web.StreamResponse, diff --git a/tests/test_jwt_identity.py b/tests/test_jwt_identity.py index b4ed430c..ad044683 100644 --- a/tests/test_jwt_identity.py +++ b/tests/test_jwt_identity.py @@ -80,3 +80,28 @@ async def check(request): resp = await client.get('/', headers=headers) assert 400 == resp.status assert 'Invalid authorization scheme' in resp.reason + + +async def test_identify_expired_signature(make_token, aiohttp_client): + kwt_secret_key = "Key" # noqa: S105 + + token = make_token({'login': 'Andrew', 'exp': 0}, kwt_secret_key) + + async def check(request): + policy = request.app[IDENTITY_KEY] + try: + await policy.identify(request) + except jwt.exceptions.PyJWTError as exc: + raise web.HTTPBadRequest(reason=str(exc)) + + return web.Response() + + app = web.Application() + _setup(app, JWTIdentityPolicy(kwt_secret_key), Autz()) + app.router.add_route('GET', '/', check) + + client = await aiohttp_client(app) + headers = {"Authorization": "Bearer {}".format(token)} + resp = await client.get('/', headers=headers) + assert 400 == resp.status + assert 'Signature has expired' in resp.reason From 9b66e29e09bf441cca31a80b72a04691603e2201 Mon Sep 17 00:00:00 2001 From: maillol Date: Tue, 28 Jan 2025 19:45:17 +0100 Subject: [PATCH 2/7] Rename JWTIdentityError to InvalidAuthorizationScheme --- aiohttp_security/jwt_identity.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/aiohttp_security/jwt_identity.py b/aiohttp_security/jwt_identity.py index 9f3fab8f..b5ba2008 100644 --- a/aiohttp_security/jwt_identity.py +++ b/aiohttp_security/jwt_identity.py @@ -11,23 +11,20 @@ try: import jwt HAS_JWT = True + _bases_error = (jwt.exceptions.PyJWTError, ValueError) except ImportError: # pragma: no cover HAS_JWT = False + _bases_error = (ValueError, ) AUTH_HEADER_NAME = 'Authorization' AUTH_SCHEME = 'Bearer ' -if HAS_JWT: - # This class inherits from ValueError to maintain backward compatibility - # with previous versions of aiohttp-security. - class JWTIdentityError(jwt.exceptions.PyJWTError, ValueError): - pass - -else: - class JWTIdentityError(ValueError): - pass +# This class inherits from ValueError to maintain backward compatibility +# with previous versions of aiohttp-security +class InvalidAuthorizationScheme(*_bases_error): + pass class JWTIdentityPolicy(AbstractIdentityPolicy): @@ -45,15 +42,14 @@ async def identify(self, request: web.Request) -> Optional[str]: return None if not header_identity.startswith(AUTH_SCHEME): - raise JWTIdentityError("Invalid authorization scheme. " - + "Should be `{}`".format(AUTH_SCHEME)) + raise InvalidAuthorizationScheme("Invalid authorization scheme. " + "Should be `{}`".format(AUTH_SCHEME)) token = header_identity.split(' ')[1].strip() identity = jwt.decode(token, self.secret, algorithms=[self.algorithm]) - return identity.get(self.key) # type: ignore[no-any-return] async def remember(self, request: web.Request, response: web.StreamResponse, From 711b8ac5571360ea2a9905b5dfd1db143c2171af Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 3 Feb 2025 23:50:12 +0000 Subject: [PATCH 3/7] Update jwt_identity.py --- aiohttp_security/jwt_identity.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/aiohttp_security/jwt_identity.py b/aiohttp_security/jwt_identity.py index b5ba2008..bc43c393 100644 --- a/aiohttp_security/jwt_identity.py +++ b/aiohttp_security/jwt_identity.py @@ -2,19 +2,20 @@ """ -from typing import Optional +from typing import Optional, Tuple, Type from aiohttp import web from .abc import AbstractIdentityPolicy +_bases_error: Tuple[Type[jwt.exceptions.PyJWTError], ...] try: import jwt HAS_JWT = True - _bases_error = (jwt.exceptions.PyJWTError, ValueError) + _bases_error = (jwt.exceptions.PyJWTError,) except ImportError: # pragma: no cover HAS_JWT = False - _bases_error = (ValueError, ) + _bases_error = () AUTH_HEADER_NAME = 'Authorization' @@ -23,7 +24,7 @@ # This class inherits from ValueError to maintain backward compatibility # with previous versions of aiohttp-security -class InvalidAuthorizationScheme(*_bases_error): +class InvalidAuthorizationScheme(ValueError, *_bases_error): # type: ignore[misc] pass From 9157f9303adc91d897f63db1ff293794f3bae7c7 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 3 Feb 2025 23:51:42 +0000 Subject: [PATCH 4/7] Update test_jwt_identity.py --- tests/test_jwt_identity.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_jwt_identity.py b/tests/test_jwt_identity.py index ad044683..0b9cd207 100644 --- a/tests/test_jwt_identity.py +++ b/tests/test_jwt_identity.py @@ -85,7 +85,7 @@ async def check(request): async def test_identify_expired_signature(make_token, aiohttp_client): kwt_secret_key = "Key" # noqa: S105 - token = make_token({'login': 'Andrew', 'exp': 0}, kwt_secret_key) + token = make_token({"login": "Andrew", "exp": 0}, kwt_secret_key) async def check(request): policy = request.app[IDENTITY_KEY] @@ -94,14 +94,14 @@ async def check(request): except jwt.exceptions.PyJWTError as exc: raise web.HTTPBadRequest(reason=str(exc)) - return web.Response() + assert False app = web.Application() _setup(app, JWTIdentityPolicy(kwt_secret_key), Autz()) - app.router.add_route('GET', '/', check) + app.router.add_route("GET", "/", check) client = await aiohttp_client(app) headers = {"Authorization": "Bearer {}".format(token)} - resp = await client.get('/', headers=headers) + resp = await client.get("/", headers=headers) assert 400 == resp.status - assert 'Signature has expired' in resp.reason + assert "Signature has expired" in resp.reason From 3b9c808a588a46d187367f6198e0912bbb0dd20d Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 3 Feb 2025 23:55:05 +0000 Subject: [PATCH 5/7] Update jwt_identity.py --- aiohttp_security/jwt_identity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_security/jwt_identity.py b/aiohttp_security/jwt_identity.py index bc43c393..68975799 100644 --- a/aiohttp_security/jwt_identity.py +++ b/aiohttp_security/jwt_identity.py @@ -25,7 +25,7 @@ # This class inherits from ValueError to maintain backward compatibility # with previous versions of aiohttp-security class InvalidAuthorizationScheme(ValueError, *_bases_error): # type: ignore[misc] - pass + """Exception when the auth method can't be read from header.""" class JWTIdentityPolicy(AbstractIdentityPolicy): From ec1cd8f4546908aedeedd81532136443cc06b37e Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 3 Feb 2025 23:57:34 +0000 Subject: [PATCH 6/7] Update jwt_identity.py --- aiohttp_security/jwt_identity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp_security/jwt_identity.py b/aiohttp_security/jwt_identity.py index 68975799..b322cda9 100644 --- a/aiohttp_security/jwt_identity.py +++ b/aiohttp_security/jwt_identity.py @@ -8,10 +8,10 @@ from .abc import AbstractIdentityPolicy -_bases_error: Tuple[Type[jwt.exceptions.PyJWTError], ...] try: import jwt HAS_JWT = True + _bases_error: Tuple[Type[jwt.exceptions.PyJWTError], ...] _bases_error = (jwt.exceptions.PyJWTError,) except ImportError: # pragma: no cover HAS_JWT = False From 4735fdcfd7b7672f179f579d89eaa53bf5198d85 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 3 Feb 2025 23:59:49 +0000 Subject: [PATCH 7/7] Update .flake8 --- .flake8 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.flake8 b/.flake8 index 4e487c57..76ddaa85 100644 --- a/.flake8 +++ b/.flake8 @@ -10,8 +10,9 @@ select = A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,B901,B902,B903,B950 # W503: Mutually exclusive with W504. ignore = E226,E501,E722,W503 per-file-ignores = + # B011: assert False used for coverage skipping # S101: Pytest uses assert - tests/*:S101 + tests/*:B011,S101 # flake8-import-order application-import-names = aiohttp_security