From 06d313233cb8ff3e53f0b04a2d9ce1c9405f2865 Mon Sep 17 00:00:00 2001 From: Mathis Felardos <3902859+hasB4K@users.noreply.github.com> Date: Tue, 27 Feb 2024 01:36:19 +0100 Subject: [PATCH 1/6] jwt_backends: create backend mechanism and add authlib support --- fastapi_jwt/__init__.py | 1 + fastapi_jwt/jwt.py | 91 ++++------ fastapi_jwt/jwt_backends/__init__.py | 9 + fastapi_jwt/jwt_backends/abstract_backend.py | 31 ++++ fastapi_jwt/jwt_backends/authlib_backend.py | 51 ++++++ .../jwt_backends/python_jose_backend.py | 47 +++++ pyproject.toml | 9 +- tests/mock_datetime_utils.py | 39 +++++ tests/test_security_jwt_bearer.py | 86 +++++---- tests/test_security_jwt_bearer_optional.py | 98 +++++++---- tests/test_security_jwt_cookie.py | 78 ++++++--- tests/test_security_jwt_cookie_optional.py | 90 ++++++---- tests/test_security_jwt_general.py | 155 +++++++++-------- tests/test_security_jwt_general_optional.py | 163 +++++++++--------- tests/test_security_jwt_multiple_places.py | 94 ++++++---- tests/test_security_jwt_set_cookie.py | 52 +++--- 16 files changed, 697 insertions(+), 397 deletions(-) create mode 100644 fastapi_jwt/jwt_backends/__init__.py create mode 100644 fastapi_jwt/jwt_backends/abstract_backend.py create mode 100644 fastapi_jwt/jwt_backends/authlib_backend.py create mode 100644 fastapi_jwt/jwt_backends/python_jose_backend.py create mode 100644 tests/mock_datetime_utils.py diff --git a/fastapi_jwt/__init__.py b/fastapi_jwt/__init__.py index 25585c5..a8099f8 100644 --- a/fastapi_jwt/__init__.py +++ b/fastapi_jwt/__init__.py @@ -1 +1,2 @@ from .jwt import * # noqa: F401, F403 +from .jwt_backends import * # noqa: F401, F403 diff --git a/fastapi_jwt/jwt.py b/fastapi_jwt/jwt.py index d9041ed..a1b32e6 100644 --- a/fastapi_jwt/jwt.py +++ b/fastapi_jwt/jwt.py @@ -8,11 +8,21 @@ from fastapi.responses import Response from fastapi.security import APIKeyCookie, HTTPBearer from starlette.status import HTTP_401_UNAUTHORIZED +from .jwt_backends import AuthlibJWTBackend, PythonJoseJWTBackend -try: - from jose import jwt -except ImportError: # pragma: nocover - jwt = None # type: ignore[assignment] + +DEFAULT_JWT_BACKEND = None + + +def define_default_jwt_backend(cls): + global DEFAULT_JWT_BACKEND + DEFAULT_JWT_BACKEND = cls + + +if AuthlibJWTBackend is not None: + define_default_jwt_backend(AuthlibJWTBackend) +elif PythonJoseJWTBackend is not None: + define_default_jwt_backend(PythonJoseJWTBackend) def utcnow(): @@ -27,6 +37,7 @@ def utcnow(): __all__ = [ + "define_default_jwt_backend", "JwtAuthorizationCredentials", "JwtAccessBearer", "JwtAccessCookie", @@ -72,28 +83,26 @@ def __init__( secret_key: str, places: Optional[Set[str]] = None, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): - assert jwt is not None, "python-jose must be installed to use JwtAuth" + self.jwt_backend = DEFAULT_JWT_BACKEND(algorithm) + self.secret_key = secret_key if places: assert places.issubset( {"header", "cookie"} ), "only 'header'/'cookie' are supported" - algorithm = algorithm.upper() - assert ( - hasattr(jwt.ALGORITHMS, algorithm) is True # type: ignore[attr-defined] - ), f"{algorithm} algorithm is not supported by python-jose library" - - self.secret_key = secret_key self.places = places or {"header"} self.auto_error = auto_error - self.algorithm = algorithm self.access_expires_delta = access_expires_delta or timedelta(minutes=15) self.refresh_expires_delta = refresh_expires_delta or timedelta(days=31) + @property + def algorithm(self): + return self.jwt_backend.algorithm + @classmethod def from_other( cls, @@ -112,30 +121,6 @@ def from_other( refresh_expires_delta=refresh_expires_delta or other.refresh_expires_delta, ) - def _decode(self, token: str) -> Optional[Dict[str, Any]]: - try: - payload: Dict[str, Any] = jwt.decode( - token, - self.secret_key, - algorithms=[self.algorithm], - options={"leeway": 10}, - ) - return payload - except jwt.ExpiredSignatureError as e: # type: ignore[attr-defined] - if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}" - ) - else: - return None - except jwt.JWTError as e: # type: ignore[attr-defined] - if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}" - ) - else: - return None - def _generate_payload( self, subject: Dict[str, Any], @@ -144,7 +129,6 @@ def _generate_payload( token_type: str, ) -> Dict[str, Any]: now = utcnow() - return { "subject": subject.copy(), # main subject "type": token_type, # 'access' or 'refresh' token @@ -172,8 +156,7 @@ async def _get_payload( return None # Try to decode jwt token. auto_error on error - payload = self._decode(token) - return payload + return self.jwt_backend.decode(token, self.secret_key, self.auto_error) def create_access_token( self, @@ -186,11 +169,7 @@ def create_access_token( to_encode = self._generate_payload( subject, expires_delta, unique_identifier, "access" ) - - jwt_encoded: str = jwt.encode( - to_encode, self.secret_key, algorithm=self.algorithm - ) - return jwt_encoded + return self.jwt_backend.encode(to_encode, self.secret_key) def create_refresh_token( self, @@ -203,11 +182,7 @@ def create_refresh_token( to_encode = self._generate_payload( subject, expires_delta, unique_identifier, "refresh" ) - - jwt_encoded: str = jwt.encode( - to_encode, self.secret_key, algorithm=self.algorithm - ) - return jwt_encoded + return self.jwt_backend.encode(to_encode, self.secret_key) @staticmethod def set_access_cookie( @@ -261,7 +236,7 @@ def __init__( secret_key: str, places: Optional[Set[str]] = None, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): @@ -293,7 +268,7 @@ def __init__( self, secret_key: str, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): @@ -317,7 +292,7 @@ def __init__( self, secret_key: str, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): @@ -342,7 +317,7 @@ def __init__( self, secret_key: str, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): @@ -372,7 +347,7 @@ def __init__( secret_key: str, places: Optional[Set[str]] = None, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): @@ -414,7 +389,7 @@ def __init__( self, secret_key: str, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): @@ -438,7 +413,7 @@ def __init__( self, secret_key: str, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): @@ -463,7 +438,7 @@ def __init__( self, secret_key: str, auto_error: bool = True, - algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] + algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): diff --git a/fastapi_jwt/jwt_backends/__init__.py b/fastapi_jwt/jwt_backends/__init__.py new file mode 100644 index 0000000..310f02c --- /dev/null +++ b/fastapi_jwt/jwt_backends/__init__.py @@ -0,0 +1,9 @@ +try: + from .authlib_backend import AuthlibJWTBackend +except ImportError: + AuthlibJWTBackend = None + +try: + from .python_jose_backend import PythonJoseJWTBackend +except ImportError: + PythonJoseJWTBackend = None diff --git a/fastapi_jwt/jwt_backends/abstract_backend.py b/fastapi_jwt/jwt_backends/abstract_backend.py new file mode 100644 index 0000000..ca337ae --- /dev/null +++ b/fastapi_jwt/jwt_backends/abstract_backend.py @@ -0,0 +1,31 @@ +from abc import ABCMeta, abstractmethod, abstractproperty +from typing import Any, Dict, Optional, Self + + + +class AbstractJWTBackend(metaclass=ABCMeta): + + # simple "SingletonArgs" implementation to keep a JWTBackend per algorithm + _instances = {} + + def __new__(cls, algorithm) -> Self: + instance_key = (cls, algorithm) + if instance_key not in cls._instances: + cls._instances[instance_key] = super(AbstractJWTBackend, cls).__new__(cls) + return cls._instances[instance_key] + + @abstractmethod + def __init__(self, algorithm) -> None: + pass + + @abstractproperty + def default_algorithm(self) -> str: + pass + + @abstractmethod + def encode(self, to_encode, secret_key) -> str: + pass + + @abstractmethod + def decode(self, token, secret_key, auto_error) -> Optional[Dict[str, Any]]: + pass diff --git a/fastapi_jwt/jwt_backends/authlib_backend.py b/fastapi_jwt/jwt_backends/authlib_backend.py new file mode 100644 index 0000000..1dfe933 --- /dev/null +++ b/fastapi_jwt/jwt_backends/authlib_backend.py @@ -0,0 +1,51 @@ +from fastapi import HTTPException +from typing import Any, Dict, Optional +from starlette.status import HTTP_401_UNAUTHORIZED + +from authlib.jose import JsonWebSignature, JsonWebToken +from authlib.jose.errors import ( + DecodeError, ExpiredTokenError, InvalidClaimError, InvalidTokenError +) +from .abstract_backend import AbstractJWTBackend + + +class AuthlibJWTBackend(AbstractJWTBackend): + + def __init__(self, algorithm) -> None: + self.algorithm = algorithm if algorithm is not None else self.default_algorithm + # from https://github.com/lepture/authlib/blob/85f9ff/authlib/jose/__init__.py#L45 + valid_algorithms = JsonWebSignature.ALGORITHMS_REGISTRY.keys() + assert ( + self.algorithm in valid_algorithms + ), f"{self.algorithm} algorithm is not supported by authlib" + self.jwt = JsonWebToken(algorithms=[self.algorithm]) + + @property + def default_algorithm(self) -> str: + return "HS256" + + def encode(self, to_encode, secret_key) -> str: + token = self.jwt.encode(header={"alg": self.algorithm}, payload=to_encode, key=secret_key) + return token.decode() # convert to string + + def decode(self, token, secret_key, auto_error) -> Optional[Dict[str, Any]]: + try: + payload = self.jwt.decode(token, secret_key) + payload.validate(leeway=10) + return dict(payload) + except ExpiredTokenError as e: # type: ignore[attr-defined] + if auto_error: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}" + ) + else: + return None + except (InvalidClaimError, + InvalidTokenError, + DecodeError) as e: # type: ignore[attr-defined] + if auto_error: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}" + ) + else: + return None diff --git a/fastapi_jwt/jwt_backends/python_jose_backend.py b/fastapi_jwt/jwt_backends/python_jose_backend.py new file mode 100644 index 0000000..521cbb7 --- /dev/null +++ b/fastapi_jwt/jwt_backends/python_jose_backend.py @@ -0,0 +1,47 @@ +from fastapi import HTTPException +from typing import Any, Dict, Optional +from starlette.status import HTTP_401_UNAUTHORIZED + +from jose import jwt + +from .abstract_backend import AbstractJWTBackend + + +class PythonJoseJWTBackend(AbstractJWTBackend): + + def __init__(self, algorithm) -> None: + self.algorithm = algorithm if algorithm is not None else self.default_algorithm + assert ( + hasattr(jwt.ALGORITHMS, self.algorithm) is True # type: ignore[attr-defined] + ), f"{algorithm} algorithm is not supported by python-jose library" + + @property + def default_algorithm(self) -> str: + return jwt.ALGORITHMS.HS256 + + def encode(self, to_encode, secret_key) -> str: + return jwt.encode(to_encode, secret_key, algorithm=self.algorithm) + + def decode(self, token, secret_key, auto_error) -> Optional[Dict[str, Any]]: + try: + payload: Dict[str, Any] = jwt.decode( + token, + secret_key, + algorithms=[self.algorithm], + options={"leeway": 10}, + ) + return payload + except jwt.ExpiredSignatureError as e: # type: ignore[attr-defined] + if auto_error: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}" + ) + else: + return None + except jwt.JWTError as e: # type: ignore[attr-defined] + if auto_error: + raise HTTPException( + status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}" + ) + else: + return None diff --git a/pyproject.toml b/pyproject.toml index 28e0bd1..03b47f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,6 @@ classifiers = [ dependencies = [ "fastapi >=0.50.0", - "python-jose[cryptography] >=3.3.0" ] @@ -37,7 +36,15 @@ documentation = "https://k4black.github.io/fastapi-jwt/" [project.optional-dependencies] +authlib = [ + "Authlib >=1.3.0" +] +python_jose = [ + "python-jose[cryptography] >=3.3.0" +] test = [ + "Authlib >=1.3.0", + "python-jose[cryptography] >=3.3.0", "httpx >=0.23.0,<1.0.0", "pytest >=7.0.0,<9.0.0", "pytest-cov >=4.0.0,<5.0.0", diff --git a/tests/mock_datetime_utils.py b/tests/mock_datetime_utils.py new file mode 100644 index 0000000..9c720e7 --- /dev/null +++ b/tests/mock_datetime_utils.py @@ -0,0 +1,39 @@ +import datetime +import time + +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend + + +_time = time.time +_now = datetime.datetime.now +_utcnow = datetime.datetime.utcnow + + +def create_datetime_mock(**timedelta_kwargs): + + class _FakeDateTime(datetime.datetime): # pragma: no cover + @staticmethod + def now(**kwargs): + return _now() + datetime.timedelta(**timedelta_kwargs) + + @staticmethod + def utcnow(**kwargs): + return _utcnow() + datetime.timedelta(**timedelta_kwargs) + + return _FakeDateTime + + +def create_time_time_mock(**kwargs): + def _fake_time_time(): + return _time() + datetime.timedelta(**kwargs).total_seconds() + + return _fake_time_time + + +def mock_now_for_backend(mocker, jwt_backend, **kwargs): + if jwt_backend is AuthlibJWTBackend: + mocker.patch("authlib.jose.rfc7519.claims.time.time", create_time_time_mock(**kwargs)) + elif jwt_backend is PythonJoseJWTBackend: + mocker.patch("jose.jwt.datetime", create_datetime_mock(**kwargs)) + else: + raise Exception("Invalid Backend") diff --git a/tests/test_security_jwt_bearer.py b/tests/test_security_jwt_bearer.py index 104917c..1098a48 100644 --- a/tests/test_security_jwt_bearer.py +++ b/tests/test_security_jwt_bearer.py @@ -1,40 +1,46 @@ +import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend -app = FastAPI() -access_security = JwtAccessBearer(secret_key="secret_key") -refresh_security = JwtRefreshBearer(secret_key="secret_key") +def create_example_client(jwt_backend): + define_default_jwt_backend(jwt_backend) + app = FastAPI() + access_security = JwtAccessBearer(secret_key="secret_key") + refresh_security = JwtRefreshBearer(secret_key="secret_key") -@app.post("/auth") -def auth(): - subject = {"username": "username", "role": "user"} - access_token = access_security.create_access_token(subject=subject) - refresh_token = access_security.create_refresh_token(subject=subject) + @app.post("/auth") + def auth(): + subject = {"username": "username", "role": "user"} - return {"access_token": access_token, "refresh_token": refresh_token} + access_token = access_security.create_access_token(subject=subject) + refresh_token = access_security.create_refresh_token(subject=subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.post("/refresh") -def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): - access_token = refresh_security.create_access_token(subject=credentials.subject) - refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) - return {"access_token": access_token, "refresh_token": refresh_token} + @app.post("/refresh") + def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): + access_token = refresh_security.create_access_token(subject=credentials.subject) + refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.get("/users/me") -def read_current_user( - credentials: JwtAuthorizationCredentials = Security(access_security), -): - return {"username": credentials["username"], "role": credentials["role"]} + @app.get("/users/me") + def read_current_user( + credentials: JwtAuthorizationCredentials = Security(access_security), + ): + return {"username": credentials["username"], "role": credentials["role"]} + + + return TestClient(app) -client = TestClient(app) openapi_schema = { "openapi": "3.1.0", @@ -88,18 +94,24 @@ def read_current_user( } -def test_openapi_schema(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_openapi_schema(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -def test_security_jwt_auth(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_auth(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text -def test_security_jwt_access_bearer(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer(jwt_backend): + client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( @@ -109,27 +121,35 @@ def test_security_jwt_access_bearer(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_bearer_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer_wrong(jwt_backend): + client = create_example_client(jwt_backend) response = client.get( "/users/me", headers={"Authorization": "Bearer wrong_access_token"} ) assert response.status_code == 401, response.text -def test_security_jwt_access_bearer_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/users/me") assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} -def test_security_jwt_access_bearer_incorrect_scheme_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} # assert response.json() == {"detail": "Invalid authentication credentials"} -def test_security_jwt_refresh_bearer(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer(jwt_backend): + client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] response = client.post( @@ -138,20 +158,26 @@ def test_security_jwt_refresh_bearer(): assert response.status_code == 200, response.text -def test_security_jwt_refresh_bearer_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer_wrong(jwt_backend): + client = create_example_client(jwt_backend) response = client.post( "/refresh", headers={"Authorization": "Bearer wrong_refresh_token"} ) assert response.status_code == 401, response.text -def test_security_jwt_refresh_bearer_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/refresh") assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} -def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Basic notreally"}) assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} diff --git a/tests/test_security_jwt_bearer_optional.py b/tests/test_security_jwt_bearer_optional.py index 5029ff6..a1aae21 100644 --- a/tests/test_security_jwt_bearer_optional.py +++ b/tests/test_security_jwt_bearer_optional.py @@ -1,49 +1,55 @@ from typing import Optional +import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend -app = FastAPI() -access_security = JwtAccessBearer(secret_key="secret_key", auto_error=False) -refresh_security = JwtRefreshBearer(secret_key="secret_key", auto_error=False) +def create_example_client(jwt_backend): + define_default_jwt_backend(jwt_backend) + app = FastAPI() + access_security = JwtAccessBearer(secret_key="secret_key", auto_error=False) + refresh_security = JwtRefreshBearer(secret_key="secret_key", auto_error=False) -@app.post("/auth") -def auth(): - subject = {"username": "username", "role": "user"} - access_token = access_security.create_access_token(subject=subject) - refresh_token = access_security.create_refresh_token(subject=subject) + @app.post("/auth") + def auth(): + subject = {"username": "username", "role": "user"} - return {"access_token": access_token, "refresh_token": refresh_token} + access_token = access_security.create_access_token(subject=subject) + refresh_token = access_security.create_refresh_token(subject=subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.post("/refresh") -def refresh( - credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), -): - if credentials is None: - return {"msg": "Create an account first"} - access_token = refresh_security.create_access_token(subject=credentials.subject) - refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) + @app.post("/refresh") + def refresh( + credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), + ): + if credentials is None: + return {"msg": "Create an account first"} - return {"access_token": access_token, "refresh_token": refresh_token} + access_token = refresh_security.create_access_token(subject=credentials.subject) + refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.get("/users/me") -def read_current_user( - credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), -): - if credentials is None: - return {"msg": "Create an account first"} - return {"username": credentials["username"], "role": credentials["role"]} + @app.get("/users/me") + def read_current_user( + credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), + ): + if credentials is None: + return {"msg": "Create an account first"} + return {"username": credentials["username"], "role": credentials["role"]} + + + return TestClient(app) -client = TestClient(app) openapi_schema = { "openapi": "3.1.0", @@ -97,18 +103,24 @@ def read_current_user( } -def test_openapi_schema(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_openapi_schema(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -def test_security_jwt_auth(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_auth(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text -def test_security_jwt_access_bearer(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer(jwt_backend): + client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( @@ -118,7 +130,9 @@ def test_security_jwt_access_bearer(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_bearer_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer_wrong(jwt_backend): + client = create_example_client(jwt_backend) response = client.get( "/users/me", headers={"Authorization": "Bearer wrong_access_token"} ) @@ -126,19 +140,25 @@ def test_security_jwt_access_bearer_wrong(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_access_bearer_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_access_bearer_incorrect_scheme_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_bearer(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer(jwt_backend): + client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] response = client.post( @@ -147,7 +167,9 @@ def test_security_jwt_refresh_bearer(): assert response.status_code == 200, response.text -def test_security_jwt_refresh_bearer_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer_wrong(jwt_backend): + client = create_example_client(jwt_backend) response = client.post( "/refresh", headers={"Authorization": "Bearer wrong_refresh_token"} ) @@ -155,13 +177,17 @@ def test_security_jwt_refresh_bearer_wrong(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_bearer_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/refresh") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Basic notreally"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_jwt_cookie.py b/tests/test_security_jwt_cookie.py index d506eba..8507732 100644 --- a/tests/test_security_jwt_cookie.py +++ b/tests/test_security_jwt_cookie.py @@ -1,40 +1,46 @@ +import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient from fastapi_jwt import JwtAccessCookie, JwtAuthorizationCredentials, JwtRefreshCookie +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend -app = FastAPI() -access_security = JwtAccessCookie(secret_key="secret_key") -refresh_security = JwtRefreshCookie(secret_key="secret_key") +def create_example_client(jwt_backend): + define_default_jwt_backend(jwt_backend) + app = FastAPI() + access_security = JwtAccessCookie(secret_key="secret_key") + refresh_security = JwtRefreshCookie(secret_key="secret_key") -@app.post("/auth") -def auth(): - subject = {"username": "username", "role": "user"} - access_token = access_security.create_access_token(subject=subject) - refresh_token = access_security.create_refresh_token(subject=subject) + @app.post("/auth") + def auth(): + subject = {"username": "username", "role": "user"} - return {"access_token": access_token, "refresh_token": refresh_token} + access_token = access_security.create_access_token(subject=subject) + refresh_token = access_security.create_refresh_token(subject=subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.post("/refresh") -def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): - access_token = refresh_security.create_access_token(subject=credentials.subject) - refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) - return {"access_token": access_token, "refresh_token": refresh_token} + @app.post("/refresh") + def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): + access_token = refresh_security.create_access_token(subject=credentials.subject) + refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.get("/users/me") -def read_current_user( - credentials: JwtAuthorizationCredentials = Security(access_security), -): - return {"username": credentials["username"], "role": credentials["role"]} + @app.get("/users/me") + def read_current_user( + credentials: JwtAuthorizationCredentials = Security(access_security), + ): + return {"username": credentials["username"], "role": credentials["role"]} + + + return TestClient(app) -client = TestClient(app) openapi_schema = { "openapi": "3.1.0", @@ -96,18 +102,24 @@ def read_current_user( } -def test_openapi_schema(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_openapi_schema(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -def test_security_jwt_auth(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_auth(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text -def test_security_jwt_access_cookie(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_cookie(jwt_backend): + client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get("/users/me", cookies={"access_token_cookie": access_token}) @@ -115,21 +127,27 @@ def test_security_jwt_access_cookie(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_cookie_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_cookie_wrong(jwt_backend): + client = create_example_client(jwt_backend) response = client.get( "/users/me", cookies={"access_token_cookie": "wrong_access_token_cookie"} ) assert response.status_code == 401, response.text -def test_security_jwt_access_cookie_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_cookie_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) client.cookies.clear() response = client.get("/users/me", cookies={}) assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} -def test_security_jwt_refresh_cookie(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_cookie(jwt_backend): + client = create_example_client(jwt_backend) client.cookies.clear() refresh_token = client.post("/auth").json()["refresh_token"] @@ -137,14 +155,18 @@ def test_security_jwt_refresh_cookie(): assert response.status_code == 200, response.text -def test_security_jwt_refresh_cookie_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_cookie_wrong(jwt_backend): + client = create_example_client(jwt_backend) response = client.post( "/refresh", cookies={"refresh_token_cookie": "wrong_refresh_token_cookie"} ) assert response.status_code == 401, response.text -def test_security_jwt_refresh_cookie_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_cookie_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) client.cookies.clear() response = client.post("/refresh", cookies={}) assert response.status_code == 401, response.text diff --git a/tests/test_security_jwt_cookie_optional.py b/tests/test_security_jwt_cookie_optional.py index 7cc2f51..1af185e 100644 --- a/tests/test_security_jwt_cookie_optional.py +++ b/tests/test_security_jwt_cookie_optional.py @@ -1,50 +1,56 @@ +import pytest from typing import Optional from fastapi import FastAPI, Security from fastapi.testclient import TestClient from fastapi_jwt import JwtAccessCookie, JwtAuthorizationCredentials, JwtRefreshCookie +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend -app = FastAPI() -access_security = JwtAccessCookie(secret_key="secret_key", auto_error=False) -refresh_security = JwtRefreshCookie(secret_key="secret_key", auto_error=False) +def create_example_client(jwt_backend): + define_default_jwt_backend(jwt_backend) + app = FastAPI() + access_security = JwtAccessCookie(secret_key="secret_key", auto_error=False) + refresh_security = JwtRefreshCookie(secret_key="secret_key", auto_error=False) -@app.post("/auth") -def auth(): - subject = {"username": "username", "role": "user"} - access_token = access_security.create_access_token(subject=subject) - refresh_token = access_security.create_refresh_token(subject=subject) + @app.post("/auth") + def auth(): + subject = {"username": "username", "role": "user"} - return {"access_token": access_token, "refresh_token": refresh_token} + access_token = access_security.create_access_token(subject=subject) + refresh_token = access_security.create_refresh_token(subject=subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.post("/refresh") -def refresh( - credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), -): - if credentials is None: - return {"msg": "Create an account first"} - access_token = refresh_security.create_access_token(subject=credentials.subject) - refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) + @app.post("/refresh") + def refresh( + credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), + ): + if credentials is None: + return {"msg": "Create an account first"} - return {"access_token": access_token, "refresh_token": refresh_token} + access_token = refresh_security.create_access_token(subject=credentials.subject) + refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.get("/users/me") -def read_current_user( - credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), -): - if credentials is None: - return {"msg": "Create an account first"} - return {"username": credentials["username"], "role": credentials["role"]} + @app.get("/users/me") + def read_current_user( + credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), + ): + if credentials is None: + return {"msg": "Create an account first"} + return {"username": credentials["username"], "role": credentials["role"]} + + + return TestClient(app) -client = TestClient(app) openapi_schema = { "openapi": "3.1.0", @@ -106,18 +112,24 @@ def read_current_user( } -def test_openapi_schema(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_openapi_schema(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -def test_security_jwt_auth(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_auth(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text -def test_security_jwt_access_cookie(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_cookie(jwt_backend): + client = create_example_client(jwt_backend) client.cookies.clear() access_token = client.post("/auth").json()["access_token"] @@ -126,7 +138,9 @@ def test_security_jwt_access_cookie(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_cookie_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_cookie_wrong(jwt_backend): + client = create_example_client(jwt_backend) response = client.get( "/users/me", cookies={"access_token_cookie": "wrong_access_token_cookie"} ) @@ -134,20 +148,26 @@ def test_security_jwt_access_cookie_wrong(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_access_cookie_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_cookie_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/users/me", cookies={}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_cookie(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_cookie(jwt_backend): + client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] response = client.post("/refresh", cookies={"refresh_token_cookie": refresh_token}) assert response.status_code == 200, response.text -def test_security_jwt_refresh_cookie_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_cookie_wrong(jwt_backend): + client = create_example_client(jwt_backend) response = client.post( "/refresh", cookies={"refresh_token_cookie": "wrong_refresh_token_cookie"} ) @@ -155,7 +175,9 @@ def test_security_jwt_refresh_cookie_wrong(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_cookie_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_cookie_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/refresh", cookies={}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_jwt_general.py b/tests/test_security_jwt_general.py index 62a160e..59566b8 100644 --- a/tests/test_security_jwt_general.py +++ b/tests/test_security_jwt_general.py @@ -1,85 +1,69 @@ -import datetime from typing import Set from uuid import uuid4 +import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient from pytest_mock import MockerFixture from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend +from .mock_datetime_utils import mock_now_for_backend -app = FastAPI() -access_security = JwtAccessBearer(secret_key="secret_key") -refresh_security = JwtRefreshBearer.from_other(access_security) +def create_example_client(jwt_backend): + define_default_jwt_backend(jwt_backend) + app = FastAPI() + access_security = JwtAccessBearer(secret_key="secret_key") + refresh_security = JwtRefreshBearer.from_other(access_security) + unique_identifiers_database: Set[str] = set() -unique_identifiers_database: Set[str] = set() + @app.post("/auth") + def auth(): + subject = {"username": "username", "role": "user"} + unique_identifier = str(uuid4()) + unique_identifiers_database.add(unique_identifier) -@app.post("/auth") -def auth(): - subject = {"username": "username", "role": "user"} - unique_identifier = str(uuid4()) - unique_identifiers_database.add(unique_identifier) + access_token = access_security.create_access_token( + subject=subject, unique_identifier=unique_identifier + ) + refresh_token = access_security.create_refresh_token(subject=subject) - access_token = access_security.create_access_token( - subject=subject, unique_identifier=unique_identifier - ) - refresh_token = access_security.create_refresh_token(subject=subject) - - return {"access_token": access_token, "refresh_token": refresh_token} - - -@app.post("/refresh") -def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): - unique_identifier = str(uuid4()) - unique_identifiers_database.add(unique_identifier) - - access_token = refresh_security.create_access_token( - subject=credentials.subject, unique_identifier=unique_identifier, - ) - refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) - - return {"access_token": access_token, "refresh_token": refresh_token} + return {"access_token": access_token, "refresh_token": refresh_token} -@app.get("/users/me") -def read_current_user( - credentials: JwtAuthorizationCredentials = Security(access_security), -): - return {"username": credentials["username"], "role": credentials["role"]} + @app.post("/refresh") + def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): + unique_identifier = str(uuid4()) + unique_identifiers_database.add(unique_identifier) + access_token = refresh_security.create_access_token( + subject=credentials.subject, unique_identifier=unique_identifier, + ) + refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) -@app.get("/auth/meta") -def get_token_meta( - credentials: JwtAuthorizationCredentials = Security(access_security), -): - return {"jti": credentials.jti} + return {"access_token": access_token, "refresh_token": refresh_token} -class _FakeDateTimeShort(datetime.datetime): # pragma: no cover - @staticmethod - def now(**kwargs): - return datetime.datetime.now() + datetime.timedelta(minutes=3) + @app.get("/users/me") + def read_current_user( + credentials: JwtAuthorizationCredentials = Security(access_security), + ): + return {"username": credentials["username"], "role": credentials["role"]} - @staticmethod - def utcnow(**kwargs): - return datetime.datetime.utcnow() + datetime.timedelta(minutes=3) + @app.get("/auth/meta") + def get_token_meta( + credentials: JwtAuthorizationCredentials = Security(access_security), + ): + return {"jti": credentials.jti} -class _FakeDateTimeLong(datetime.datetime): # pragma: no cover - @staticmethod - def now(**kwargs): - return datetime.datetime.now() + datetime.timedelta(days=42) - @staticmethod - def utcnow(**kwargs): - return datetime.datetime.utcnow() + datetime.timedelta(days=42) + return TestClient(app), unique_identifiers_database -client = TestClient(app) - openapi_schema = { "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, @@ -145,13 +129,17 @@ def utcnow(**kwargs): } -def test_openapi_schema(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_openapi_schema(jwt_backend): + client, _ = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -def test_security_jwt_access_token(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_token(jwt_backend): + client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( @@ -161,7 +149,9 @@ def test_security_jwt_access_token(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_token_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_token_wrong(jwt_backend): + client, _ = create_example_client(jwt_backend) response = client.get( "/users/me", headers={"Authorization": "Bearer wrong_access_token"} ) @@ -175,7 +165,9 @@ def test_security_jwt_access_token_wrong(): assert response.json()["detail"].startswith("Wrong token:") -def test_security_jwt_access_token_changed(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_token_changed(jwt_backend): + client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] access_token = access_token.split(".")[0] + ".wrong." + access_token.split(".")[-1] @@ -187,28 +179,28 @@ def test_security_jwt_access_token_changed(): assert response.json()["detail"].startswith("Wrong token:") -def test_security_jwt_access_token_expiration(mocker: MockerFixture): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_token_expiration(mocker: MockerFixture, jwt_backend): + client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - mocker.patch("jose.jwt.datetime", _FakeDateTimeShort) # 3 min left - + mock_now_for_backend(mocker, jwt_backend, minutes=3) # 3 min left response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) assert response.status_code == 200, response.text - mocker.patch("jose.jwt.datetime", _FakeDateTimeLong) # 42 days left - + mock_now_for_backend(mocker, jwt_backend, days=42) # 42 days left response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith( - "Token time expired: Signature has expired" - ) + assert response.json()["detail"].startswith("Token time expired:") -def test_security_jwt_refresh_token(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token(jwt_backend): + client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] response = client.post( @@ -217,7 +209,9 @@ def test_security_jwt_refresh_token(): assert response.status_code == 200, response.text -def test_security_jwt_refresh_token_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token_wrong(jwt_backend): + client, _ = create_example_client(jwt_backend) response = client.post( "/refresh", headers={"Authorization": "Bearer wrong_refresh_token"} ) @@ -231,7 +225,9 @@ def test_security_jwt_refresh_token_wrong(): assert response.json()["detail"].startswith("Wrong token:") -def test_security_jwt_refresh_token_using_access_token(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token_using_access_token(jwt_backend): + client, _ = create_example_client(jwt_backend) tokens = client.post("/auth").json() access_token, refresh_token = tokens["access_token"], tokens["refresh_token"] assert access_token != refresh_token @@ -243,7 +239,9 @@ def test_security_jwt_refresh_token_using_access_token(): assert response.json()["detail"].startswith("Wrong token: 'type' is not 'refresh'") -def test_security_jwt_refresh_token_changed(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token_changed(jwt_backend): + client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] refresh_token = ( @@ -257,21 +255,22 @@ def test_security_jwt_refresh_token_changed(): assert response.json()["detail"].startswith("Wrong token:") -def test_security_jwt_refresh_token_expired(mocker: MockerFixture): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token_expired(mocker: MockerFixture, jwt_backend): + client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] - mocker.patch("jose.jwt.datetime", _FakeDateTimeLong) # 42 days left - + mock_now_for_backend(mocker, jwt_backend, days=42) # 42 days left response = client.post( "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} ) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith( - "Token time expired: Signature has expired" - ) + assert response.json()["detail"].startswith("Token time expired:") -def test_security_jwt_custom_jti(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_custom_jti(jwt_backend): + client, unique_identifiers_database = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( diff --git a/tests/test_security_jwt_general_optional.py b/tests/test_security_jwt_general_optional.py index ab32257..e0571d0 100644 --- a/tests/test_security_jwt_general_optional.py +++ b/tests/test_security_jwt_general_optional.py @@ -1,93 +1,77 @@ -import datetime from typing import Optional, Set from uuid import uuid4 +import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient from pytest_mock import MockerFixture from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend +from .mock_datetime_utils import mock_now_for_backend -app = FastAPI() -access_security = JwtAccessBearer(secret_key="secret_key", auto_error=False) -refresh_security = JwtRefreshBearer.from_other(access_security) +def create_example_client(jwt_backend): + define_default_jwt_backend(jwt_backend) + app = FastAPI() + access_security = JwtAccessBearer(secret_key="secret_key", auto_error=False) + refresh_security = JwtRefreshBearer.from_other(access_security) + unique_identifiers_database: Set[str] = set() -unique_identifiers_database: Set[str] = set() + @app.post("/auth") + def auth(): + subject = {"username": "username", "role": "user"} + unique_identifier = str(uuid4()) + unique_identifiers_database.add(unique_identifier) -@app.post("/auth") -def auth(): - subject = {"username": "username", "role": "user"} - unique_identifier = str(uuid4()) - unique_identifiers_database.add(unique_identifier) - - access_token = access_security.create_access_token( - subject=subject, unique_identifier=unique_identifier - ) - refresh_token = access_security.create_refresh_token(subject=subject) - - return {"access_token": access_token, "refresh_token": refresh_token} + access_token = access_security.create_access_token( + subject=subject, unique_identifier=unique_identifier + ) + refresh_token = access_security.create_refresh_token(subject=subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.post("/refresh") -def refresh( - credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), -): - if credentials is None: - return {"msg": "Create an account first"} - - unique_identifier = str(uuid4()) - unique_identifiers_database.add(unique_identifier) - - access_token = refresh_security.create_access_token( - subject=credentials.subject, unique_identifier=unique_identifier, - ) - refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) - return {"access_token": access_token, "refresh_token": refresh_token} + @app.post("/refresh") + def refresh( + credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), + ): + if credentials is None: + return {"msg": "Create an account first"} + unique_identifier = str(uuid4()) + unique_identifiers_database.add(unique_identifier) -@app.get("/users/me") -def read_current_user( - credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), -): - if credentials is None: - return {"msg": "Create an account first"} - return {"username": credentials["username"], "role": credentials["role"]} + access_token = refresh_security.create_access_token( + subject=credentials.subject, unique_identifier=unique_identifier, + ) + refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.get("/auth/meta") -def get_token_meta( - credentials: JwtAuthorizationCredentials = Security(access_security), -): - if credentials is None: - return {"msg": "Create an account first"} - return {"jti": credentials.jti} + @app.get("/users/me") + def read_current_user( + credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), + ): + if credentials is None: + return {"msg": "Create an account first"} + return {"username": credentials["username"], "role": credentials["role"]} -class _FakeDateTimeShort(datetime.datetime): # pragma: no cover - @staticmethod - def now(**kwargs): - return datetime.datetime.now() + datetime.timedelta(minutes=3) - @staticmethod - def utcnow(**kwargs): - return datetime.datetime.utcnow() + datetime.timedelta(minutes=3) + @app.get("/auth/meta") + def get_token_meta( + credentials: JwtAuthorizationCredentials = Security(access_security), + ): + if credentials is None: + return {"msg": "Create an account first"} + return {"jti": credentials.jti} -class _FakeDateTimeLong(datetime.datetime): # pragma: no cover - @staticmethod - def now(**kwargs): - return datetime.datetime.now() + datetime.timedelta(days=42) + return TestClient(app), unique_identifiers_database - @staticmethod - def utcnow(**kwargs): - return datetime.datetime.utcnow() + datetime.timedelta(days=42) - - -client = TestClient(app) openapi_schema = { "openapi": "3.1.0", @@ -154,13 +138,17 @@ def utcnow(**kwargs): } -def test_openapi_schema(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_openapi_schema(jwt_backend): + client, _ = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -def test_security_jwt_access_token(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_token(jwt_backend): + client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( @@ -170,7 +158,9 @@ def test_security_jwt_access_token(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_token_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_token_wrong(jwt_backend): + client, _ = create_example_client(jwt_backend) response = client.get( "/users/me", headers={"Authorization": "Bearer wrong_access_token"} ) @@ -184,7 +174,9 @@ def test_security_jwt_access_token_wrong(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_access_token_changed(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_token_changed(jwt_backend): + client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] access_token = access_token.split(".")[0] + ".wrong." + access_token.split(".")[-1] @@ -196,19 +188,19 @@ def test_security_jwt_access_token_changed(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_access_token_expiration(mocker: MockerFixture): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_token_expiration(mocker: MockerFixture, jwt_backend): + client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - mocker.patch("jose.jwt.datetime", _FakeDateTimeShort) # 3 min left - + mock_now_for_backend(mocker, jwt_backend, minutes=3) # 3 min left response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) assert response.status_code == 200, response.text assert response.json() == {"username": "username", "role": "user"} - mocker.patch("jose.jwt.datetime", _FakeDateTimeLong) # 42 days left - + mock_now_for_backend(mocker, jwt_backend, days=42) # 42 days left response = client.get( "/users/me", headers={"Authorization": f"Bearer {access_token}"} ) @@ -216,7 +208,9 @@ def test_security_jwt_access_token_expiration(mocker: MockerFixture): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_token(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token(jwt_backend): + client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] response = client.post( @@ -226,7 +220,9 @@ def test_security_jwt_refresh_token(): assert "msg" not in response.json() -def test_security_jwt_refresh_token_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token_wrong(jwt_backend): + client, _ = create_example_client(jwt_backend) response = client.post( "/refresh", headers={"Authorization": "Bearer wrong_refresh_token"} ) @@ -240,7 +236,9 @@ def test_security_jwt_refresh_token_wrong(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_token_using_access_token(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token_using_access_token(jwt_backend): + client, _ = create_example_client(jwt_backend) tokens = client.post("/auth").json() access_token, refresh_token = tokens["access_token"], tokens["refresh_token"] assert access_token != refresh_token @@ -252,7 +250,9 @@ def test_security_jwt_refresh_token_using_access_token(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_token_changed(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token_changed(jwt_backend): + client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] refresh_token = ( @@ -266,11 +266,12 @@ def test_security_jwt_refresh_token_changed(): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_refresh_token_expired(mocker: MockerFixture): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_token_expired(mocker: MockerFixture, jwt_backend): + client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] - mocker.patch("jose.jwt.datetime", _FakeDateTimeLong) # 42 days left - + mock_now_for_backend(mocker, jwt_backend, days=42) # 42 days left response = client.post( "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} ) @@ -278,7 +279,9 @@ def test_security_jwt_refresh_token_expired(mocker: MockerFixture): assert response.json() == {"msg": "Create an account first"} -def test_security_jwt_custom_jti(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_custom_jti(jwt_backend): + client, unique_identifiers_database = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( diff --git a/tests/test_security_jwt_multiple_places.py b/tests/test_security_jwt_multiple_places.py index af60bb8..6a91cf7 100644 --- a/tests/test_security_jwt_multiple_places.py +++ b/tests/test_security_jwt_multiple_places.py @@ -1,40 +1,46 @@ +import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient from fastapi_jwt import JwtAccessBearerCookie, JwtAuthorizationCredentials, JwtRefreshBearerCookie +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend -app = FastAPI() -access_security = JwtAccessBearerCookie(secret_key="secret_key") -refresh_security = JwtRefreshBearerCookie(secret_key="secret_key") +def create_example_client(jwt_backend): + define_default_jwt_backend(jwt_backend) + app = FastAPI() + access_security = JwtAccessBearerCookie(secret_key="secret_key") + refresh_security = JwtRefreshBearerCookie(secret_key="secret_key") -@app.post("/auth") -def auth(): - subject = {"username": "username", "role": "user"} - access_token = access_security.create_access_token(subject=subject) - refresh_token = access_security.create_refresh_token(subject=subject) + @app.post("/auth") + def auth(): + subject = {"username": "username", "role": "user"} - return {"access_token": access_token, "refresh_token": refresh_token} + access_token = access_security.create_access_token(subject=subject) + refresh_token = access_security.create_refresh_token(subject=subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.post("/refresh") -def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): - access_token = refresh_security.create_access_token(subject=credentials.subject) - refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) - return {"access_token": access_token, "refresh_token": refresh_token} + @app.post("/refresh") + def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): + access_token = refresh_security.create_access_token(subject=credentials.subject) + refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.get("/users/me") -def read_current_user( - credentials: JwtAuthorizationCredentials = Security(access_security), -): - return {"username": credentials["username"], "role": credentials["role"]} + @app.get("/users/me") + def read_current_user( + credentials: JwtAuthorizationCredentials = Security(access_security), + ): + return {"username": credentials["username"], "role": credentials["role"]} + + + return TestClient(app) -client = TestClient(app) openapi_schema = { "openapi": "3.1.0", @@ -98,13 +104,17 @@ def read_current_user( } -def test_openapi_schema(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_openapi_schema(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -def test_security_jwt_access_both_correct(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_both_correct(jwt_backend): + client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( @@ -116,7 +126,9 @@ def test_security_jwt_access_both_correct(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_only_cookie(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_only_cookie(jwt_backend): + client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get("/users/me", cookies={"access_token_cookie": access_token}) @@ -124,7 +136,9 @@ def test_security_jwt_access_only_cookie(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_only_bearer(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_only_bearer(jwt_backend): + client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( @@ -134,7 +148,9 @@ def test_security_jwt_access_only_bearer(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_bearer_wrong_cookie_correct(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer_wrong_cookie_correct(jwt_backend): + client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( @@ -146,7 +162,9 @@ def test_security_jwt_access_bearer_wrong_cookie_correct(): assert response.json()["detail"].startswith("Wrong token:") -def test_security_jwt_access_bearer_correct_cookie_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_bearer_correct_cookie_wrong(jwt_backend): + client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] response = client.get( @@ -158,20 +176,26 @@ def test_security_jwt_access_bearer_correct_cookie_wrong(): assert response.json() == {"username": "username", "role": "user"} -def test_security_jwt_access_both_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_access_both_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/users/me") assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} -def test_security_jwt_refresh_only_cookie(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_only_cookie(jwt_backend): + client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] response = client.post("/refresh", cookies={"refresh_token_cookie": refresh_token}) assert response.status_code == 200, response.text -def test_security_jwt_refresh_bearer_correct_cookie_wrong(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer_correct_cookie_wrong(jwt_backend): + client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] response = client.post( @@ -182,7 +206,9 @@ def test_security_jwt_refresh_bearer_correct_cookie_wrong(): assert response.status_code == 200, response.text -def test_security_jwt_refresh_bearer_wrong_cookie_correct(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_bearer_wrong_cookie_correct(jwt_backend): + client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] response = client.post( @@ -194,7 +220,9 @@ def test_security_jwt_refresh_bearer_wrong_cookie_correct(): assert response.json()["detail"].startswith("Wrong token:") -def test_security_jwt_refresh_cookie_wrong_using_access_token(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_cookie_wrong_using_access_token(jwt_backend): + client = create_example_client(jwt_backend) tokens = client.post("/auth").json() access_token, refresh_token = tokens["access_token"], tokens["refresh_token"] assert access_token != refresh_token @@ -204,7 +232,9 @@ def test_security_jwt_refresh_cookie_wrong_using_access_token(): assert response.json()["detail"].startswith("Wrong token: 'type' is not 'refresh'") -def test_security_jwt_refresh_both_no_credentials(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_refresh_both_no_credentials(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/refresh") assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} diff --git a/tests/test_security_jwt_set_cookie.py b/tests/test_security_jwt_set_cookie.py index 6057cbc..d2fd32e 100644 --- a/tests/test_security_jwt_set_cookie.py +++ b/tests/test_security_jwt_set_cookie.py @@ -1,36 +1,42 @@ +import pytest from fastapi import FastAPI, Response from fastapi.testclient import TestClient from fastapi_jwt import JwtAccessCookie, JwtRefreshCookie +from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend -app = FastAPI() -access_security = JwtAccessCookie(secret_key="secret_key") -refresh_security = JwtRefreshCookie(secret_key="secret_key") +def create_example_client(jwt_backend): + define_default_jwt_backend(jwt_backend) + app = FastAPI() + access_security = JwtAccessCookie(secret_key="secret_key") + refresh_security = JwtRefreshCookie(secret_key="secret_key") -@app.post("/auth") -def auth(response: Response): - subject = {"username": "username", "role": "user"} - access_token = access_security.create_access_token(subject=subject) - refresh_token = access_security.create_refresh_token(subject=subject) + @app.post("/auth") + def auth(response: Response): + subject = {"username": "username", "role": "user"} - access_security.set_access_cookie(response, access_token) - refresh_security.set_refresh_cookie(response, refresh_token) + access_token = access_security.create_access_token(subject=subject) + refresh_token = access_security.create_refresh_token(subject=subject) - return {"access_token": access_token, "refresh_token": refresh_token} + access_security.set_access_cookie(response, access_token) + refresh_security.set_refresh_cookie(response, refresh_token) + return {"access_token": access_token, "refresh_token": refresh_token} -@app.delete("/auth") -def logout(response: Response): - access_security.unset_access_cookie(response) - refresh_security.unset_refresh_cookie(response) - return {"msg": "Successful logout"} + @app.delete("/auth") + def logout(response: Response): + access_security.unset_access_cookie(response) + refresh_security.unset_refresh_cookie(response) + return {"msg": "Successful logout"} + + + return TestClient(app) -client = TestClient(app) openapi_schema = { "openapi": "3.1.0", @@ -62,13 +68,17 @@ def logout(response: Response): } -def test_openapi_schema(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_openapi_schema(jwt_backend): + client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -def test_security_jwt_auth(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_auth(jwt_backend): + client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text @@ -78,7 +88,9 @@ def test_security_jwt_auth(): assert response.cookies["refresh_token_cookie"] == response.json()["refresh_token"] -def test_security_jwt_logout(): +@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) +def test_security_jwt_logout(jwt_backend): + client = create_example_client(jwt_backend) response = client.delete("/auth") assert response.status_code == 200, response.text From ea60e91dd80ae5bc35e835390affbb39539d037d Mon Sep 17 00:00:00 2001 From: Konstantin Chernyshev Date: Mon, 6 May 2024 12:51:31 +0200 Subject: [PATCH 2/6] style: multiple style fixes including typings --- fastapi_jwt/jwt.py | 76 ++++++----------- fastapi_jwt/jwt_backends/__init__.py | 6 +- fastapi_jwt/jwt_backends/abstract_backend.py | 28 ++++--- fastapi_jwt/jwt_backends/authlib_backend.py | 52 ++++++------ .../jwt_backends/python_jose_backend.py | 35 ++++---- pyproject.toml | 3 +- tests/mock_datetime_utils.py | 2 - tests/test_security_jwt_bearer.py | 57 ++++++------- tests/test_security_jwt_bearer_optional.py | 37 ++++---- tests/test_security_jwt_cookie.py | 29 ++++--- tests/test_security_jwt_cookie_optional.py | 27 +++--- tests/test_security_jwt_general.py | 82 +++++++----------- tests/test_security_jwt_general_optional.py | 84 +++++++------------ tests/test_security_jwt_multiple_places.py | 25 +++--- tests/test_security_jwt_set_cookie.py | 20 ++--- 15 files changed, 245 insertions(+), 318 deletions(-) diff --git a/fastapi_jwt/jwt.py b/fastapi_jwt/jwt.py index a1b32e6..b86db07 100644 --- a/fastapi_jwt/jwt.py +++ b/fastapi_jwt/jwt.py @@ -1,6 +1,6 @@ from abc import ABC from datetime import datetime, timedelta -from typing import Any, Dict, Optional, Set +from typing import Any, Dict, Optional, Set, Type from uuid import uuid4 from fastapi.exceptions import HTTPException @@ -8,24 +8,26 @@ from fastapi.responses import Response from fastapi.security import APIKeyCookie, HTTPBearer from starlette.status import HTTP_401_UNAUTHORIZED -from .jwt_backends import AuthlibJWTBackend, PythonJoseJWTBackend +from .jwt_backends import AbstractJWTBackend, AuthlibJWTBackend, PythonJoseJWTBackend -DEFAULT_JWT_BACKEND = None +DEFAULT_JWT_BACKEND: Optional[Type[AbstractJWTBackend]] = None -def define_default_jwt_backend(cls): +def define_default_jwt_backend(cls: Type[AbstractJWTBackend]) -> None: global DEFAULT_JWT_BACKEND DEFAULT_JWT_BACKEND = cls if AuthlibJWTBackend is not None: - define_default_jwt_backend(AuthlibJWTBackend) + DEFAULT_JWT_BACKEND = AuthlibJWTBackend elif PythonJoseJWTBackend is not None: - define_default_jwt_backend(PythonJoseJWTBackend) + DEFAULT_JWT_BACKEND = PythonJoseJWTBackend +else: # pragma: nocover + raise ImportError("No JWT backend found, please install 'python-jose' or 'authlib'") -def utcnow(): +def utcnow() -> datetime: try: from datetime import UTC except ImportError: # pragma: nocover @@ -60,15 +62,11 @@ def __getitem__(self, item: str) -> Any: class JwtAuthBase(ABC): class JwtAccessCookie(APIKeyCookie): def __init__(self, *args: Any, **kwargs: Any): - APIKeyCookie.__init__( - self, *args, name="access_token_cookie", auto_error=False, **kwargs - ) + APIKeyCookie.__init__(self, *args, name="access_token_cookie", auto_error=False, **kwargs) class JwtRefreshCookie(APIKeyCookie): def __init__(self, *args: Any, **kwargs: Any): - APIKeyCookie.__init__( - self, *args, name="refresh_token_cookie", auto_error=False, **kwargs - ) + APIKeyCookie.__init__(self, *args, name="refresh_token_cookie", auto_error=False, **kwargs) class JwtAccessBearer(HTTPBearer): def __init__(self, *args: Any, **kwargs: Any): @@ -87,12 +85,12 @@ def __init__( access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, ): + assert DEFAULT_JWT_BACKEND is not None, "No JWT backend found, please install 'python-jose' or 'authlib'" + self.jwt_backend = DEFAULT_JWT_BACKEND(algorithm) self.secret_key = secret_key if places: - assert places.issubset( - {"header", "cookie"} - ), "only 'header'/'cookie' are supported" + assert places.issubset({"header", "cookie"}), "only 'header'/'cookie' are supported" self.places = places or {"header"} self.auto_error = auto_error @@ -100,19 +98,19 @@ def __init__( self.refresh_expires_delta = refresh_expires_delta or timedelta(days=31) @property - def algorithm(self): + def algorithm(self) -> str: return self.jwt_backend.algorithm @classmethod def from_other( cls, - other: 'JwtAuthBase', + other: "JwtAuthBase", secret_key: Optional[str] = None, auto_error: Optional[bool] = None, algorithm: Optional[str] = None, access_expires_delta: Optional[timedelta] = None, refresh_expires_delta: Optional[timedelta] = None, - ) -> 'JwtAuthBase': + ) -> "JwtAuthBase": return cls( secret_key=secret_key or other.secret_key, auto_error=auto_error or other.auto_error, @@ -149,9 +147,7 @@ async def _get_payload( # Check token exist if not token: if self.auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, detail="Credentials are not provided" - ) + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail="Credentials are not provided") else: return None @@ -166,9 +162,7 @@ def create_access_token( ) -> str: expires_delta = expires_delta or self.access_expires_delta unique_identifier = unique_identifier or str(uuid4()) - to_encode = self._generate_payload( - subject, expires_delta, unique_identifier, "access" - ) + to_encode = self._generate_payload(subject, expires_delta, unique_identifier, "access") return self.jwt_backend.encode(to_encode, self.secret_key) def create_refresh_token( @@ -179,18 +173,12 @@ def create_refresh_token( ) -> str: expires_delta = expires_delta or self.refresh_expires_delta unique_identifier = unique_identifier or str(uuid4()) - to_encode = self._generate_payload( - subject, expires_delta, unique_identifier, "refresh" - ) + to_encode = self._generate_payload(subject, expires_delta, unique_identifier, "refresh") return self.jwt_backend.encode(to_encode, self.secret_key) @staticmethod - def set_access_cookie( - response: Response, access_token: str, expires_delta: Optional[timedelta] = None - ) -> None: - seconds_expires: Optional[int] = ( - int(expires_delta.total_seconds()) if expires_delta else None - ) + def set_access_cookie(response: Response, access_token: str, expires_delta: Optional[timedelta] = None) -> None: + seconds_expires: Optional[int] = int(expires_delta.total_seconds()) if expires_delta else None response.set_cookie( key="access_token_cookie", value=access_token, @@ -204,9 +192,7 @@ def set_refresh_cookie( refresh_token: str, expires_delta: Optional[timedelta] = None, ) -> None: - seconds_expires: Optional[int] = ( - int(expires_delta.total_seconds()) if expires_delta else None - ) + seconds_expires: Optional[int] = int(expires_delta.total_seconds()) if expires_delta else None response.set_cookie( key="refresh_token_cookie", value=refresh_token, @@ -216,15 +202,11 @@ def set_refresh_cookie( @staticmethod def unset_access_cookie(response: Response) -> None: - response.set_cookie( - key="access_token_cookie", value="", httponly=False, max_age=-1 - ) + response.set_cookie(key="access_token_cookie", value="", httponly=False, max_age=-1) @staticmethod def unset_refresh_cookie(response: Response) -> None: - response.set_cookie( - key="refresh_token_cookie", value="", httponly=True, max_age=-1 - ) + response.set_cookie(key="refresh_token_cookie", value="", httponly=True, max_age=-1) class JwtAccess(JwtAuthBase): @@ -257,9 +239,7 @@ async def _get_credentials( payload = await self._get_payload(bearer, cookie) if payload: - return JwtAuthorizationCredentials( - payload["subject"], payload.get("jti", None) - ) + return JwtAuthorizationCredentials(payload["subject"], payload.get("jti", None)) return None @@ -379,9 +359,7 @@ async def _get_credentials( else: return None - return JwtAuthorizationCredentials( - payload["subject"], payload.get("jti", None) - ) + return JwtAuthorizationCredentials(payload["subject"], payload.get("jti", None)) class JwtRefreshBearer(JwtRefresh): diff --git a/fastapi_jwt/jwt_backends/__init__.py b/fastapi_jwt/jwt_backends/__init__.py index 310f02c..251c72f 100644 --- a/fastapi_jwt/jwt_backends/__init__.py +++ b/fastapi_jwt/jwt_backends/__init__.py @@ -1,9 +1,11 @@ try: from .authlib_backend import AuthlibJWTBackend except ImportError: - AuthlibJWTBackend = None + AuthlibJWTBackend = None # type: ignore try: from .python_jose_backend import PythonJoseJWTBackend except ImportError: - PythonJoseJWTBackend = None + PythonJoseJWTBackend = None # type: ignore + +from .abstract_backend import AbstractJWTBackend # noqa: F401 diff --git a/fastapi_jwt/jwt_backends/abstract_backend.py b/fastapi_jwt/jwt_backends/abstract_backend.py index ca337ae..3d11a30 100644 --- a/fastapi_jwt/jwt_backends/abstract_backend.py +++ b/fastapi_jwt/jwt_backends/abstract_backend.py @@ -2,30 +2,34 @@ from typing import Any, Dict, Optional, Self - class AbstractJWTBackend(metaclass=ABCMeta): - # simple "SingletonArgs" implementation to keep a JWTBackend per algorithm - _instances = {} + _instances: Dict[Any, "AbstractJWTBackend"] = {} - def __new__(cls, algorithm) -> Self: + def __new__(cls, algorithm: Optional[str]) -> "AbstractJWTBackend": instance_key = (cls, algorithm) if instance_key not in cls._instances: cls._instances[instance_key] = super(AbstractJWTBackend, cls).__new__(cls) return cls._instances[instance_key] @abstractmethod - def __init__(self, algorithm) -> None: - pass + def __init__(self, algorithm: Optional[str]) -> None: + raise NotImplementedError - @abstractproperty + @property + @abstractmethod def default_algorithm(self) -> str: - pass + raise NotImplementedError + + @property + @abstractmethod + def algorithm(self) -> str: + raise NotImplementedError @abstractmethod - def encode(self, to_encode, secret_key) -> str: - pass + def encode(self, to_encode: Dict[str, Any], secret_key: str) -> str: + raise NotImplementedError @abstractmethod - def decode(self, token, secret_key, auto_error) -> Optional[Dict[str, Any]]: - pass + def decode(self, token: str, secret_key: str, auto_error: bool) -> Optional[Dict[str, Any]]: + raise NotImplementedError diff --git a/fastapi_jwt/jwt_backends/authlib_backend.py b/fastapi_jwt/jwt_backends/authlib_backend.py index 1dfe933..81be16f 100644 --- a/fastapi_jwt/jwt_backends/authlib_backend.py +++ b/fastapi_jwt/jwt_backends/authlib_backend.py @@ -1,51 +1,55 @@ -from fastapi import HTTPException from typing import Any, Dict, Optional + +from fastapi import HTTPException from starlette.status import HTTP_401_UNAUTHORIZED -from authlib.jose import JsonWebSignature, JsonWebToken -from authlib.jose.errors import ( - DecodeError, ExpiredTokenError, InvalidClaimError, InvalidTokenError -) +try: + import authlib.jose as authlib_jose + import authlib.jose.errors as authlib_jose_errors +except ImportError: + authlib_jose = None + from .abstract_backend import AbstractJWTBackend class AuthlibJWTBackend(AbstractJWTBackend): + def __init__(self, algorithm: Optional[str] = None) -> None: + assert authlib_jose is not None, "To use AuthlibJWTBackend, you need to install authlib" - def __init__(self, algorithm) -> None: - self.algorithm = algorithm if algorithm is not None else self.default_algorithm + self._algorithm = algorithm or self.default_algorithm # from https://github.com/lepture/authlib/blob/85f9ff/authlib/jose/__init__.py#L45 - valid_algorithms = JsonWebSignature.ALGORITHMS_REGISTRY.keys() - assert ( - self.algorithm in valid_algorithms - ), f"{self.algorithm} algorithm is not supported by authlib" - self.jwt = JsonWebToken(algorithms=[self.algorithm]) + valid_algorithms = authlib_jose.JsonWebSignature.ALGORITHMS_REGISTRY.keys() + assert self._algorithm in valid_algorithms, f"{self._algorithm} algorithm is not supported by authlib" + self.jwt = authlib_jose.JsonWebToken(algorithms=[self._algorithm]) @property def default_algorithm(self) -> str: return "HS256" - def encode(self, to_encode, secret_key) -> str: + @property + def algorithm(self) -> str: + return self._algorithm + + def encode(self, to_encode: Dict[str, Any], secret_key: str) -> str: token = self.jwt.encode(header={"alg": self.algorithm}, payload=to_encode, key=secret_key) return token.decode() # convert to string - def decode(self, token, secret_key, auto_error) -> Optional[Dict[str, Any]]: + def decode(self, token: str, secret_key: str, auto_error: bool) -> Optional[Dict[str, Any]]: try: payload = self.jwt.decode(token, secret_key) payload.validate(leeway=10) return dict(payload) - except ExpiredTokenError as e: # type: ignore[attr-defined] + except authlib_jose_errors.ExpiredTokenError as e: if auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}" - ) + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}") else: return None - except (InvalidClaimError, - InvalidTokenError, - DecodeError) as e: # type: ignore[attr-defined] + except ( + authlib_jose_errors.InvalidClaimError, + authlib_jose_errors.InvalidTokenError, + authlib_jose_errors.DecodeError, + ) as e: if auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}" - ) + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}") else: return None diff --git a/fastapi_jwt/jwt_backends/python_jose_backend.py b/fastapi_jwt/jwt_backends/python_jose_backend.py index 521cbb7..de8bfd3 100644 --- a/fastapi_jwt/jwt_backends/python_jose_backend.py +++ b/fastapi_jwt/jwt_backends/python_jose_backend.py @@ -1,47 +1,52 @@ -from fastapi import HTTPException from typing import Any, Dict, Optional + +from fastapi import HTTPException from starlette.status import HTTP_401_UNAUTHORIZED -from jose import jwt +try: + from jose import jwt +except ImportError: + jwt = None # type: ignore from .abstract_backend import AbstractJWTBackend class PythonJoseJWTBackend(AbstractJWTBackend): + def __init__(self, algorithm: Optional[str] = None) -> None: + assert jwt is not None, "To use PythonJoseJWTBackend, you need to install python-jose" - def __init__(self, algorithm) -> None: - self.algorithm = algorithm if algorithm is not None else self.default_algorithm + self._algorithm = algorithm or self.default_algorithm assert ( - hasattr(jwt.ALGORITHMS, self.algorithm) is True # type: ignore[attr-defined] + hasattr(jwt.ALGORITHMS, self._algorithm) is True # type: ignore[attr-defined] ), f"{algorithm} algorithm is not supported by python-jose library" @property def default_algorithm(self) -> str: return jwt.ALGORITHMS.HS256 - def encode(self, to_encode, secret_key) -> str: - return jwt.encode(to_encode, secret_key, algorithm=self.algorithm) + @property + def algorithm(self) -> str: + return self._algorithm + + def encode(self, to_encode: Dict[str, Any], secret_key: str) -> str: + return jwt.encode(to_encode, secret_key, algorithm=self._algorithm) - def decode(self, token, secret_key, auto_error) -> Optional[Dict[str, Any]]: + def decode(self, token: str, secret_key: str, auto_error: bool) -> Optional[Dict[str, Any]]: try: payload: Dict[str, Any] = jwt.decode( token, secret_key, - algorithms=[self.algorithm], + algorithms=[self._algorithm], options={"leeway": 10}, ) return payload except jwt.ExpiredSignatureError as e: # type: ignore[attr-defined] if auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}" - ) + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}") else: return None except jwt.JWTError as e: # type: ignore[attr-defined] if auto_error: - raise HTTPException( - status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}" - ) + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}") else: return None diff --git a/pyproject.toml b/pyproject.toml index 03c008a..b754a8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -71,8 +71,7 @@ docs = [ [tool.setuptools.dynamic] version = {file = "VERSION"} - -[mypy] +[tool.mypy] ignore_missing_imports = true no_incremental = true disallow_untyped_defs = true diff --git a/tests/mock_datetime_utils.py b/tests/mock_datetime_utils.py index 9c720e7..a35e576 100644 --- a/tests/mock_datetime_utils.py +++ b/tests/mock_datetime_utils.py @@ -3,14 +3,12 @@ from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend - _time = time.time _now = datetime.datetime.now _utcnow = datetime.datetime.utcnow def create_datetime_mock(**timedelta_kwargs): - class _FakeDateTime(datetime.datetime): # pragma: no cover @staticmethod def now(**kwargs): diff --git a/tests/test_security_jwt_bearer.py b/tests/test_security_jwt_bearer.py index 1098a48..3d73417 100644 --- a/tests/test_security_jwt_bearer.py +++ b/tests/test_security_jwt_bearer.py @@ -2,18 +2,24 @@ from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer -from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend - - -def create_example_client(jwt_backend): +from fastapi_jwt import ( + AuthlibJWTBackend, + JwtAccessBearer, + JwtAuthorizationCredentials, + JwtRefreshBearer, + PythonJoseJWTBackend, + define_default_jwt_backend, +) +from fastapi_jwt.jwt_backends import AbstractJWTBackend + + +def create_example_client(jwt_backend: AbstractJWTBackend): define_default_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessBearer(secret_key="secret_key") refresh_security = JwtRefreshBearer(secret_key="secret_key") - @app.post("/auth") def auth(): subject = {"username": "username", "role": "user"} @@ -23,7 +29,6 @@ def auth(): return {"access_token": access_token, "refresh_token": refresh_token} - @app.post("/refresh") def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): access_token = refresh_security.create_access_token(subject=credentials.subject) @@ -31,14 +36,12 @@ def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security return {"access_token": access_token, "refresh_token": refresh_token} - @app.get("/users/me") def read_current_user( credentials: JwtAuthorizationCredentials = Security(access_security), ): return {"username": credentials["username"], "role": credentials["role"]} - return TestClient(app) @@ -95,7 +98,7 @@ def read_current_user( @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend): +def test_openapi_schema(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text @@ -103,35 +106,31 @@ def test_openapi_schema(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_auth(jwt_backend): +def test_security_jwt_auth(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer(jwt_backend): +def test_security_jwt_access_bearer(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"username": "username", "role": "user"} @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_wrong(jwt_backend): +def test_security_jwt_access_bearer_wrong(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) - response = client.get( - "/users/me", headers={"Authorization": "Bearer wrong_access_token"} - ) + response = client.get("/users/me", headers={"Authorization": "Bearer wrong_access_token"}) assert response.status_code == 401, response.text @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_no_credentials(jwt_backend): +def test_security_jwt_access_bearer_no_credentials(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) response = client.get("/users/me") assert response.status_code == 401, response.text @@ -139,7 +138,7 @@ def test_security_jwt_access_bearer_no_credentials(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend): +def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 401, response.text @@ -148,27 +147,23 @@ def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer(jwt_backend): +def test_security_jwt_refresh_bearer(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 200, response.text @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_wrong(jwt_backend): +def test_security_jwt_refresh_bearer_wrong(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) - response = client.post( - "/refresh", headers={"Authorization": "Bearer wrong_refresh_token"} - ) + response = client.post("/refresh", headers={"Authorization": "Bearer wrong_refresh_token"}) assert response.status_code == 401, response.text @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_no_credentials(jwt_backend): +def test_security_jwt_refresh_bearer_no_credentials(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) response = client.post("/refresh") assert response.status_code == 401, response.text @@ -176,7 +171,7 @@ def test_security_jwt_refresh_bearer_no_credentials(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(jwt_backend): +def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(jwt_backend: AbstractJWTBackend): client = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Basic notreally"}) assert response.status_code == 401, response.text diff --git a/tests/test_security_jwt_bearer_optional.py b/tests/test_security_jwt_bearer_optional.py index a1aae21..9f29a6f 100644 --- a/tests/test_security_jwt_bearer_optional.py +++ b/tests/test_security_jwt_bearer_optional.py @@ -4,18 +4,24 @@ from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer -from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend - - -def create_example_client(jwt_backend): +from fastapi_jwt import ( + AuthlibJWTBackend, + JwtAccessBearer, + JwtAuthorizationCredentials, + JwtRefreshBearer, + PythonJoseJWTBackend, + define_default_jwt_backend, +) +from fastapi_jwt.jwt_backends import AbstractJWTBackend + + +def create_example_client(jwt_backend: AbstractJWTBackend): define_default_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessBearer(secret_key="secret_key", auto_error=False) refresh_security = JwtRefreshBearer(secret_key="secret_key", auto_error=False) - @app.post("/auth") def auth(): subject = {"username": "username", "role": "user"} @@ -25,7 +31,6 @@ def auth(): return {"access_token": access_token, "refresh_token": refresh_token} - @app.post("/refresh") def refresh( credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), @@ -38,7 +43,6 @@ def refresh( return {"access_token": access_token, "refresh_token": refresh_token} - @app.get("/users/me") def read_current_user( credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), @@ -47,7 +51,6 @@ def read_current_user( return {"msg": "Create an account first"} return {"username": credentials["username"], "role": credentials["role"]} - return TestClient(app) @@ -123,9 +126,7 @@ def test_security_jwt_access_bearer(jwt_backend): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"username": "username", "role": "user"} @@ -133,9 +134,7 @@ def test_security_jwt_access_bearer(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_access_bearer_wrong(jwt_backend): client = create_example_client(jwt_backend) - response = client.get( - "/users/me", headers={"Authorization": "Bearer wrong_access_token"} - ) + response = client.get("/users/me", headers={"Authorization": "Bearer wrong_access_token"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -161,18 +160,14 @@ def test_security_jwt_refresh_bearer(jwt_backend): client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 200, response.text @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_refresh_bearer_wrong(jwt_backend): client = create_example_client(jwt_backend) - response = client.post( - "/refresh", headers={"Authorization": "Bearer wrong_refresh_token"} - ) + response = client.post("/refresh", headers={"Authorization": "Bearer wrong_refresh_token"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_jwt_cookie.py b/tests/test_security_jwt_cookie.py index 8507732..4e9b2e5 100644 --- a/tests/test_security_jwt_cookie.py +++ b/tests/test_security_jwt_cookie.py @@ -2,18 +2,24 @@ from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import JwtAccessCookie, JwtAuthorizationCredentials, JwtRefreshCookie -from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend - - -def create_example_client(jwt_backend): +from fastapi_jwt import ( + AuthlibJWTBackend, + JwtAccessCookie, + JwtAuthorizationCredentials, + JwtRefreshCookie, + PythonJoseJWTBackend, + define_default_jwt_backend, +) +from fastapi_jwt.jwt_backends import AbstractJWTBackend + + +def create_example_client(jwt_backend: AbstractJWTBackend): define_default_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessCookie(secret_key="secret_key") refresh_security = JwtRefreshCookie(secret_key="secret_key") - @app.post("/auth") def auth(): subject = {"username": "username", "role": "user"} @@ -23,7 +29,6 @@ def auth(): return {"access_token": access_token, "refresh_token": refresh_token} - @app.post("/refresh") def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): access_token = refresh_security.create_access_token(subject=credentials.subject) @@ -31,14 +36,12 @@ def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security return {"access_token": access_token, "refresh_token": refresh_token} - @app.get("/users/me") def read_current_user( credentials: JwtAuthorizationCredentials = Security(access_security), ): return {"username": credentials["username"], "role": credentials["role"]} - return TestClient(app) @@ -130,9 +133,7 @@ def test_security_jwt_access_cookie(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_access_cookie_wrong(jwt_backend): client = create_example_client(jwt_backend) - response = client.get( - "/users/me", cookies={"access_token_cookie": "wrong_access_token_cookie"} - ) + response = client.get("/users/me", cookies={"access_token_cookie": "wrong_access_token_cookie"}) assert response.status_code == 401, response.text @@ -158,9 +159,7 @@ def test_security_jwt_refresh_cookie(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_refresh_cookie_wrong(jwt_backend): client = create_example_client(jwt_backend) - response = client.post( - "/refresh", cookies={"refresh_token_cookie": "wrong_refresh_token_cookie"} - ) + response = client.post("/refresh", cookies={"refresh_token_cookie": "wrong_refresh_token_cookie"}) assert response.status_code == 401, response.text diff --git a/tests/test_security_jwt_cookie_optional.py b/tests/test_security_jwt_cookie_optional.py index 1af185e..6a98720 100644 --- a/tests/test_security_jwt_cookie_optional.py +++ b/tests/test_security_jwt_cookie_optional.py @@ -1,21 +1,27 @@ -import pytest from typing import Optional +import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import JwtAccessCookie, JwtAuthorizationCredentials, JwtRefreshCookie -from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend +from fastapi_jwt import ( + AuthlibJWTBackend, + JwtAccessCookie, + JwtAuthorizationCredentials, + JwtRefreshCookie, + PythonJoseJWTBackend, + define_default_jwt_backend, +) +from fastapi_jwt.jwt_backends import AbstractJWTBackend -def create_example_client(jwt_backend): +def create_example_client(jwt_backend: AbstractJWTBackend): define_default_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessCookie(secret_key="secret_key", auto_error=False) refresh_security = JwtRefreshCookie(secret_key="secret_key", auto_error=False) - @app.post("/auth") def auth(): subject = {"username": "username", "role": "user"} @@ -25,7 +31,6 @@ def auth(): return {"access_token": access_token, "refresh_token": refresh_token} - @app.post("/refresh") def refresh( credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), @@ -38,7 +43,6 @@ def refresh( return {"access_token": access_token, "refresh_token": refresh_token} - @app.get("/users/me") def read_current_user( credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), @@ -48,7 +52,6 @@ def read_current_user( return {"username": credentials["username"], "role": credentials["role"]} - return TestClient(app) @@ -141,9 +144,7 @@ def test_security_jwt_access_cookie(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_access_cookie_wrong(jwt_backend): client = create_example_client(jwt_backend) - response = client.get( - "/users/me", cookies={"access_token_cookie": "wrong_access_token_cookie"} - ) + response = client.get("/users/me", cookies={"access_token_cookie": "wrong_access_token_cookie"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -168,9 +169,7 @@ def test_security_jwt_refresh_cookie(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_refresh_cookie_wrong(jwt_backend): client = create_example_client(jwt_backend) - response = client.post( - "/refresh", cookies={"refresh_token_cookie": "wrong_refresh_token_cookie"} - ) + response = client.post("/refresh", cookies={"refresh_token_cookie": "wrong_refresh_token_cookie"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} diff --git a/tests/test_security_jwt_general.py b/tests/test_security_jwt_general.py index 59566b8..c30f4b6 100644 --- a/tests/test_security_jwt_general.py +++ b/tests/test_security_jwt_general.py @@ -6,12 +6,20 @@ from fastapi.testclient import TestClient from pytest_mock import MockerFixture -from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer -from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend +from fastapi_jwt import ( + AuthlibJWTBackend, + JwtAccessBearer, + JwtAuthorizationCredentials, + JwtRefreshBearer, + PythonJoseJWTBackend, + define_default_jwt_backend, +) +from fastapi_jwt.jwt_backends import AbstractJWTBackend + from .mock_datetime_utils import mock_now_for_backend -def create_example_client(jwt_backend): +def create_example_client(jwt_backend: AbstractJWTBackend): define_default_jwt_backend(jwt_backend) app = FastAPI() @@ -19,48 +27,42 @@ def create_example_client(jwt_backend): refresh_security = JwtRefreshBearer.from_other(access_security) unique_identifiers_database: Set[str] = set() - @app.post("/auth") def auth(): subject = {"username": "username", "role": "user"} unique_identifier = str(uuid4()) unique_identifiers_database.add(unique_identifier) - access_token = access_security.create_access_token( - subject=subject, unique_identifier=unique_identifier - ) + access_token = access_security.create_access_token(subject=subject, unique_identifier=unique_identifier) refresh_token = access_security.create_refresh_token(subject=subject) return {"access_token": access_token, "refresh_token": refresh_token} - @app.post("/refresh") def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): unique_identifier = str(uuid4()) unique_identifiers_database.add(unique_identifier) access_token = refresh_security.create_access_token( - subject=credentials.subject, unique_identifier=unique_identifier, + subject=credentials.subject, + unique_identifier=unique_identifier, ) refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) return {"access_token": access_token, "refresh_token": refresh_token} - @app.get("/users/me") def read_current_user( credentials: JwtAuthorizationCredentials = Security(access_security), ): return {"username": credentials["username"], "role": credentials["role"]} - @app.get("/auth/meta") def get_token_meta( credentials: JwtAuthorizationCredentials = Security(access_security), ): return {"jti": credentials.jti} - return TestClient(app), unique_identifiers_database @@ -142,9 +144,7 @@ def test_security_jwt_access_token(jwt_backend): client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"username": "username", "role": "user"} @@ -152,15 +152,11 @@ def test_security_jwt_access_token(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_access_token_wrong(jwt_backend): client, _ = create_example_client(jwt_backend) - response = client.get( - "/users/me", headers={"Authorization": "Bearer wrong_access_token"} - ) + response = client.get("/users/me", headers={"Authorization": "Bearer wrong_access_token"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Wrong token:") - response = client.get( - "/users/me", headers={"Authorization": "Bearer wrong.access.token"} - ) + response = client.get("/users/me", headers={"Authorization": "Bearer wrong.access.token"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Wrong token:") @@ -172,9 +168,7 @@ def test_security_jwt_access_token_changed(jwt_backend): access_token = access_token.split(".")[0] + ".wrong." + access_token.split(".")[-1] - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Wrong token:") @@ -185,15 +179,11 @@ def test_security_jwt_access_token_expiration(mocker: MockerFixture, jwt_backend access_token = client.post("/auth").json()["access_token"] mock_now_for_backend(mocker, jwt_backend, minutes=3) # 3 min left - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text mock_now_for_backend(mocker, jwt_backend, days=42) # 42 days left - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Token time expired:") @@ -203,24 +193,18 @@ def test_security_jwt_refresh_token(jwt_backend): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 200, response.text @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_refresh_token_wrong(jwt_backend): client, _ = create_example_client(jwt_backend) - response = client.post( - "/refresh", headers={"Authorization": "Bearer wrong_refresh_token"} - ) + response = client.post("/refresh", headers={"Authorization": "Bearer wrong_refresh_token"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Wrong token:") - response = client.post( - "/refresh", headers={"Authorization": "Bearer wrong.refresh.token"} - ) + response = client.post("/refresh", headers={"Authorization": "Bearer wrong.refresh.token"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Wrong token:") @@ -232,9 +216,7 @@ def test_security_jwt_refresh_token_using_access_token(jwt_backend): access_token, refresh_token = tokens["access_token"], tokens["refresh_token"] assert access_token != refresh_token - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Wrong token: 'type' is not 'refresh'") @@ -244,13 +226,9 @@ def test_security_jwt_refresh_token_changed(jwt_backend): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] - refresh_token = ( - refresh_token.split(".")[0] + ".wrong." + refresh_token.split(".")[-1] - ) + refresh_token = refresh_token.split(".")[0] + ".wrong." + refresh_token.split(".")[-1] - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Wrong token:") @@ -261,9 +239,7 @@ def test_security_jwt_refresh_token_expired(mocker: MockerFixture, jwt_backend): refresh_token = client.post("/auth").json()["refresh_token"] mock_now_for_backend(mocker, jwt_backend, days=42) # 42 days left - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 401, response.text assert response.json()["detail"].startswith("Token time expired:") @@ -273,8 +249,6 @@ def test_security_jwt_custom_jti(jwt_backend): client, unique_identifiers_database = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - response = client.get( - "/auth/meta", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/auth/meta", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json()["jti"] in unique_identifiers_database diff --git a/tests/test_security_jwt_general_optional.py b/tests/test_security_jwt_general_optional.py index e0571d0..aa2d057 100644 --- a/tests/test_security_jwt_general_optional.py +++ b/tests/test_security_jwt_general_optional.py @@ -6,12 +6,20 @@ from fastapi.testclient import TestClient from pytest_mock import MockerFixture -from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer -from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend +from fastapi_jwt import ( + AuthlibJWTBackend, + JwtAccessBearer, + JwtAuthorizationCredentials, + JwtRefreshBearer, + PythonJoseJWTBackend, + define_default_jwt_backend, +) +from fastapi_jwt.jwt_backends import AbstractJWTBackend + from .mock_datetime_utils import mock_now_for_backend -def create_example_client(jwt_backend): +def create_example_client(jwt_backend: AbstractJWTBackend): define_default_jwt_backend(jwt_backend) app = FastAPI() @@ -19,21 +27,17 @@ def create_example_client(jwt_backend): refresh_security = JwtRefreshBearer.from_other(access_security) unique_identifiers_database: Set[str] = set() - @app.post("/auth") def auth(): subject = {"username": "username", "role": "user"} unique_identifier = str(uuid4()) unique_identifiers_database.add(unique_identifier) - access_token = access_security.create_access_token( - subject=subject, unique_identifier=unique_identifier - ) + access_token = access_security.create_access_token(subject=subject, unique_identifier=unique_identifier) refresh_token = access_security.create_refresh_token(subject=subject) return {"access_token": access_token, "refresh_token": refresh_token} - @app.post("/refresh") def refresh( credentials: Optional[JwtAuthorizationCredentials] = Security(refresh_security), @@ -45,13 +49,13 @@ def refresh( unique_identifiers_database.add(unique_identifier) access_token = refresh_security.create_access_token( - subject=credentials.subject, unique_identifier=unique_identifier, + subject=credentials.subject, + unique_identifier=unique_identifier, ) refresh_token = refresh_security.create_refresh_token(subject=credentials.subject) return {"access_token": access_token, "refresh_token": refresh_token} - @app.get("/users/me") def read_current_user( credentials: Optional[JwtAuthorizationCredentials] = Security(access_security), @@ -60,16 +64,14 @@ def read_current_user( return {"msg": "Create an account first"} return {"username": credentials["username"], "role": credentials["role"]} - @app.get("/auth/meta") def get_token_meta( - credentials: JwtAuthorizationCredentials = Security(access_security), + credentials: JwtAuthorizationCredentials = Security(access_security), ): if credentials is None: return {"msg": "Create an account first"} return {"jti": credentials.jti} - return TestClient(app), unique_identifiers_database @@ -151,9 +153,7 @@ def test_security_jwt_access_token(jwt_backend): client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"username": "username", "role": "user"} @@ -161,15 +161,11 @@ def test_security_jwt_access_token(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_access_token_wrong(jwt_backend): client, _ = create_example_client(jwt_backend) - response = client.get( - "/users/me", headers={"Authorization": "Bearer wrong_access_token"} - ) + response = client.get("/users/me", headers={"Authorization": "Bearer wrong_access_token"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} - response = client.get( - "/users/me", headers={"Authorization": "Bearer wrong.access.token"} - ) + response = client.get("/users/me", headers={"Authorization": "Bearer wrong.access.token"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -181,9 +177,7 @@ def test_security_jwt_access_token_changed(jwt_backend): access_token = access_token.split(".")[0] + ".wrong." + access_token.split(".")[-1] - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -194,16 +188,12 @@ def test_security_jwt_access_token_expiration(mocker: MockerFixture, jwt_backend access_token = client.post("/auth").json()["access_token"] mock_now_for_backend(mocker, jwt_backend, minutes=3) # 3 min left - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"username": "username", "role": "user"} mock_now_for_backend(mocker, jwt_backend, days=42) # 42 days left - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -213,9 +203,7 @@ def test_security_jwt_refresh_token(jwt_backend): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 200, response.text assert "msg" not in response.json() @@ -223,15 +211,11 @@ def test_security_jwt_refresh_token(jwt_backend): @pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_refresh_token_wrong(jwt_backend): client, _ = create_example_client(jwt_backend) - response = client.post( - "/refresh", headers={"Authorization": "Bearer wrong_refresh_token"} - ) + response = client.post("/refresh", headers={"Authorization": "Bearer wrong_refresh_token"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} - response = client.post( - "/refresh", headers={"Authorization": "Bearer wrong.refresh.token"} - ) + response = client.post("/refresh", headers={"Authorization": "Bearer wrong.refresh.token"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -243,9 +227,7 @@ def test_security_jwt_refresh_token_using_access_token(jwt_backend): access_token, refresh_token = tokens["access_token"], tokens["refresh_token"] assert access_token != refresh_token - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -255,13 +237,9 @@ def test_security_jwt_refresh_token_changed(jwt_backend): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] - refresh_token = ( - refresh_token.split(".")[0] + ".wrong." + refresh_token.split(".")[-1] - ) + refresh_token = refresh_token.split(".")[0] + ".wrong." + refresh_token.split(".")[-1] - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -272,9 +250,7 @@ def test_security_jwt_refresh_token_expired(mocker: MockerFixture, jwt_backend): refresh_token = client.post("/auth").json()["refresh_token"] mock_now_for_backend(mocker, jwt_backend, days=42) # 42 days left - response = client.post( - "/refresh", headers={"Authorization": f"Bearer {refresh_token}"} - ) + response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} @@ -284,8 +260,6 @@ def test_security_jwt_custom_jti(jwt_backend): client, unique_identifiers_database = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - response = client.get( - "/auth/meta", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/auth/meta", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json()["jti"] in unique_identifiers_database diff --git a/tests/test_security_jwt_multiple_places.py b/tests/test_security_jwt_multiple_places.py index 6a91cf7..4a90d01 100644 --- a/tests/test_security_jwt_multiple_places.py +++ b/tests/test_security_jwt_multiple_places.py @@ -2,18 +2,24 @@ from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import JwtAccessBearerCookie, JwtAuthorizationCredentials, JwtRefreshBearerCookie -from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend - - -def create_example_client(jwt_backend): +from fastapi_jwt import ( + AuthlibJWTBackend, + JwtAccessBearerCookie, + JwtAuthorizationCredentials, + JwtRefreshBearerCookie, + PythonJoseJWTBackend, + define_default_jwt_backend, +) +from fastapi_jwt.jwt_backends import AbstractJWTBackend + + +def create_example_client(jwt_backend: AbstractJWTBackend): define_default_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessBearerCookie(secret_key="secret_key") refresh_security = JwtRefreshBearerCookie(secret_key="secret_key") - @app.post("/auth") def auth(): subject = {"username": "username", "role": "user"} @@ -23,7 +29,6 @@ def auth(): return {"access_token": access_token, "refresh_token": refresh_token} - @app.post("/refresh") def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security)): access_token = refresh_security.create_access_token(subject=credentials.subject) @@ -31,14 +36,12 @@ def refresh(credentials: JwtAuthorizationCredentials = Security(refresh_security return {"access_token": access_token, "refresh_token": refresh_token} - @app.get("/users/me") def read_current_user( credentials: JwtAuthorizationCredentials = Security(access_security), ): return {"username": credentials["username"], "role": credentials["role"]} - return TestClient(app) @@ -141,9 +144,7 @@ def test_security_jwt_access_only_bearer(jwt_backend): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] - response = client.get( - "/users/me", headers={"Authorization": f"Bearer {access_token}"} - ) + response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 200, response.text assert response.json() == {"username": "username", "role": "user"} diff --git a/tests/test_security_jwt_set_cookie.py b/tests/test_security_jwt_set_cookie.py index d2fd32e..41b1db3 100644 --- a/tests/test_security_jwt_set_cookie.py +++ b/tests/test_security_jwt_set_cookie.py @@ -2,18 +2,23 @@ from fastapi import FastAPI, Response from fastapi.testclient import TestClient -from fastapi_jwt import JwtAccessCookie, JwtRefreshCookie -from fastapi_jwt import AuthlibJWTBackend, PythonJoseJWTBackend, define_default_jwt_backend +from fastapi_jwt import ( + AuthlibJWTBackend, + JwtAccessCookie, + JwtRefreshCookie, + PythonJoseJWTBackend, + define_default_jwt_backend, +) +from fastapi_jwt.jwt_backends import AbstractJWTBackend -def create_example_client(jwt_backend): +def create_example_client(jwt_backend: AbstractJWTBackend): define_default_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessCookie(secret_key="secret_key") refresh_security = JwtRefreshCookie(secret_key="secret_key") - @app.post("/auth") def auth(response: Response): subject = {"username": "username", "role": "user"} @@ -26,7 +31,6 @@ def auth(response: Response): return {"access_token": access_token, "refresh_token": refresh_token} - @app.delete("/auth") def logout(response: Response): access_security.unset_access_cookie(response) @@ -34,7 +38,6 @@ def logout(response: Response): return {"msg": "Successful logout"} - return TestClient(app) @@ -97,10 +100,7 @@ def test_security_jwt_logout(jwt_backend): assert "access_token_cookie" in response.headers["set-cookie"] assert 'access_token_cookie=""; Max-Age=-1;' in response.headers["set-cookie"] assert "refresh_token_cookie" in response.headers["set-cookie"] - assert ( - 'refresh_token_cookie=""; HttpOnly; Max-Age=-1' - in response.headers["set-cookie"] - ) + assert 'refresh_token_cookie=""; HttpOnly; Max-Age=-1' in response.headers["set-cookie"] # assert "access_token_cookie" not in response.cookies # assert response.cookies["access_token_cookie"].max_age == -1 # assert "refresh_token_cookie" not in response.cookies From 93cbdee6a0cf386994e6905f00aeb4165226396c Mon Sep 17 00:00:00 2001 From: Konstantin Chernyshev Date: Mon, 6 May 2024 13:01:25 +0200 Subject: [PATCH 3/6] fix: remove unused typings Self import --- fastapi_jwt/jwt_backends/abstract_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastapi_jwt/jwt_backends/abstract_backend.py b/fastapi_jwt/jwt_backends/abstract_backend.py index 3d11a30..6ea18a1 100644 --- a/fastapi_jwt/jwt_backends/abstract_backend.py +++ b/fastapi_jwt/jwt_backends/abstract_backend.py @@ -1,5 +1,5 @@ -from abc import ABCMeta, abstractmethod, abstractproperty -from typing import Any, Dict, Optional, Self +from abc import ABCMeta, abstractmethod +from typing import Any, Dict, Optional class AbstractJWTBackend(metaclass=ABCMeta): From ad4221d371b8c360598c012f337b3f6b0f7ced54 Mon Sep 17 00:00:00 2001 From: Konstantin Chernyshev Date: Mon, 6 May 2024 14:08:07 +0200 Subject: [PATCH 4/6] refactor: backends design + tests --- fastapi_jwt/jwt.py | 34 ++++++----- fastapi_jwt/jwt_backends/__init__.py | 13 +--- fastapi_jwt/jwt_backends/abstract_backend.py | 22 ++----- fastapi_jwt/jwt_backends/authlib_backend.py | 22 +++---- .../jwt_backends/python_jose_backend.py | 38 +++++------- tests/conftest.py | 10 ++++ tests/test_security_jwt_bearer.py | 46 +++++---------- tests/test_security_jwt_bearer_optional.py | 46 +++++---------- tests/test_security_jwt_cookie.py | 40 ++++--------- tests/test_security_jwt_cookie_optional.py | 40 ++++--------- tests/test_security_jwt_general.py | 59 +++++++------------ tests/test_security_jwt_general_optional.py | 45 ++++---------- tests/test_security_jwt_multiple_places.py | 58 +++++++----------- tests/test_security_jwt_set_cookie.py | 24 +++----- 14 files changed, 176 insertions(+), 321 deletions(-) create mode 100644 tests/conftest.py diff --git a/fastapi_jwt/jwt.py b/fastapi_jwt/jwt.py index b86db07..4b86a1f 100644 --- a/fastapi_jwt/jwt.py +++ b/fastapi_jwt/jwt.py @@ -9,24 +9,23 @@ from fastapi.security import APIKeyCookie, HTTPBearer from starlette.status import HTTP_401_UNAUTHORIZED -from .jwt_backends import AbstractJWTBackend, AuthlibJWTBackend, PythonJoseJWTBackend +from .jwt_backends import AbstractJWTBackend, authlib_backend, python_jose_backend +from .jwt_backends.abstract_backend import BackendException DEFAULT_JWT_BACKEND: Optional[Type[AbstractJWTBackend]] = None +if authlib_backend.authlib_jose is not None: + DEFAULT_JWT_BACKEND = authlib_backend.AuthlibJWTBackend +elif python_jose_backend.jose is not None: + DEFAULT_JWT_BACKEND = python_jose_backend.PythonJoseJWTBackend +else: # pragma: nocover + raise ImportError("No JWT backend found, please install 'python-jose' or 'authlib'") -def define_default_jwt_backend(cls: Type[AbstractJWTBackend]) -> None: +def force_jwt_backend(cls: Type[AbstractJWTBackend]) -> None: global DEFAULT_JWT_BACKEND DEFAULT_JWT_BACKEND = cls -if AuthlibJWTBackend is not None: - DEFAULT_JWT_BACKEND = AuthlibJWTBackend -elif PythonJoseJWTBackend is not None: - DEFAULT_JWT_BACKEND = PythonJoseJWTBackend -else: # pragma: nocover - raise ImportError("No JWT backend found, please install 'python-jose' or 'authlib'") - - def utcnow() -> datetime: try: from datetime import UTC @@ -39,7 +38,7 @@ def utcnow() -> datetime: __all__ = [ - "define_default_jwt_backend", + "force_jwt_backend", "JwtAuthorizationCredentials", "JwtAccessBearer", "JwtAccessCookie", @@ -89,10 +88,9 @@ def __init__( self.jwt_backend = DEFAULT_JWT_BACKEND(algorithm) self.secret_key = secret_key - if places: - assert places.issubset({"header", "cookie"}), "only 'header'/'cookie' are supported" self.places = places or {"header"} + assert self.places.issubset({"header", "cookie"}), "only 'header' and/or 'cookie' places are supported" self.auto_error = auto_error self.access_expires_delta = access_expires_delta or timedelta(minutes=15) self.refresh_expires_delta = refresh_expires_delta or timedelta(days=31) @@ -152,7 +150,13 @@ async def _get_payload( return None # Try to decode jwt token. auto_error on error - return self.jwt_backend.decode(token, self.secret_key, self.auto_error) + try: + return self.jwt_backend.decode(token, self.secret_key) + except BackendException as e: + if self.auto_error: + raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=str(e)) + else: + return None def create_access_token( self, @@ -354,7 +358,7 @@ async def _get_credentials( if self.auto_error: raise HTTPException( status_code=HTTP_401_UNAUTHORIZED, - detail="Wrong token: 'type' is not 'refresh'", + detail="Invalid token: 'type' is not 'refresh'", ) else: return None diff --git a/fastapi_jwt/jwt_backends/__init__.py b/fastapi_jwt/jwt_backends/__init__.py index 251c72f..59dd097 100644 --- a/fastapi_jwt/jwt_backends/__init__.py +++ b/fastapi_jwt/jwt_backends/__init__.py @@ -1,11 +1,4 @@ -try: - from .authlib_backend import AuthlibJWTBackend -except ImportError: - AuthlibJWTBackend = None # type: ignore - -try: - from .python_jose_backend import PythonJoseJWTBackend -except ImportError: - PythonJoseJWTBackend = None # type: ignore - +from . import abstract_backend, authlib_backend, python_jose_backend # noqa: F401 from .abstract_backend import AbstractJWTBackend # noqa: F401 +from .authlib_backend import AuthlibJWTBackend # noqa: F401 +from .python_jose_backend import PythonJoseJWTBackend # noqa: F401 diff --git a/fastapi_jwt/jwt_backends/abstract_backend.py b/fastapi_jwt/jwt_backends/abstract_backend.py index 6ea18a1..b85ec24 100644 --- a/fastapi_jwt/jwt_backends/abstract_backend.py +++ b/fastapi_jwt/jwt_backends/abstract_backend.py @@ -1,24 +1,14 @@ -from abc import ABCMeta, abstractmethod +from abc import ABC, abstractmethod from typing import Any, Dict, Optional -class AbstractJWTBackend(metaclass=ABCMeta): - # simple "SingletonArgs" implementation to keep a JWTBackend per algorithm - _instances: Dict[Any, "AbstractJWTBackend"] = {} +class BackendException(Exception): + pass - def __new__(cls, algorithm: Optional[str]) -> "AbstractJWTBackend": - instance_key = (cls, algorithm) - if instance_key not in cls._instances: - cls._instances[instance_key] = super(AbstractJWTBackend, cls).__new__(cls) - return cls._instances[instance_key] +class AbstractJWTBackend(ABC): @abstractmethod - def __init__(self, algorithm: Optional[str]) -> None: - raise NotImplementedError - - @property - @abstractmethod - def default_algorithm(self) -> str: + def __init__(self, algorithm: Optional[str] = None) -> None: raise NotImplementedError @property @@ -31,5 +21,5 @@ def encode(self, to_encode: Dict[str, Any], secret_key: str) -> str: raise NotImplementedError @abstractmethod - def decode(self, token: str, secret_key: str, auto_error: bool) -> Optional[Dict[str, Any]]: + def decode(self, token: str, secret_key: str) -> Optional[Dict[str, Any]]: raise NotImplementedError diff --git a/fastapi_jwt/jwt_backends/authlib_backend.py b/fastapi_jwt/jwt_backends/authlib_backend.py index 81be16f..9180dc0 100644 --- a/fastapi_jwt/jwt_backends/authlib_backend.py +++ b/fastapi_jwt/jwt_backends/authlib_backend.py @@ -1,15 +1,12 @@ from typing import Any, Dict, Optional -from fastapi import HTTPException -from starlette.status import HTTP_401_UNAUTHORIZED - try: import authlib.jose as authlib_jose import authlib.jose.errors as authlib_jose_errors except ImportError: authlib_jose = None -from .abstract_backend import AbstractJWTBackend +from .abstract_backend import AbstractJWTBackend, BackendException class AuthlibJWTBackend(AbstractJWTBackend): @@ -18,8 +15,9 @@ def __init__(self, algorithm: Optional[str] = None) -> None: self._algorithm = algorithm or self.default_algorithm # from https://github.com/lepture/authlib/blob/85f9ff/authlib/jose/__init__.py#L45 - valid_algorithms = authlib_jose.JsonWebSignature.ALGORITHMS_REGISTRY.keys() - assert self._algorithm in valid_algorithms, f"{self._algorithm} algorithm is not supported by authlib" + assert ( + self._algorithm in authlib_jose.JsonWebSignature.ALGORITHMS_REGISTRY.keys() + ), f"{self._algorithm} algorithm is not supported by authlib" self.jwt = authlib_jose.JsonWebToken(algorithms=[self._algorithm]) @property @@ -34,22 +32,16 @@ def encode(self, to_encode: Dict[str, Any], secret_key: str) -> str: token = self.jwt.encode(header={"alg": self.algorithm}, payload=to_encode, key=secret_key) return token.decode() # convert to string - def decode(self, token: str, secret_key: str, auto_error: bool) -> Optional[Dict[str, Any]]: + def decode(self, token: str, secret_key: str) -> Optional[Dict[str, Any]]: try: payload = self.jwt.decode(token, secret_key) payload.validate(leeway=10) return dict(payload) except authlib_jose_errors.ExpiredTokenError as e: - if auto_error: - raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}") - else: - return None + raise BackendException(f"Token time expired: {e}") except ( authlib_jose_errors.InvalidClaimError, authlib_jose_errors.InvalidTokenError, authlib_jose_errors.DecodeError, ) as e: - if auto_error: - raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}") - else: - return None + raise BackendException(f"Invalid token: {e}") diff --git a/fastapi_jwt/jwt_backends/python_jose_backend.py b/fastapi_jwt/jwt_backends/python_jose_backend.py index de8bfd3..b5771f5 100644 --- a/fastapi_jwt/jwt_backends/python_jose_backend.py +++ b/fastapi_jwt/jwt_backends/python_jose_backend.py @@ -1,52 +1,46 @@ +import warnings from typing import Any, Dict, Optional -from fastapi import HTTPException -from starlette.status import HTTP_401_UNAUTHORIZED - try: - from jose import jwt + import jose + import jose.jwt except ImportError: - jwt = None # type: ignore + jose = None # type: ignore -from .abstract_backend import AbstractJWTBackend +from .abstract_backend import AbstractJWTBackend, BackendException class PythonJoseJWTBackend(AbstractJWTBackend): def __init__(self, algorithm: Optional[str] = None) -> None: - assert jwt is not None, "To use PythonJoseJWTBackend, you need to install python-jose" + assert jose is not None, "To use PythonJoseJWTBackend, you need to install python-jose" + warnings.warn("PythonJoseJWTBackend is deprecated as python-jose library is not maintained anymore.") self._algorithm = algorithm or self.default_algorithm assert ( - hasattr(jwt.ALGORITHMS, self._algorithm) is True # type: ignore[attr-defined] + hasattr(jose.jwt.ALGORITHMS, self._algorithm) is True # type: ignore[attr-defined] ), f"{algorithm} algorithm is not supported by python-jose library" @property def default_algorithm(self) -> str: - return jwt.ALGORITHMS.HS256 + return jose.jwt.ALGORITHMS.HS256 # type: ignore[attr-defined] @property def algorithm(self) -> str: return self._algorithm def encode(self, to_encode: Dict[str, Any], secret_key: str) -> str: - return jwt.encode(to_encode, secret_key, algorithm=self._algorithm) + return jose.jwt.encode(to_encode, secret_key, algorithm=self._algorithm) - def decode(self, token: str, secret_key: str, auto_error: bool) -> Optional[Dict[str, Any]]: + def decode(self, token: str, secret_key: str) -> Optional[Dict[str, Any]]: try: - payload: Dict[str, Any] = jwt.decode( + payload: Dict[str, Any] = jose.jwt.decode( token, secret_key, algorithms=[self._algorithm], options={"leeway": 10}, ) return payload - except jwt.ExpiredSignatureError as e: # type: ignore[attr-defined] - if auto_error: - raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=f"Token time expired: {e}") - else: - return None - except jwt.JWTError as e: # type: ignore[attr-defined] - if auto_error: - raise HTTPException(status_code=HTTP_401_UNAUTHORIZED, detail=f"Wrong token: {e}") - else: - return None + except jose.jwt.ExpiredSignatureError as e: # type: ignore[attr-defined] + raise BackendException(f"Token time expired: {e}") + except jose.jwt.JWTError as e: # type: ignore[attr-defined] + raise BackendException(f"Invalid token: {e}") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..0966f9b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,10 @@ +from typing import Type + +import pytest + +from fastapi_jwt.jwt_backends import AbstractJWTBackend, AuthlibJWTBackend, PythonJoseJWTBackend + + +@pytest.fixture(params=[PythonJoseJWTBackend, AuthlibJWTBackend]) +def jwt_backend(request: pytest.FixtureRequest) -> Type[AbstractJWTBackend]: + return request.param diff --git a/tests/test_security_jwt_bearer.py b/tests/test_security_jwt_bearer.py index 3d73417..c927a80 100644 --- a/tests/test_security_jwt_bearer.py +++ b/tests/test_security_jwt_bearer.py @@ -1,20 +1,14 @@ -import pytest +from typing import Type + from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import ( - AuthlibJWTBackend, - JwtAccessBearer, - JwtAuthorizationCredentials, - JwtRefreshBearer, - PythonJoseJWTBackend, - define_default_jwt_backend, -) +from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer, force_jwt_backend from fastapi_jwt.jwt_backends import AbstractJWTBackend -def create_example_client(jwt_backend: AbstractJWTBackend): - define_default_jwt_backend(jwt_backend) +def create_example_client(jwt_backend: Type[AbstractJWTBackend]): + force_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessBearer(secret_key="secret_key") @@ -97,23 +91,20 @@ def read_current_user( } -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend: AbstractJWTBackend): +def test_openapi_schema(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_auth(jwt_backend: AbstractJWTBackend): +def test_security_jwt_auth(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer(jwt_backend: AbstractJWTBackend): +def test_security_jwt_access_bearer(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -122,23 +113,20 @@ def test_security_jwt_access_bearer(jwt_backend: AbstractJWTBackend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_wrong(jwt_backend: AbstractJWTBackend): +def test_security_jwt_access_bearer_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Bearer wrong_access_token"}) assert response.status_code == 401, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_no_credentials(jwt_backend: AbstractJWTBackend): +def test_security_jwt_access_bearer_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me") assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend: AbstractJWTBackend): +def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 401, response.text @@ -146,8 +134,7 @@ def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend: Ab # assert response.json() == {"detail": "Invalid authentication credentials"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer(jwt_backend: AbstractJWTBackend): +def test_security_jwt_refresh_bearer(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -155,23 +142,20 @@ def test_security_jwt_refresh_bearer(jwt_backend: AbstractJWTBackend): assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_wrong(jwt_backend: AbstractJWTBackend): +def test_security_jwt_refresh_bearer_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Bearer wrong_refresh_token"}) assert response.status_code == 401, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_no_credentials(jwt_backend: AbstractJWTBackend): +def test_security_jwt_refresh_bearer_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh") assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(jwt_backend: AbstractJWTBackend): +def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Basic notreally"}) assert response.status_code == 401, response.text diff --git a/tests/test_security_jwt_bearer_optional.py b/tests/test_security_jwt_bearer_optional.py index 9f29a6f..67ffe94 100644 --- a/tests/test_security_jwt_bearer_optional.py +++ b/tests/test_security_jwt_bearer_optional.py @@ -1,22 +1,14 @@ -from typing import Optional +from typing import Optional, Type -import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import ( - AuthlibJWTBackend, - JwtAccessBearer, - JwtAuthorizationCredentials, - JwtRefreshBearer, - PythonJoseJWTBackend, - define_default_jwt_backend, -) +from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer, force_jwt_backend from fastapi_jwt.jwt_backends import AbstractJWTBackend -def create_example_client(jwt_backend: AbstractJWTBackend): - define_default_jwt_backend(jwt_backend) +def create_example_client(jwt_backend: Type[AbstractJWTBackend]): + force_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessBearer(secret_key="secret_key", auto_error=False) @@ -106,23 +98,20 @@ def read_current_user( } -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend): +def test_openapi_schema(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_auth(jwt_backend): +def test_security_jwt_auth(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer(jwt_backend): +def test_security_jwt_access_bearer(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -131,32 +120,28 @@ def test_security_jwt_access_bearer(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_wrong(jwt_backend): +def test_security_jwt_access_bearer_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Bearer wrong_access_token"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_no_credentials(jwt_backend): +def test_security_jwt_access_bearer_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend): +def test_security_jwt_access_bearer_incorrect_scheme_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Basic notreally"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer(jwt_backend): +def test_security_jwt_refresh_bearer(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -164,24 +149,21 @@ def test_security_jwt_refresh_bearer(jwt_backend): assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_wrong(jwt_backend): +def test_security_jwt_refresh_bearer_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Bearer wrong_refresh_token"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_no_credentials(jwt_backend): +def test_security_jwt_refresh_bearer_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh") assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(jwt_backend): +def test_security_jwt_refresh_bearer_incorrect_scheme_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Basic notreally"}) assert response.status_code == 200, response.text diff --git a/tests/test_security_jwt_cookie.py b/tests/test_security_jwt_cookie.py index 4e9b2e5..51a0f99 100644 --- a/tests/test_security_jwt_cookie.py +++ b/tests/test_security_jwt_cookie.py @@ -1,20 +1,14 @@ -import pytest +from typing import Type + from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import ( - AuthlibJWTBackend, - JwtAccessCookie, - JwtAuthorizationCredentials, - JwtRefreshCookie, - PythonJoseJWTBackend, - define_default_jwt_backend, -) +from fastapi_jwt import JwtAccessCookie, JwtAuthorizationCredentials, JwtRefreshCookie, force_jwt_backend from fastapi_jwt.jwt_backends import AbstractJWTBackend -def create_example_client(jwt_backend: AbstractJWTBackend): - define_default_jwt_backend(jwt_backend) +def create_example_client(jwt_backend: Type[AbstractJWTBackend]): + force_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessCookie(secret_key="secret_key") @@ -105,23 +99,20 @@ def read_current_user( } -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend): +def test_openapi_schema(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_auth(jwt_backend): +def test_security_jwt_auth(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_cookie(jwt_backend): +def test_security_jwt_access_cookie(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -130,15 +121,13 @@ def test_security_jwt_access_cookie(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_cookie_wrong(jwt_backend): +def test_security_jwt_access_cookie_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me", cookies={"access_token_cookie": "wrong_access_token_cookie"}) assert response.status_code == 401, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_cookie_no_credentials(jwt_backend): +def test_security_jwt_access_cookie_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) client.cookies.clear() response = client.get("/users/me", cookies={}) @@ -146,8 +135,7 @@ def test_security_jwt_access_cookie_no_credentials(jwt_backend): assert response.json() == {"detail": "Credentials are not provided"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_cookie(jwt_backend): +def test_security_jwt_refresh_cookie(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) client.cookies.clear() refresh_token = client.post("/auth").json()["refresh_token"] @@ -156,15 +144,13 @@ def test_security_jwt_refresh_cookie(jwt_backend): assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_cookie_wrong(jwt_backend): +def test_security_jwt_refresh_cookie_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh", cookies={"refresh_token_cookie": "wrong_refresh_token_cookie"}) assert response.status_code == 401, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_cookie_no_credentials(jwt_backend): +def test_security_jwt_refresh_cookie_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) client.cookies.clear() response = client.post("/refresh", cookies={}) diff --git a/tests/test_security_jwt_cookie_optional.py b/tests/test_security_jwt_cookie_optional.py index 6a98720..7a03c91 100644 --- a/tests/test_security_jwt_cookie_optional.py +++ b/tests/test_security_jwt_cookie_optional.py @@ -1,22 +1,14 @@ -from typing import Optional +from typing import Optional, Type -import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import ( - AuthlibJWTBackend, - JwtAccessCookie, - JwtAuthorizationCredentials, - JwtRefreshCookie, - PythonJoseJWTBackend, - define_default_jwt_backend, -) +from fastapi_jwt import JwtAccessCookie, JwtAuthorizationCredentials, JwtRefreshCookie, force_jwt_backend from fastapi_jwt.jwt_backends import AbstractJWTBackend -def create_example_client(jwt_backend: AbstractJWTBackend): - define_default_jwt_backend(jwt_backend) +def create_example_client(jwt_backend: Type[AbstractJWTBackend]): + force_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessCookie(secret_key="secret_key", auto_error=False) @@ -115,23 +107,20 @@ def read_current_user( } -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend): +def test_openapi_schema(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_auth(jwt_backend): +def test_security_jwt_auth(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_cookie(jwt_backend): +def test_security_jwt_access_cookie(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) client.cookies.clear() access_token = client.post("/auth").json()["access_token"] @@ -141,24 +130,21 @@ def test_security_jwt_access_cookie(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_cookie_wrong(jwt_backend): +def test_security_jwt_access_cookie_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me", cookies={"access_token_cookie": "wrong_access_token_cookie"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_cookie_no_credentials(jwt_backend): +def test_security_jwt_access_cookie_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me", cookies={}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_cookie(jwt_backend): +def test_security_jwt_refresh_cookie(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -166,16 +152,14 @@ def test_security_jwt_refresh_cookie(jwt_backend): assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_cookie_wrong(jwt_backend): +def test_security_jwt_refresh_cookie_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh", cookies={"refresh_token_cookie": "wrong_refresh_token_cookie"}) assert response.status_code == 200, response.text assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_cookie_no_credentials(jwt_backend): +def test_security_jwt_refresh_cookie_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh", cookies={}) assert response.status_code == 200, response.text diff --git a/tests/test_security_jwt_general.py b/tests/test_security_jwt_general.py index c30f4b6..5e281dc 100644 --- a/tests/test_security_jwt_general.py +++ b/tests/test_security_jwt_general.py @@ -1,26 +1,18 @@ -from typing import Set +from typing import Set, Type from uuid import uuid4 -import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient from pytest_mock import MockerFixture -from fastapi_jwt import ( - AuthlibJWTBackend, - JwtAccessBearer, - JwtAuthorizationCredentials, - JwtRefreshBearer, - PythonJoseJWTBackend, - define_default_jwt_backend, -) +from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer, force_jwt_backend from fastapi_jwt.jwt_backends import AbstractJWTBackend from .mock_datetime_utils import mock_now_for_backend -def create_example_client(jwt_backend: AbstractJWTBackend): - define_default_jwt_backend(jwt_backend) +def create_example_client(jwt_backend: Type[AbstractJWTBackend]): + force_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessBearer(secret_key="secret_key") @@ -131,16 +123,14 @@ def get_token_meta( } -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend): +def test_openapi_schema(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_token(jwt_backend): +def test_security_jwt_access_token(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -149,20 +139,18 @@ def test_security_jwt_access_token(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_token_wrong(jwt_backend): +def test_security_jwt_access_token_wrong(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Bearer wrong_access_token"}) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token:") + assert response.json()["detail"].startswith("Invalid token:") response = client.get("/users/me", headers={"Authorization": "Bearer wrong.access.token"}) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token:") + assert response.json()["detail"].startswith("Invalid token:") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_token_changed(jwt_backend): +def test_security_jwt_access_token_changed(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -170,10 +158,9 @@ def test_security_jwt_access_token_changed(jwt_backend): response = client.get("/users/me", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token:") + assert response.json()["detail"].startswith("Invalid token:") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_access_token_expiration(mocker: MockerFixture, jwt_backend): client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -188,8 +175,7 @@ def test_security_jwt_access_token_expiration(mocker: MockerFixture, jwt_backend assert response.json()["detail"].startswith("Token time expired:") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_token(jwt_backend): +def test_security_jwt_refresh_token(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -197,20 +183,18 @@ def test_security_jwt_refresh_token(jwt_backend): assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_token_wrong(jwt_backend): +def test_security_jwt_refresh_token_wrong(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Bearer wrong_refresh_token"}) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token:") + assert response.json()["detail"].startswith("Invalid token:") response = client.post("/refresh", headers={"Authorization": "Bearer wrong.refresh.token"}) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token:") + assert response.json()["detail"].startswith("Invalid token:") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_token_using_access_token(jwt_backend): +def test_security_jwt_refresh_token_using_access_token(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) tokens = client.post("/auth").json() access_token, refresh_token = tokens["access_token"], tokens["refresh_token"] @@ -218,11 +202,10 @@ def test_security_jwt_refresh_token_using_access_token(jwt_backend): response = client.post("/refresh", headers={"Authorization": f"Bearer {access_token}"}) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token: 'type' is not 'refresh'") + assert response.json()["detail"].startswith("Invalid token: 'type' is not 'refresh'") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_token_changed(jwt_backend): +def test_security_jwt_refresh_token_changed(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -230,10 +213,9 @@ def test_security_jwt_refresh_token_changed(jwt_backend): response = client.post("/refresh", headers={"Authorization": f"Bearer {refresh_token}"}) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token:") + assert response.json()["detail"].startswith("Invalid token:") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_refresh_token_expired(mocker: MockerFixture, jwt_backend): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -244,8 +226,7 @@ def test_security_jwt_refresh_token_expired(mocker: MockerFixture, jwt_backend): assert response.json()["detail"].startswith("Token time expired:") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_custom_jti(jwt_backend): +def test_security_jwt_custom_jti(jwt_backend: Type[AbstractJWTBackend]): client, unique_identifiers_database = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] diff --git a/tests/test_security_jwt_general_optional.py b/tests/test_security_jwt_general_optional.py index aa2d057..86b1f29 100644 --- a/tests/test_security_jwt_general_optional.py +++ b/tests/test_security_jwt_general_optional.py @@ -1,26 +1,18 @@ -from typing import Optional, Set +from typing import Optional, Set, Type from uuid import uuid4 -import pytest from fastapi import FastAPI, Security from fastapi.testclient import TestClient from pytest_mock import MockerFixture -from fastapi_jwt import ( - AuthlibJWTBackend, - JwtAccessBearer, - JwtAuthorizationCredentials, - JwtRefreshBearer, - PythonJoseJWTBackend, - define_default_jwt_backend, -) +from fastapi_jwt import JwtAccessBearer, JwtAuthorizationCredentials, JwtRefreshBearer, force_jwt_backend from fastapi_jwt.jwt_backends import AbstractJWTBackend from .mock_datetime_utils import mock_now_for_backend -def create_example_client(jwt_backend: AbstractJWTBackend): - define_default_jwt_backend(jwt_backend) +def create_example_client(jwt_backend: Type[AbstractJWTBackend]): + force_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessBearer(secret_key="secret_key", auto_error=False) @@ -140,16 +132,14 @@ def get_token_meta( } -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend): +def test_openapi_schema(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_token(jwt_backend): +def test_security_jwt_access_token(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -158,8 +148,7 @@ def test_security_jwt_access_token(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_token_wrong(jwt_backend): +def test_security_jwt_access_token_wrong(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) response = client.get("/users/me", headers={"Authorization": "Bearer wrong_access_token"}) assert response.status_code == 200, response.text @@ -170,8 +159,7 @@ def test_security_jwt_access_token_wrong(jwt_backend): assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_token_changed(jwt_backend): +def test_security_jwt_access_token_changed(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -182,7 +170,6 @@ def test_security_jwt_access_token_changed(jwt_backend): assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_access_token_expiration(mocker: MockerFixture, jwt_backend): client, _ = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -198,8 +185,7 @@ def test_security_jwt_access_token_expiration(mocker: MockerFixture, jwt_backend assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_token(jwt_backend): +def test_security_jwt_refresh_token(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -208,8 +194,7 @@ def test_security_jwt_refresh_token(jwt_backend): assert "msg" not in response.json() -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_token_wrong(jwt_backend): +def test_security_jwt_refresh_token_wrong(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) response = client.post("/refresh", headers={"Authorization": "Bearer wrong_refresh_token"}) assert response.status_code == 200, response.text @@ -220,8 +205,7 @@ def test_security_jwt_refresh_token_wrong(jwt_backend): assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_token_using_access_token(jwt_backend): +def test_security_jwt_refresh_token_using_access_token(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) tokens = client.post("/auth").json() access_token, refresh_token = tokens["access_token"], tokens["refresh_token"] @@ -232,8 +216,7 @@ def test_security_jwt_refresh_token_using_access_token(jwt_backend): assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_token_changed(jwt_backend): +def test_security_jwt_refresh_token_changed(jwt_backend: Type[AbstractJWTBackend]): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -244,7 +227,6 @@ def test_security_jwt_refresh_token_changed(jwt_backend): assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) def test_security_jwt_refresh_token_expired(mocker: MockerFixture, jwt_backend): client, _ = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -255,8 +237,7 @@ def test_security_jwt_refresh_token_expired(mocker: MockerFixture, jwt_backend): assert response.json() == {"msg": "Create an account first"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_custom_jti(jwt_backend): +def test_security_jwt_custom_jti(jwt_backend: Type[AbstractJWTBackend]): client, unique_identifiers_database = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] diff --git a/tests/test_security_jwt_multiple_places.py b/tests/test_security_jwt_multiple_places.py index 4a90d01..d2a8937 100644 --- a/tests/test_security_jwt_multiple_places.py +++ b/tests/test_security_jwt_multiple_places.py @@ -1,20 +1,14 @@ -import pytest +from typing import Type + from fastapi import FastAPI, Security from fastapi.testclient import TestClient -from fastapi_jwt import ( - AuthlibJWTBackend, - JwtAccessBearerCookie, - JwtAuthorizationCredentials, - JwtRefreshBearerCookie, - PythonJoseJWTBackend, - define_default_jwt_backend, -) +from fastapi_jwt import JwtAccessBearerCookie, JwtAuthorizationCredentials, JwtRefreshBearerCookie, force_jwt_backend from fastapi_jwt.jwt_backends import AbstractJWTBackend -def create_example_client(jwt_backend: AbstractJWTBackend): - define_default_jwt_backend(jwt_backend) +def create_example_client(jwt_backend: Type[AbstractJWTBackend]): + force_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessBearerCookie(secret_key="secret_key") @@ -107,16 +101,14 @@ def read_current_user( } -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend): +def test_openapi_schema(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_both_correct(jwt_backend): +def test_security_jwt_access_both_correct(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -129,8 +121,7 @@ def test_security_jwt_access_both_correct(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_only_cookie(jwt_backend): +def test_security_jwt_access_only_cookie(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -139,8 +130,7 @@ def test_security_jwt_access_only_cookie(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_only_bearer(jwt_backend): +def test_security_jwt_access_only_bearer(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -149,8 +139,7 @@ def test_security_jwt_access_only_bearer(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_wrong_cookie_correct(jwt_backend): +def test_security_jwt_access_bearer_wrong_cookie_correct(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -160,11 +149,10 @@ def test_security_jwt_access_bearer_wrong_cookie_correct(jwt_backend): cookies={"access_token_cookie": access_token}, ) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token:") + assert response.json()["detail"].startswith("Invalid token:") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_bearer_correct_cookie_wrong(jwt_backend): +def test_security_jwt_access_bearer_correct_cookie_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) access_token = client.post("/auth").json()["access_token"] @@ -177,16 +165,14 @@ def test_security_jwt_access_bearer_correct_cookie_wrong(jwt_backend): assert response.json() == {"username": "username", "role": "user"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_access_both_no_credentials(jwt_backend): +def test_security_jwt_access_both_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/users/me") assert response.status_code == 401, response.text assert response.json() == {"detail": "Credentials are not provided"} -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_only_cookie(jwt_backend): +def test_security_jwt_refresh_only_cookie(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -194,8 +180,7 @@ def test_security_jwt_refresh_only_cookie(jwt_backend): assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_correct_cookie_wrong(jwt_backend): +def test_security_jwt_refresh_bearer_correct_cookie_wrong(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -207,8 +192,7 @@ def test_security_jwt_refresh_bearer_correct_cookie_wrong(jwt_backend): assert response.status_code == 200, response.text -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_bearer_wrong_cookie_correct(jwt_backend): +def test_security_jwt_refresh_bearer_wrong_cookie_correct(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) refresh_token = client.post("/auth").json()["refresh_token"] @@ -218,11 +202,10 @@ def test_security_jwt_refresh_bearer_wrong_cookie_correct(jwt_backend): cookies={"refresh_token_cookie": refresh_token}, ) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token:") + assert response.json()["detail"].startswith("Invalid token:") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_cookie_wrong_using_access_token(jwt_backend): +def test_security_jwt_refresh_cookie_wrong_using_access_token(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) tokens = client.post("/auth").json() access_token, refresh_token = tokens["access_token"], tokens["refresh_token"] @@ -230,11 +213,10 @@ def test_security_jwt_refresh_cookie_wrong_using_access_token(jwt_backend): response = client.post("/refresh", cookies={"refresh_token_cookie": access_token}) assert response.status_code == 401, response.text - assert response.json()["detail"].startswith("Wrong token: 'type' is not 'refresh'") + assert response.json()["detail"].startswith("Invalid token: 'type' is not 'refresh'") -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_refresh_both_no_credentials(jwt_backend): +def test_security_jwt_refresh_both_no_credentials(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/refresh") assert response.status_code == 401, response.text diff --git a/tests/test_security_jwt_set_cookie.py b/tests/test_security_jwt_set_cookie.py index 41b1db3..a86e68e 100644 --- a/tests/test_security_jwt_set_cookie.py +++ b/tests/test_security_jwt_set_cookie.py @@ -1,19 +1,14 @@ -import pytest +from typing import Type + from fastapi import FastAPI, Response from fastapi.testclient import TestClient -from fastapi_jwt import ( - AuthlibJWTBackend, - JwtAccessCookie, - JwtRefreshCookie, - PythonJoseJWTBackend, - define_default_jwt_backend, -) +from fastapi_jwt import JwtAccessCookie, JwtRefreshCookie, force_jwt_backend from fastapi_jwt.jwt_backends import AbstractJWTBackend -def create_example_client(jwt_backend: AbstractJWTBackend): - define_default_jwt_backend(jwt_backend) +def create_example_client(jwt_backend: Type[AbstractJWTBackend]): + force_jwt_backend(jwt_backend) app = FastAPI() access_security = JwtAccessCookie(secret_key="secret_key") @@ -71,16 +66,14 @@ def logout(response: Response): } -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_openapi_schema(jwt_backend): +def test_openapi_schema(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == openapi_schema -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_auth(jwt_backend): +def test_security_jwt_auth(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.post("/auth") assert response.status_code == 200, response.text @@ -91,8 +84,7 @@ def test_security_jwt_auth(jwt_backend): assert response.cookies["refresh_token_cookie"] == response.json()["refresh_token"] -@pytest.mark.parametrize("jwt_backend", [AuthlibJWTBackend, PythonJoseJWTBackend]) -def test_security_jwt_logout(jwt_backend): +def test_security_jwt_logout(jwt_backend: Type[AbstractJWTBackend]): client = create_example_client(jwt_backend) response = client.delete("/auth") assert response.status_code == 200, response.text From 67866bfe2ec678b9af692f058529152f010678d1 Mon Sep 17 00:00:00 2001 From: Konstantin Chernyshev Date: Mon, 6 May 2024 14:22:08 +0200 Subject: [PATCH 5/6] chore: add # pragma: no cover where needed --- fastapi_jwt/jwt_backends/abstract_backend.py | 4 ++-- fastapi_jwt/jwt_backends/authlib_backend.py | 2 +- fastapi_jwt/jwt_backends/python_jose_backend.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fastapi_jwt/jwt_backends/abstract_backend.py b/fastapi_jwt/jwt_backends/abstract_backend.py index b85ec24..c15f0c0 100644 --- a/fastapi_jwt/jwt_backends/abstract_backend.py +++ b/fastapi_jwt/jwt_backends/abstract_backend.py @@ -2,11 +2,11 @@ from typing import Any, Dict, Optional -class BackendException(Exception): +class BackendException(Exception): # pragma: no cover pass -class AbstractJWTBackend(ABC): +class AbstractJWTBackend(ABC): # pragma: no cover @abstractmethod def __init__(self, algorithm: Optional[str] = None) -> None: raise NotImplementedError diff --git a/fastapi_jwt/jwt_backends/authlib_backend.py b/fastapi_jwt/jwt_backends/authlib_backend.py index 9180dc0..780992e 100644 --- a/fastapi_jwt/jwt_backends/authlib_backend.py +++ b/fastapi_jwt/jwt_backends/authlib_backend.py @@ -3,7 +3,7 @@ try: import authlib.jose as authlib_jose import authlib.jose.errors as authlib_jose_errors -except ImportError: +except ImportError: # pragma: no cover authlib_jose = None from .abstract_backend import AbstractJWTBackend, BackendException diff --git a/fastapi_jwt/jwt_backends/python_jose_backend.py b/fastapi_jwt/jwt_backends/python_jose_backend.py index b5771f5..4abe9ad 100644 --- a/fastapi_jwt/jwt_backends/python_jose_backend.py +++ b/fastapi_jwt/jwt_backends/python_jose_backend.py @@ -4,7 +4,7 @@ try: import jose import jose.jwt -except ImportError: +except ImportError: # pragma: no cover jose = None # type: ignore from .abstract_backend import AbstractJWTBackend, BackendException From 57b463bf38a7da35f5efbb5e265c737909f433ed Mon Sep 17 00:00:00 2001 From: Konstantin Chernyshev Date: Mon, 6 May 2024 14:27:15 +0200 Subject: [PATCH 6/6] docs: update README.md --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 90e7c33..3e675dc 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,14 @@ FastAPI native extension, easy and simple JWT auth ## Installation You can access package [fastapi-jwt in pypi](https://pypi.org/project/fastapi-jwt/) ```shell -pip install fastapi-jwt +pip install fastapi-jwt[authlib] +# or +pip install fastapi-jwt[python_jose] ``` +The fastapi-jwt will choose the backend automatically if library is installed with the following priority: +1. authlib +2. python_jose (deprecated) ## Usage This library made in fastapi style, so it can be used as standard security features @@ -81,7 +86,7 @@ There it is open and maintained [Pull Request #3305](https://github.com/tiangolo ## Requirements * `fastapi` -* `python-jose[cryptography]` +* `authlib` or `python-jose[cryptography]` (deprecated) ## License -This project is licensed under the terms of the MIT license. \ No newline at end of file +This project is licensed under the terms of the MIT license.