-
Notifications
You must be signed in to change notification settings - Fork 19
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
Conversation
722c988
to
2d61588
Compare
@@ -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, |
There was a problem hiding this comment.
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.
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.
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
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.
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) |
There was a problem hiding this comment.
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
authlib = [ | ||
"Authlib >=1.3.0" | ||
] | ||
python_jose = [ | ||
"python-jose[cryptography] >=3.3.0" | ||
] |
There was a problem hiding this comment.
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]
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.
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.
@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.
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) |
There was a problem hiding this comment.
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
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_backends
folder withPythonJoseJWTBackend
andAuthlibJWTBackend
.jwt_backends/__init__.py
.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.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,