From f57ca909cbd8e7492c762986bca50f1a7c45a855 Mon Sep 17 00:00:00 2001 From: Lily Acadia Gilbert Date: Mon, 18 Nov 2024 10:25:38 -0700 Subject: [PATCH] Fix documentation around identity needing to be a string (#558) Previously we allowed identity to be any data that was JSON serializable, however it turns out that is in violation of the JWT spec, which requires `sub` to be a string. The underlying library that we are using to manage the JWTs (PyJWT) released a new version that is enforcing this behavior, where it didn't before. Because `sub` should be a string per the spec, I've opted to keep that change in this extension, and update the documentation to match this new behavior. --- docs/automatic_user_loading.rst | 2 +- flask_jwt_extended/default_callbacks.py | 2 +- flask_jwt_extended/jwt_manager.py | 7 +++---- flask_jwt_extended/utils.py | 14 ++++++-------- requirements.txt | 2 +- tests/test_view_decorators.py | 11 +++++++++++ 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/docs/automatic_user_loading.rst b/docs/automatic_user_loading.rst index cc981394..785d065e 100644 --- a/docs/automatic_user_loading.rst +++ b/docs/automatic_user_loading.rst @@ -6,7 +6,7 @@ accessing a protected route. We provide a couple callback functions that make this seamless while working with JWTs. The first is :meth:`~flask_jwt_extended.JWTManager.user_identity_loader`, which -will convert any ``User`` object used to create a JWT into a JSON serializable format. +will convert any ``User`` object used to create a JWT into a string. On the flip side, you can use :meth:`~flask_jwt_extended.JWTManager.user_lookup_loader` to automatically load your ``User`` object when a JWT is present in the request. diff --git a/flask_jwt_extended/default_callbacks.py b/flask_jwt_extended/default_callbacks.py index ad8fb2b9..6f10e5da 100644 --- a/flask_jwt_extended/default_callbacks.py +++ b/flask_jwt_extended/default_callbacks.py @@ -41,7 +41,7 @@ def default_jwt_headers_callback(default_headers) -> dict: return {} -def default_user_identity_callback(userdata: Any) -> Any: +def default_user_identity_callback(userdata: Any) -> str: """ By default, we use the passed in object directly as the jwt identity. See this for additional info: diff --git a/flask_jwt_extended/jwt_manager.py b/flask_jwt_extended/jwt_manager.py index 2bc00771..e5f563ed 100644 --- a/flask_jwt_extended/jwt_manager.py +++ b/flask_jwt_extended/jwt_manager.py @@ -434,15 +434,14 @@ def unauthorized_loader(self, callback: Callable) -> Callable: def user_identity_loader(self, callback: Callable) -> Callable: """ This decorator sets the callback function used to convert an identity to - a JSON serializable format when creating JWTs. This is useful for - using objects (such as SQLAlchemy instances) as the identity when - creating your tokens. + a string when creating JWTs. This is useful for using objects (such as + SQLAlchemy instances) as the identity when creating your tokens. The decorated function must take **one** argument. The argument is the identity that was used when creating a JWT. - The decorated function must return JSON serializable data. + The decorated function must return a string. """ self._user_identity_callback = callback return callback diff --git a/flask_jwt_extended/utils.py b/flask_jwt_extended/utils.py index 8ddd2750..d42f582a 100644 --- a/flask_jwt_extended/utils.py +++ b/flask_jwt_extended/utils.py @@ -139,10 +139,9 @@ def create_access_token( Create a new access token. :param identity: - The identity of this token. It can be any data that is json serializable. - You can use :meth:`~flask_jwt_extended.JWTManager.user_identity_loader` - to define a callback function to convert any object passed in into a json - serializable format. + The identity of this token. This must either be a string, or you must have + defined :meth:`~flask_jwt_extended.JWTManager.user_identity_loader` in order + to convert the object you passed in into a string. :param fresh: If this token should be marked as fresh, and can thus access endpoints @@ -192,10 +191,9 @@ def create_refresh_token( Create a new refresh token. :param identity: - The identity of this token. It can be any data that is json serializable. - You can use :meth:`~flask_jwt_extended.JWTManager.user_identity_loader` - to define a callback function to convert any object passed in into a json - serializable format. + The identity of this token. This must either be a string, or you must have + defined :meth:`~flask_jwt_extended.JWTManager.user_identity_loader` in order + to convert the object you passed in into a string. :param expires_delta: A ``datetime.timedelta`` for how long this token should last before it expires. diff --git a/requirements.txt b/requirements.txt index 17e21199..57bb4cf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,5 +2,5 @@ black==23.12.1 cryptography==42.0.4 Flask==3.0.1 pre-commit==3.6.0 -PyJWT==2.8.0 +PyJWT==2.10.0 tox==4.12.1 diff --git a/tests/test_view_decorators.py b/tests/test_view_decorators.py index e2072b8b..4c0274f1 100644 --- a/tests/test_view_decorators.py +++ b/tests/test_view_decorators.py @@ -469,3 +469,14 @@ def custom(): response = test_client.get(url, headers=make_headers(token)) assert response.status_code == 200 assert response.get_json() == {"foo": "bar"} + + +def test_non_string_identity(app): + url = "/protected" + test_client = app.test_client() + with app.test_request_context(): + token = create_access_token(1234) + + response = test_client.get(url, headers=make_headers(token)) + assert response.status_code == 422 + assert response.get_json() == {"msg": "Subject must be a string"}