-
Notifications
You must be signed in to change notification settings - Fork 694
feat: implement Token Family system and optional blacklist cache #913
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
base: master
Are you sure you want to change the base?
Changes from all commits
3d26e19
f17d862
371c01d
db58cdb
771a3ef
2d4ac39
db06924
ffc214d
0893e65
987ad32
fe90ef4
76412d4
d0341a8
3ad3b1e
47451fa
09215d6
e488fac
aa17342
14da7e7
2805617
17eef74
afb06dd
d691bc6
5c6814d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
Cache Support | ||
============== | ||
|
||
SimpleJWT provides optional cache support for improving performance. | ||
Currently, caching is available for: | ||
|
||
- Blacklisted refresh tokens | ||
- Blacklisted token families | ||
|
||
To enable caching in SimpleJWT, you must first configure Django's ``CACHES`` setting: | ||
|
||
.. code-block:: python | ||
|
||
CACHES = { | ||
"default": { | ||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache", | ||
"LOCATION": "unique-name", | ||
}, | ||
"redis": { | ||
"BACKEND": "django_redis.cache.RedisCache", | ||
"LOCATION": "redis://127.0.0.1:6379/0", | ||
"OPTIONS": { | ||
"CLIENT_CLASS": "django_redis.client.DefaultClient", | ||
} | ||
} | ||
} | ||
|
||
In this example, two cache backends are defined. You can choose which one to use by | ||
setting the ``SJWT_CACHE_NAME`` option in your SimpleJWT configuration. For this case, | ||
it could be either `"default"` or `"redis"`. | ||
|
||
.. Note:: | ||
Ensure that your Django `CACHES` setting includes a cache matching the alias | ||
defined by `SJWT_CACHE_NAME`. | ||
|
||
|
||
Blacklist Cache | ||
---------------- | ||
|
||
When enabled via the appropriate settings, blacklisted refresh tokens and token families | ||
will be cached. This reduces the number of database queries when verifying whether a token | ||
or family is blacklisted. | ||
|
||
.. code-block:: python | ||
|
||
SIMPLE_JWT = { | ||
... | ||
|
||
"SJWT_CACHE_NAME": "default", | ||
"CACHE_BLACKLISTED_REFRESH_TOKENS": True, | ||
"CACHE_BLACKLISTED_FAMILIES": True, | ||
"CACHE_TTL_BLACKLISTED_REFRESH_TOKENS": 3600, #time in seconds | ||
"CACHE_TTL_BLACKLISTED_FAMILIES": 3600, #time in seconds | ||
"CACHE_KEY_PREFIX_BLACKLISTED_REFRESH_TOKENS": "sjwt_brt", | ||
"CACHE_KEY_PREFIX_BLACKLISTED_FAMILIES": "sjwt_btf", | ||
|
||
... | ||
} | ||
|
||
|
||
Cache keys follow this format: | ||
|
||
- Refresh token: ``sjwt_brt:<jti>`` | ||
- Token family: ``sjwt_btf:<family_id>`` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
.. _family_app: | ||
|
||
Family app | ||
=========== | ||
|
||
The **Token Family** system provides a way to group refresh and access tokens into logical families, | ||
allowing developers to track, manage, and invalidate related tokens as a unit. | ||
|
||
This feature is especially useful in enhancing security and traceability in authentication flows. | ||
Each token family is identified by a unique ``family_id``, which is included in the token payload. | ||
This enables the system to: | ||
|
||
- Detect and respond to refresh token reuse by invalidating the entire token family. | ||
- Revoke all related tokens at once. | ||
- Enforce expiration policies at the family level via a ``family_exp`` claim. | ||
|
||
A new token family is automatically created every time a user successfully obtains a pair of tokens | ||
from the ``TokenObtainPairView`` (i.e., when starting a new session). From that point onward, as long as the user | ||
continues to refresh their tokens, the newly issued access and refresh tokens will retain the same | ||
``family_id`` and ``family_exp`` values. This means all tokens issued as part of a session are | ||
considered to belong to the same token family. | ||
|
||
This session-based grouping allows administrators or systems to treat the token family as the unit of trust. | ||
If suspicious activity is detected, the entire session can be invalidated at once by blacklisting the | ||
associated token family. | ||
|
||
The Token Family system is optional and customizable. It works best when paired with the | ||
:doc:`/blacklist_app`. | ||
|
||
By organizing tokens into families, you gain finer control over user sessions and potential compromises. | ||
For example, when the settings ``BLACKLIST_AFTER_ROTATION`` and ``TOKEN_FAMILY_BLACKLIST_ON_REUSE`` are | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if you don't have the token blacklist app on? |
||
set to ``True``, if a refresh token is stolen and used by both the valid user and the attacker, | ||
the system will detect the reuse of the token and automatically blacklist the associated family. | ||
This invalidates every token that shares the same ``family_id`` as the reused refresh token, effectively | ||
cutting off access without waiting for individual token expiration. | ||
|
||
------- | ||
|
||
Simple JWT includes an app that provides token family functionality. To use | ||
this app, include it in your list of installed apps in ``settings.py``: | ||
|
||
.. code-block:: python | ||
|
||
# Django project settings.py | ||
|
||
... | ||
|
||
INSTALLED_APPS = ( | ||
... | ||
'rest_framework_simplejwt.token_family', | ||
... | ||
) | ||
|
||
Also, make sure to run ``python manage.py migrate`` to run the app's | ||
migrations. | ||
|
||
If the token family app is detected in ``INSTALLED_APPS`` and the setting | ||
``TOKEN_FAMILY_ENABLED`` is set to ``True``, Simple JWT will add a new family | ||
to the family list and will also add two new claims, "family_id" and | ||
"family_exp", to the refresh tokens. It will also check that the | ||
family indicated in the token's payload does not appear in a blacklist of | ||
families before it considers it as valid, and it will also check that the | ||
family expiration date ("family_exp") has not passed; if the family is expired, | ||
then the token will be considered as invalid. | ||
|
||
The Simple JWT family app implements its family and blacklisted family | ||
lists using two models: ``TokenFamily`` and ``BlacklistedTokenFamily``. Model | ||
admins are defined for both of these models. To add a family to the blacklist, | ||
find its corresponding ``TokenFamily`` record in the admin and use the | ||
admin again to create a ``BlacklistedTokenFamily`` record that points to the | ||
``TokenFamily`` record. | ||
|
||
Alternatively, you can blacklist a family by creating a ``FamilyMixin`` | ||
subclass instance and calling the instance's ``blacklist_family`` method: | ||
|
||
.. code-block:: python | ||
|
||
from rest_framework_simplejwt.tokens import RefreshToken | ||
|
||
token = RefreshToken(base64_encoded_token_string) | ||
token.blacklist_family() | ||
|
||
Keep in mind that the ``base64_encoded_token_string`` should already | ||
contain a family ID claim in its payload. | ||
|
||
This will create a unique family and blacklist records for the token's | ||
"family_id" claim or whichever claim is specified by the ``TOKEN_FAMILY_CLAIM`` setting. | ||
|
||
|
||
In a ``urls.py`` file, you can also include a route for ``TokenFamilyBlacklistView``: | ||
|
||
.. code-block:: python | ||
|
||
from rest_framework_simplejwt.views import TokenFamilyBlacklistView | ||
|
||
urlpatterns = [ | ||
... | ||
path('api/token/family/blacklist/', TokenFamilyBlacklistView.as_view(), name='token_family_blacklist'), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would we want to use a different view for token family blacklist? Shouldn't this simply replace the need for the Blacklist url itself? |
||
... | ||
] | ||
|
||
It allows API users to blacklist token families sending them to ``/api/token/family/blacklist/``, for example using curl: | ||
|
||
.. code-block:: bash | ||
|
||
curl \ | ||
-X POST \ | ||
-H "Content-Type: application/json" \ | ||
-d '{"refresh":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTc0NzI0OTU1MywiaWF0IjoxNzQ3MjQ0MTUzLCJqdGkiOiI1YmMzMjlmMjVkODE0OGFhOTY1ODI1YjgwNDQ1ZDQ5OCIsInVzZXJfaWQiOjIsImZhbWlseV9pZCI6ImMyZGYyM2M1YjU1NjRmYjNhNTA3MjFhYzVkMTljNThmIiwiZmFtaWx5X2V4cCI6MTc0NzI0OTE1M30.4oDOmtkgot_W2mXByKuCyJLi6_xeMZtDQJmHIBXZx98"}' \ | ||
http://localhost:8000/api/token/family/blacklist/ | ||
|
||
The family app also provides a management command, ``flushexpiredfamilies``, | ||
which will delete any families from the token family list and family blacklist that have | ||
expired. The command will not affect families that have ``None`` as their expiration. | ||
You should set up a cron job on your server or hosting platform which | ||
runs this command daily. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the recommended pattern for migrating to this new family token system? I noticed that we silently allow tokens without the family, but is the recommended approach to force invalidate all tokens without family? I understand the code silently allows the tokens without the family claim, but it should be noted somewhere here in the docs |
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 reference to the original Auth0 post could be helpful