feat: jwt_backends - create backend mechanism and add authlib support#41
feat: jwt_backends - create backend mechanism and add authlib support#41k4black merged 7 commits intok4black:mainfrom
Conversation
722c988 to
2d61588
Compare
| places: Optional[Set[str]] = None, | ||
| auto_error: bool = True, | ||
| algorithm: str = jwt.ALGORITHMS.HS256, # type: ignore[attr-defined] | ||
| algorithm: Optional[str] = None, |
There was a problem hiding this comment.
default algorithm is now handled in the jwt backend directly.
| 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 | ||
|
|
There was a problem hiding this comment.
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
| 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] |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
authlib doesn't auto add the alg header 🤷♂️ - manually adding it
| authlib = [ | ||
| "Authlib >=1.3.0" | ||
| ] | ||
| python_jose = [ | ||
| "python-jose[cryptography] >=3.3.0" | ||
| ] |
There was a problem hiding this comment.
useful for people to be able to do:
pip install fastapi-jwt[authlib]or
pip install fastapi-jwt[python_jose]
fastapi_jwt/jwt.py
Outdated
| if AuthlibJWTBackend is not None: | ||
| define_default_jwt_backend(AuthlibJWTBackend) | ||
| elif PythonJoseJWTBackend is not None: | ||
| define_default_jwt_backend(PythonJoseJWTBackend) |
There was a problem hiding this comment.
Authlib is the default if both authlib and python-jose are installed.
| @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 |
There was a problem hiding this comment.
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_clientto 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) |
There was a problem hiding this comment.
and for two tests we need to expose unique_identifiers_database with the client
2d61588 to
49e10c0
Compare
49e10c0 to
06d3132
Compare
|
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? |
|
@rjjanuary, it seems that the maintainer of this project is not very active. 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 |
|
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. |
Codecov ReportAttention: Patch coverage is
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. |
|
@hasB4K Thank you for the great contribution! Regarding your contribution: |
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:
jwt_backendsfolder withPythonJoseJWTBackendandAuthlibJWTBackend.jwt_backends/__init__.py.define_default_jwt_backendfunction, so users can pick their preferred backend. I think most will either stick with the default or set it once.python-josesupport 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,