diff --git a/atlassian_jwt_auth/contrib/django/__init__.py b/atlassian_jwt_auth/contrib/django/__init__.py deleted file mode 100644 index 9f7e5b8..0000000 --- a/atlassian_jwt_auth/contrib/django/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -import warnings - -warnings.warn( - "The atlassian_jwt_auth.contrib.django package is deprecated in 4.0.0 " - "in favour of atlassian_jwt_auth.frameworks.django.", - DeprecationWarning, - stacklevel=2, -) diff --git a/atlassian_jwt_auth/contrib/django/decorators.py b/atlassian_jwt_auth/contrib/django/decorators.py deleted file mode 100644 index 2438fb6..0000000 --- a/atlassian_jwt_auth/contrib/django/decorators.py +++ /dev/null @@ -1,68 +0,0 @@ -from collections.abc import Callable -from functools import wraps -from typing import Iterable, Optional - -from django.http.response import HttpResponse - -from atlassian_jwt_auth.frameworks.django.decorators import with_asap - - -def validate_asap( - issuers: Optional[Iterable[str]] = None, - subjects: Optional[Iterable[str]] = None, - required: bool = True, -) -> Callable: - """Decorator to allow endpoint-specific ASAP authorization, assuming ASAP - authentication has already occurred. - - :param list issuers: A list of issuers that are allowed to use the - endpoint. - :param list subjects: A list of subjects that are allowed to use the - endpoint. - :param boolean required: Whether or not to require ASAP on this endpoint. - Note that requirements will be still be verified if claims are present. - """ - - def validate_asap_decorator(func): - @wraps(func) - def validate_asap_wrapper(request, *args, **kwargs): - asap_claims = getattr(request, "asap_claims", None) - if required and not asap_claims: - message = "Unauthorized: Invalid or missing token" - response = HttpResponse(message, status=401) - response["WWW-Authenticate"] = "Bearer" - return response - - if asap_claims: - iss = asap_claims["iss"] - if issuers and iss not in issuers: - message = "Forbidden: Invalid token issuer" - return HttpResponse(message, status=403) - - sub = asap_claims.get("sub") - if subjects and sub not in subjects: - message = "Forbidden: Invalid token subject" - return HttpResponse(message, status=403) - - return func(request, *args, **kwargs) - - return validate_asap_wrapper - - return validate_asap_decorator - - -def requires_asap( - issuers: Optional[Iterable[str]] = None, - subject_should_match_issuer: Optional[bool] = None, - func: Optional[Callable] = None, -) -> Callable: - """Decorator for Django endpoints to require ASAP - - :param list issuers: *required The 'iss' claims that this endpoint is from. - """ - return with_asap( - func=func, - required=True, - issuers=issuers, - subject_should_match_issuer=subject_should_match_issuer, - ) diff --git a/atlassian_jwt_auth/contrib/django/middleware.py b/atlassian_jwt_auth/contrib/django/middleware.py deleted file mode 100644 index cf22e9e..0000000 --- a/atlassian_jwt_auth/contrib/django/middleware.py +++ /dev/null @@ -1,66 +0,0 @@ -from typing import Any, Callable, Optional - -from django.conf import settings -from django.utils.deprecation import MiddlewareMixin - -from atlassian_jwt_auth.frameworks.django.middleware import OldStyleASAPMiddleware - - -class ProxiedAsapMiddleware(OldStyleASAPMiddleware, MiddlewareMixin): - """Enable client auth for ASAP-enabled services that are forwarding - non-ASAP client requests. - - This must come before any authentication middleware.""" - - def __init__(self, get_response: Optional[Any] = None) -> None: - super(ProxiedAsapMiddleware, self).__init__() - self.get_response = get_response - - # Rely on this header to tell us if a request has been forwarded - # from an ASAP-enabled service; will overwrite X-Forwarded-For - self.xfwd = getattr( - settings, "ASAP_PROXIED_FORWARDED_FOR_HEADER", "HTTP_X_ASAP_FORWARDED_FOR" - ) - - # This header won't always be set, i.e. some users will be anonymous - self.xauth = getattr( - settings, "ASAP_PROXIED_AUTHORIZATION_HEADER", "HTTP_X_ASAP_AUTHORIZATION" - ) - - def process_request(self, request) -> Optional[str]: - error_response = super(ProxiedAsapMiddleware, self).process_request(request) - - if error_response: - return error_response - - forwarded_for = request.META.pop(self.xfwd, None) - if forwarded_for is None: - return None - - request.asap_forwarded = True - request.META["HTTP_X_FORWARDED_FOR"] = forwarded_for - - asap_auth = request.META.pop("HTTP_AUTHORIZATION", None) - orig_auth = request.META.pop(self.xauth, None) - - # Swap original client header in to allow regular auth middleware - if orig_auth is not None: - request.META["HTTP_AUTHORIZATION"] = orig_auth - if asap_auth is not None: - request.META[self.xauth] = asap_auth - return None - - def process_view( - self, request: Any, view_func: Callable, view_args: Any, view_kwargs: Any - ) -> None: - if not hasattr(request, "asap_forwarded"): - return None - - # swap headers back into place - asap_auth = request.META.pop(self.xauth, None) - orig_auth = request.META.pop("HTTP_AUTHORIZATION", None) - - if asap_auth is not None: - request.META["HTTP_AUTHORIZATION"] = asap_auth - if orig_auth is not None: - request.META[self.xauth] = orig_auth diff --git a/atlassian_jwt_auth/contrib/flask_app/__init__.py b/atlassian_jwt_auth/contrib/flask_app/__init__.py deleted file mode 100644 index 5acc92b..0000000 --- a/atlassian_jwt_auth/contrib/flask_app/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -import warnings - -from .decorators import requires_asap - -__all__ = [ - "requires_asap", -] - -warnings.warn( - "The atlassian_jwt_auth.contrib.flask_app package is deprecated in 4.0.0 " - "in favour of atlassian_jwt_auth.frameworks.flask.", - DeprecationWarning, - stacklevel=2, -) diff --git a/atlassian_jwt_auth/contrib/flask_app/decorators.py b/atlassian_jwt_auth/contrib/flask_app/decorators.py deleted file mode 100644 index 5485085..0000000 --- a/atlassian_jwt_auth/contrib/flask_app/decorators.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import Callable, Iterable, Optional - -from atlassian_jwt_auth.frameworks.flask.decorators import with_asap - - -def requires_asap( - f: Callable, - issuers: Optional[Iterable[str]] = None, - subject_should_match_issuer: Optional[bool] = None, -) -> Callable: - """ - Wrapper for Flask endpoints to make them require asap authentication to - access. - """ - - return with_asap( - func=f, - required=True, - issuers=issuers, - subject_should_match_issuer=subject_should_match_issuer, - ) diff --git a/atlassian_jwt_auth/contrib/server/__init__.py b/atlassian_jwt_auth/contrib/server/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/atlassian_jwt_auth/frameworks/django/__init__.py b/atlassian_jwt_auth/frameworks/django/__init__.py index 2b29c0d..0f5cc56 100644 --- a/atlassian_jwt_auth/frameworks/django/__init__.py +++ b/atlassian_jwt_auth/frameworks/django/__init__.py @@ -1,4 +1,10 @@ -from .decorators import restrict_asap, with_asap +from .decorators import requires_asap, restrict_asap, with_asap from .middleware import OldStyleASAPMiddleware, asap_middleware -__all__ = ["restrict_asap", "with_asap", "OldStyleASAPMiddleware", "asap_middleware"] +__all__ = [ + "restrict_asap", + "with_asap", + "requires_asap", + "OldStyleASAPMiddleware", + "asap_middleware", +] diff --git a/atlassian_jwt_auth/frameworks/django/decorators.py b/atlassian_jwt_auth/frameworks/django/decorators.py index aab43a0..1ff6d2b 100644 --- a/atlassian_jwt_auth/frameworks/django/decorators.py +++ b/atlassian_jwt_auth/frameworks/django/decorators.py @@ -63,3 +63,20 @@ def restrict_asap( required, subject_should_match_issuer=None, ) + + +def requires_asap( + issuers: Optional[Iterable[str]] = None, + subject_should_match_issuer: Optional[bool] = None, + func: Optional[Callable] = None, +) -> Callable: + """Decorator for Django endpoints to require ASAP + + :param list issuers: *required The 'iss' claims that this endpoint is from. + """ + return with_asap( + func=func, + required=True, + issuers=issuers, + subject_should_match_issuer=subject_should_match_issuer, + ) diff --git a/atlassian_jwt_auth/frameworks/django/tests/test_django.py b/atlassian_jwt_auth/frameworks/django/tests/test_django.py index 026bbc1..1f3e6f7 100644 --- a/atlassian_jwt_auth/frameworks/django/tests/test_django.py +++ b/atlassian_jwt_auth/frameworks/django/tests/test_django.py @@ -196,18 +196,6 @@ def test_request_decorated_issuer_is_allowed(self): retriever_key="whitelist/key01", ) - # TODO: modify JWTAuthSigner to allow non-issuer subjects and update the - # decorated subject test cases - def test_request_non_decorated_subject_is_rejected(self): - self.check_response( - "restricted_subject", - "Forbidden", - 403, - issuer="whitelist", - key_id="whitelist/key01", - retriever_key="whitelist/key01", - ) - def test_request_using_settings_only_is_allowed(self): self.check_response("unneeded", "two") diff --git a/atlassian_jwt_auth/frameworks/django/tests/urls.py b/atlassian_jwt_auth/frameworks/django/tests/urls.py index 2c7dbd5..3d07d19 100644 --- a/atlassian_jwt_auth/frameworks/django/tests/urls.py +++ b/atlassian_jwt_auth/frameworks/django/tests/urls.py @@ -29,9 +29,4 @@ views.restricted_issuer_view, name="restricted_issuer", ), - path( - "asap/restricted_subject", - views.restricted_subject_view, - name="restricted_subject", - ), ] diff --git a/atlassian_jwt_auth/frameworks/django/tests/views.py b/atlassian_jwt_auth/frameworks/django/tests/views.py index 45aef00..bbac17f 100644 --- a/atlassian_jwt_auth/frameworks/django/tests/views.py +++ b/atlassian_jwt_auth/frameworks/django/tests/views.py @@ -1,7 +1,6 @@ from django.http import HttpResponse -from atlassian_jwt_auth.contrib.django.decorators import requires_asap, validate_asap -from atlassian_jwt_auth.frameworks.django import restrict_asap, with_asap +from atlassian_jwt_auth.frameworks.django import requires_asap, restrict_asap, with_asap @with_asap(issuers=["client-app"]) @@ -52,8 +51,3 @@ def unneeded_view(request): @restrict_asap(issuers=["whitelist"]) def restricted_issuer_view(request): return HttpResponse("three") - - -@validate_asap(subjects=["client-app"]) -def restricted_subject_view(request): - return HttpResponse("four") diff --git a/atlassian_jwt_auth/frameworks/flask/__init__.py b/atlassian_jwt_auth/frameworks/flask/__init__.py index 3394e9c..6f64b31 100644 --- a/atlassian_jwt_auth/frameworks/flask/__init__.py +++ b/atlassian_jwt_auth/frameworks/flask/__init__.py @@ -1,3 +1,3 @@ -from .decorators import with_asap +from .decorators import requires_asap, with_asap -__all__ = ["with_asap"] +__all__ = ["with_asap", "requires_asap"] diff --git a/atlassian_jwt_auth/frameworks/flask/decorators.py b/atlassian_jwt_auth/frameworks/flask/decorators.py index e81fb4e..08c6adf 100644 --- a/atlassian_jwt_auth/frameworks/flask/decorators.py +++ b/atlassian_jwt_auth/frameworks/flask/decorators.py @@ -32,3 +32,21 @@ def with_asap( return _with_asap( func, FlaskBackend(), issuers, required, subject_should_match_issuer ) + + +def requires_asap( + f: Callable, + issuers: Optional[Iterable[str]] = None, + subject_should_match_issuer: Optional[bool] = None, +) -> Callable: + """ + Wrapper for Flask endpoints to make them require asap authentication to + access. + """ + + return with_asap( + func=f, + required=True, + issuers=issuers, + subject_should_match_issuer=subject_should_match_issuer, + ) diff --git a/atlassian_jwt_auth/frameworks/flask/tests/test_flask.py b/atlassian_jwt_auth/frameworks/flask/tests/test_flask.py index dc560ec..7a4b1d9 100644 --- a/atlassian_jwt_auth/frameworks/flask/tests/test_flask.py +++ b/atlassian_jwt_auth/frameworks/flask/tests/test_flask.py @@ -2,9 +2,8 @@ from flask import Flask -from atlassian_jwt_auth.contrib.flask_app import requires_asap from atlassian_jwt_auth.contrib.tests.utils import get_static_retriever_class -from atlassian_jwt_auth.frameworks.flask import with_asap +from atlassian_jwt_auth.frameworks.flask import requires_asap, with_asap from atlassian_jwt_auth.tests import utils from atlassian_jwt_auth.tests.utils import ( create_token, diff --git a/atlassian_jwt_auth/signer.py b/atlassian_jwt_auth/signer.py index 5487aac..cd9fb0c 100644 --- a/atlassian_jwt_auth/signer.py +++ b/atlassian_jwt_auth/signer.py @@ -6,6 +6,7 @@ import jwt from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization +from jwt.types import Options from atlassian_jwt_auth import algorithms, key from atlassian_jwt_auth.key import BasePrivateKeyRetriever, KeyIdentifier @@ -116,8 +117,9 @@ def can_reuse_token(self, existing_token, claims) -> bool: """ if existing_token is None: return False + existing_claims = jwt.decode( - existing_token, options={"verify_signature": False} + existing_token, options=Options(verify_signature=False) ) existing_lifetime = int(existing_claims["exp"]) - int(existing_claims["iat"]) this_lifetime = (claims["exp"] - claims["iat"]).total_seconds() diff --git a/atlassian_jwt_auth/verifier.py b/atlassian_jwt_auth/verifier.py index 4575f18..2a5c50c 100644 --- a/atlassian_jwt_auth/verifier.py +++ b/atlassian_jwt_auth/verifier.py @@ -10,6 +10,7 @@ from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey from jwt import PyJWK from jwt.exceptions import InvalidAlgorithmError +from jwt.types import Options from atlassian_jwt_auth import KeyIdentifier, algorithms, exceptions, key from atlassian_jwt_auth.key import BasePublicKeyRetriever @@ -96,12 +97,12 @@ def _decode_jwt( leeway: int = 0, ) -> Dict[Any, Any]: """Decode JWT and check if it's valid""" - options = { - "verify_signature": True, - "require": ["exp", "iat"], - "require_exp": True, - "require_iat": True, - } + options: Options = Options( + verify_signature=True, + require=["exp", "iat"], + verify_exp=True, + verify_iat=True, + ) claims = jwt.decode( a_jwt, diff --git a/requirements.txt b/requirements.txt index c6eeec0..18b2a99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -PyJWT>=2.4.0,<3.0.0 +PyJWT>=2.11.0,<3.0.0 PyJWT[crypto]>=2.4.0,<3.0.0 requests>=2.8.1,<3.0.0 CacheControl diff --git a/setup.py b/setup.py index 6e64fbe..e7c8900 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( - setup_requires=["pbr<7.0.0"], + setup_requires=["pbr<8.0.0"], pbr=True, platforms=["any"], zip_safe=False,