Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: jwt_backends - create backend mechanism and add authlib support #41

Merged
merged 7 commits into from
May 6, 2024

Conversation

hasB4K
Copy link
Contributor

@hasB4K hasB4K commented Feb 27, 2024

Hello @k4black,

I've implemented the changes discussed in issue #40. The idea is to add a JWTBackend concept to fastapi-jwt for better security and customization. This allows using other authentication libraries besides python-jose, addressing the security concerns mentioned in #40.

Here's what's new in this PR:

  • A jwt_backends folder with PythonJoseJWTBackend and AuthlibJWTBackend.
  • A dynamic import system in jwt_backends/__init__.py.
  • A define_default_jwt_backend function, so users can pick their preferred backend. I think most will either stick with the default or set it once.
  • I've kept python-jose support for now, given its popularity, but we might want to consider a deprecation warning.

I think this approach could also help with:

I hope this PR helps.

Have a great week!
Kind regards,

@hasB4K hasB4K force-pushed the authlib_backend branch 3 times, most recently from 722c988 to 2d61588 Compare February 27, 2024 01:41
@@ -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,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default algorithm is now handled in the jwt backend directly.

Comment on lines -115 to -138
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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was moved in the backend PythonJoseJWTBackend. Since the error strings are now in the backend, a dev could create its own backend to customize the error handling like thhis PR wanted to do: #7

Comment on lines 11 to 15
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]
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A simple SingletonArgs to handle the JWTBackend based on their implementation and the algorithm used. Useful to avoid recreating a lot of backends for nothing.

return "HS256"

def encode(self, to_encode, secret_key) -> str:
token = self.jwt.encode(header={"alg": self.algorithm}, payload=to_encode, key=secret_key)
Copy link
Contributor Author

@hasB4K hasB4K Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authlib doesn't auto add the alg header 🤷‍♂️ - manually adding it

Comment on lines +39 to +44
authlib = [
"Authlib >=1.3.0"
]
python_jose = [
"python-jose[cryptography] >=3.3.0"
]
Copy link
Contributor Author

@hasB4K hasB4K Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useful for people to be able to do:

pip install fastapi-jwt[authlib]

or

pip install fastapi-jwt[python_jose]

Comment on lines 22 to 25
if AuthlibJWTBackend is not None:
define_default_jwt_backend(AuthlibJWTBackend)
elif PythonJoseJWTBackend is not None:
define_default_jwt_backend(PythonJoseJWTBackend)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authlib is the default if both authlib and python-jose are installed.

Comment on lines 191 to 203
@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
Copy link
Contributor Author

@hasB4K hasB4K Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the tests changes are about:

  • make it parametrize to test the two backends
  • mock datetime.now or time.time depending of the backend used (if necessary)
  • create a client depending of the backend used (need to be dynamic and wrapped inside a create_example_client to really test the backends, since a JWTBackend is created when JwtAccessBearer is created

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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and for two tests we need to expose unique_identifiers_database with the client

@rjjanuary
Copy link

I'm looking at moving to JWT's for a few FastAPI projects and was considering building my own framework. Looking through fastapi-jwt, I love the design, however also feel that it's not in our best interest to rely on python-jose. Has there been any consideration of merging this into the project? If so, is there any additional work needed I could assist with?

@hasB4K
Copy link
Contributor Author

hasB4K commented Apr 25, 2024

@rjjanuary, it seems that the maintainer of this project is not very active.
Regarding this PR, I don't think it will be merged, and tbh I would be in favor of a full deprecation of python-jose entirely.

I'm now using fastapi-users which provide an auth framework in which you can select what you want and what you want to change. The project is actively maintained too. They had a similar issue with passlib being unmaintained, and the maintainer of fastapi-users handled the situation very well. See: fastapi-users/fastapi-users#1325

@rjjanuary
Copy link

Thank you @hasB4K, I appreciate the quick response. I'll look deeper into fastapi-users, however at first glance it may be a little overkill for current projects.

Copy link

codecov bot commented May 6, 2024

Codecov Report

Attention: Patch coverage is 98.01980% with 2 lines in your changes are missing coverage. Please review.

Project coverage is 99.08%. Comparing base (19c6038) to head (57b463b).

Files Patch % Lines
fastapi_jwt/jwt.py 94.59% 2 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##              main      #41      +/-   ##
===========================================
- Coverage   100.00%   99.08%   -0.92%     
===========================================
  Files            2        6       +4     
  Lines          158      219      +61     
===========================================
+ Hits           158      217      +59     
- Misses           0        2       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@k4black k4black self-requested a review May 6, 2024 10:17
@k4black k4black changed the title jwt_backends: create backend mechanism and add authlib support feat: jwt_backends - create backend mechanism and add authlib support May 6, 2024
@k4black
Copy link
Owner

k4black commented May 6, 2024

@hasB4K Thank you for the great contribution!
Sorry for not being really active, if you eager to use and update this project i can grant you rights. Please write me email at kdchernyshev at gmail dot com and we can have a short call.

Regarding your contribution:
I'll update backends structure a bit to make jwt.py make a backend choice. For now i'll keep backends feature, but in general I agree with deprecation and removal of jose.

@k4black k4black merged commit 0ea6c04 into k4black:main May 6, 2024
6 of 8 checks passed
@k4black k4black removed their request for review May 6, 2024 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants