Skip to content

Commit

Permalink
Fix documentation around identity needing to be a string (#558)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
vimalloc authored Nov 18, 2024
1 parent 85a3750 commit f57ca90
Show file tree
Hide file tree
Showing 6 changed files with 23 additions and 15 deletions.
2 changes: 1 addition & 1 deletion docs/automatic_user_loading.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion flask_jwt_extended/default_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 3 additions & 4 deletions flask_jwt_extended/jwt_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 6 additions & 8 deletions flask_jwt_extended/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 11 additions & 0 deletions tests/test_view_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}

0 comments on commit f57ca90

Please sign in to comment.