Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3d26e19
feat: initialize token_family app structure
juanbailon Apr 2, 2025
f17d862
feat: add token_family app initialization and configuration
juanbailon Apr 2, 2025
371c01d
feat: add models for token_family app
juanbailon Apr 2, 2025
db58cdb
migration: add initial migrations for token_family models
juanbailon Apr 2, 2025
771a3ef
feat: add custom exceptions for token family support
juanbailon Apr 4, 2025
2d4ac39
feat: add default settings for token family support
juanbailon Apr 4, 2025
db06924
feat: add token family support and improve blacklist handling
juanbailon Apr 4, 2025
ffc214d
feat: integrate token family support in serializers and improve refre…
juanbailon Apr 4, 2025
0893e65
feat: add TokenFamilyBlacklistView
juanbailon Apr 4, 2025
987ad32
feat: validate token family settings on app startup in TokenFamilyConfig
juanbailon Apr 4, 2025
fe90ef4
refactor: rename TokenFamilyBlacklist model to BlacklistedTokenFamily…
juanbailon Apr 28, 2025
76412d4
migration: rename TokenFamilyBlacklist model to BlacklistedTokenFamily
juanbailon Apr 28, 2025
d0341a8
feat: add management command to delete expired token families from th…
juanbailon Apr 28, 2025
3ad3b1e
test: add tests for token family functionality across views, serializ…
juanbailon May 4, 2025
47451fa
feat: add default blacklist cache related settings
juanbailon May 11, 2025
09215d6
feat: add cache.py for handling caching logic for blacklisted tokens …
juanbailon May 11, 2025
e488fac
feat: integrate caching logic for blacklisted refresh tokens and fami…
juanbailon May 11, 2025
aa17342
test: add tests for blacklist cache functionality
juanbailon May 11, 2025
14da7e7
feat: add admin interface for TokenFamily and BlacklistedTokenFamily …
juanbailon May 15, 2025
2805617
docs: add token family feature documentation
juanbailon May 15, 2025
17eef74
docs: add cache support documentation
juanbailon May 15, 2025
afb06dd
docs: include token_family and cache.py in Sphinx build-docs
juanbailon May 16, 2025
d691bc6
fix: update blacklist() type hint, blacklist_family() type hint and r…
juanbailon May 20, 2025
5c6814d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ build-docs:
*confest* \
tests/* \
rest_framework_simplejwt/token_blacklist/* \
rest_framework_simplejwt/token_family/* \
rest_framework_simplejwt/backends.py \
rest_framework_simplejwt/exceptions.py \
rest_framework_simplejwt/settings.py \
rest_framework_simplejwt/state.py
rest_framework_simplejwt/state.py \
rest_framework_simplejwt/cache.py
$(MAKE) -C docs clean
$(MAKE) -C docs html
$(MAKE) -C docs doctest
Expand Down
64 changes: 64 additions & 0 deletions docs/cache_support.rst
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>``
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def django_configure():
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
"rest_framework_simplejwt.token_family",
),
)

Expand Down
116 changes: 116 additions & 0 deletions docs/family_app.rst
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,
Copy link
Member

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

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
Copy link
Member

Choose a reason for hiding this comment

The 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'),
Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The 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

2 changes: 2 additions & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ Contents
creating_tokens_manually
token_types
blacklist_app
family_app
stateless_user_authentication
cache_support
development_and_contributing
drf_yasg_integration
rest_framework_simplejwt
Expand Down
110 changes: 110 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Some of Simple JWT's behavior can be customized through settings variables in
"BLACKLIST_AFTER_ROTATION": False,
"UPDATE_LAST_LOGIN": False,

"TOKEN_FAMILY_ENABLED": False,
"TOKEN_FAMILY_LIFETIME": timedelta(days=30),
"TOKEN_FAMILY_CHECK_ON_ACCESS": False,
"TOKEN_FAMILY_BLACKLIST_ON_REUSE": False,

"ALGORITHM": "HS256",
"SIGNING_KEY": settings.SECRET_KEY,
"VERIFYING_KEY": "",
Expand All @@ -29,6 +34,14 @@ Some of Simple JWT's behavior can be customized through settings variables in
"JWK_URL": None,
"LEEWAY": 0,

"SJWT_CACHE_NAME": "default",
"CACHE_BLACKLISTED_REFRESH_TOKENS": False,
"CACHE_BLACKLISTED_FAMILIES": False,
"CACHE_TTL_BLACKLISTED_REFRESH_TOKENS": 3600, # time is seconds
"CACHE_TTL_BLACKLISTED_FAMILIES": 3600, # time in seconds
"CACHE_KEY_PREFIX_BLACKLISTED_REFRESH_TOKENS": "sjwt_brt",
"CACHE_KEY_PREFIX_BLACKLISTED_FAMILIES": "sjwt_btf",

"AUTH_HEADER_TYPES": ("Bearer",),
"AUTH_HEADER_NAME": "HTTP_AUTHORIZATION",
"USER_ID_FIELD": "id",
Expand All @@ -43,6 +56,9 @@ Some of Simple JWT's behavior can be customized through settings variables in

"JTI_CLAIM": "jti",

"TOKEN_FAMILY_CLAIM": "family_id",
"TOKEN_FAMILY_EXPIRATION_CLAIM": "family_exp",

"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
Expand All @@ -53,6 +69,7 @@ Some of Simple JWT's behavior can be customized through settings variables in
"TOKEN_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenBlacklistSerializer",
"SLIDING_TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainSlidingSerializer",
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer",
"TOKEN_FAMILY_BLACKLIST_SERIALIZER": "rest_framework_simplejwt.serializers.TokenFamilyBlacklistSerializer",
}

Above, the default values for these settings are shown.
Expand Down Expand Up @@ -105,6 +122,46 @@ login (TokenObtainPairView).
a security vulnerability. If you really want this, throttle the endpoint with
DRF at the very least.

``TOKEN_FAMILY_ENABLED``
----------------------------

When set to ``True``, enables the Token Family tracking system. This allows
refresh tokens to be grouped into families using a shared identifier. By default,
this identifier is only included in refresh tokens, but it can also be added to
access tokens if ``TOKEN_FAMILY_CHECK_ON_ACCESS`` is set to ``True``.
Families can be invalidated as a whole, meaning all tokens associated with the
same family will then be considered invalid.
You need to add ``'rest_framework_simplejwt.token_family',`` to your
``INSTALLED_APPS`` in the settings file to use this setting.

This feature is most effective when used in conjunction with the :doc:`/blacklist_app`

Learn more about :doc:`/family_app`.

``TOKEN_FAMILY_LIFETIME``
----------------------------

A ``datetime.timedelta`` object that specifies how long a token family is considered valid.
This ``timedelta`` value is added to the current UTC time during token generation
to obtain the token's default "family_exp" claim value.
This setting can also be set to ``None``, in which case the "family_exp" claim
will not be included in the token payload and the token family will never expire automatically.
In that case, the only way to invalidate the family is by blacklisting it.

``TOKEN_FAMILY_CHECK_ON_ACCESS``
-------------------------------------

When set to ``True``, the token family claims ("family_id" and "family_exp") will be included
in the access token payload. Requests authenticated with access tokens will then verify
that the token's family is valid, meaning it has not expired and has not been blacklisted.

``TOKEN_FAMILY_BLACKLIST_ON_REUSE``
-------------------------------------

When set to ``True``, any detected reuse of a refresh token will trigger blacklisting of
the entire token family. This invalidates all tokens that share the same family identifier.
This feature can be enhanced when used together with ``BLACKLIST_AFTER_ROTATION`` set to ``True``.

``ALGORITHM``
-------------

Expand Down Expand Up @@ -175,6 +232,47 @@ integer for seconds or a ``datetime.timedelta``. Please reference
https://pyjwt.readthedocs.io/en/latest/usage.html#expiration-time-claim-exp
for more information.

``SJWT_CACHE_NAME``
---------------------

Specifies the Django cache alias to use. This must match a defined entry
in Django's ``CACHES`` setting.

Learn more about :doc:`/cache_support`.

``CACHE_BLACKLISTED_REFRESH_TOKENS``
--------------------------------------

When set to ``True``, enables caching of blacklisted refresh tokens.
Blacklisted refresh token entries will be cached for a period defined
by ``CACHE_TTL_BLACKLISTED_REFRESH_TOKENS``.

``CACHE_BLACKLISTED_FAMILIES``
--------------------------------

When set to ``True``, enables caching of blacklisted token families.
Blacklisted family entries will be cached for a period defined
by ``CACHE_TTL_BLACKLISTED_FAMILIES``.

``CACHE_TTL_BLACKLISTED_REFRESH_TOKENS``
------------------------------------------

Time-to-live (TTL) in seconds for cached refresh token blacklist entries.

``CACHE_TTL_BLACKLISTED_FAMILIES``
------------------------------------

Time-to-live (TTL) in seconds for cached token family blacklist entries.

``CACHE_KEY_PREFIX_BLACKLISTED_REFRESH_TOKENS``
-------------------------------------------------

Prefix used for cache keys when storing blacklisted refresh tokens.

``CACHE_KEY_PREFIX_BLACKLISTED_FAMILIES``
-------------------------------------------

Prefix used for cache keys when storing blacklisted token families.

``AUTH_HEADER_TYPES``
---------------------
Expand Down Expand Up @@ -259,6 +357,18 @@ identifier is used to identify revoked tokens in the blacklist app. It may be
necessary in some cases to use another claim besides the default "jti" claim to
store such a value.

``TOKEN_FAMILY_CLAIM``
---------------------------

The claim name used to store the token family's unique identifier in the token
payload. Defaults to "family_id".

``TOKEN_FAMILY_EXPIRATION_CLAIM``
-------------------------------------

The claim name used to store the token family's expiration date in the token
payload. Defaults to "family_exp".

``TOKEN_USER_CLASS``
--------------------

Expand Down
Loading
Loading