diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
new file mode 100644
index 000000000..0869c9e9f
--- /dev/null
+++ b/.github/dependabot.yaml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ assignees:
+ - "djangorestframework-simplejwt"
+ labels:
+ - "dependencies"
diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml
new file mode 100644
index 000000000..8281e90c9
--- /dev/null
+++ b/.github/workflows/i18n.yml
@@ -0,0 +1,48 @@
+name: Update locale files
+
+on:
+ push:
+ branches:
+ - main
+ - master
+
+jobs:
+ locale-updater:
+ permissions:
+ pull-requests: write
+ contents: write
+ if: github.repository == 'jazzband/djangorestframework-simplejwt'
+ name: Locale updater
+ runs-on: ubuntu-latest
+ steps:
+
+ - name: Checkout Repository
+ uses: actions/checkout@v5
+
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ cache: 'pip'
+ cache-dependency-path: setup.py
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get install -y gettext
+ python -m pip install --upgrade pip wheel setuptools
+ pip install -e .[dev]
+
+ - name: Run locale Update Script
+ working-directory:
+ rest_framework_simplejwt
+ run: python ../scripts/i18n_updater.py
+
+ - name: Create Pull Request
+ uses: peter-evans/create-pull-request@v7
+ with:
+ branch: i18n-auto-update
+ title: "[i18n] Update"
+ body: "Updated locale files on trunk"
+ commit-message: "Update locale files"
+ add-paths: rest_framework_simplejwt/locale/**
+ delete-branch: true
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 152109f4b..d8b94e307 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,19 +11,28 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Set up Python
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v6
with:
- python-version: 3.8
+ python-version: '3.9'
- name: Install dependencies
run: |
+ sudo apt-get install -y gettext
python -m pip install -U pip
python -m pip install -U setuptools twine wheel
+ pip install -e .[dev]
+
+ - name: Check locale
+ working-directory: rest_framework_simplejwt
+ run: |
+ echo "Checking if locale files need updating. If they do, cd rest_framework_simplejwt && run python ../scripts/i18n_updater.py"
+ python ../scripts/i18n_updater.py
+ git diff --exit-code
- name: Build package
run: |
@@ -33,7 +42,7 @@ jobs:
- name: Upload packages to Jazzband
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
- uses: pypa/gh-action-pypi-publish@master
+ uses: pypa/gh-action-pypi-publish@release/v1
with:
user: jazzband
password: ${{ secrets.JAZZBAND_RELEASE_KEY }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 2c57cba90..49ea9370e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -15,36 +15,30 @@ jobs:
fail-fast: false
max-parallel: 5
matrix:
- python-version: ['3.7', '3.8', '3.9', '3.10']
- django-version: ['2.2', '3.1', '3.2', 'main']
- drf-version: ['3.10', '3.11', '3.12']
+ python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
+ django-version: ['4.2', '5.0', '5.1', '5.2']
+ drf-version: ['3.14', '3.15']
exclude:
- - python-version: '3.7'
- django-version: 'main'
- - python-version: '3.8'
- django-version: 'main'
- - django-version: '3.1'
- drf-version: '3.10'
- - python-version: '3.10'
- django-version: '2.2'
- - python-version: '3.10'
- django-version: '3.1'
+ - drf-version: '3.14'
+ django-version: '5.0'
+ - drf-version: '3.14'
+ django-version: '5.1'
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v2
+ uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Get pip cache dir
id: pip-cache
run: |
- echo "::set-output name=dir::$(pip cache dir)"
+ echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT
- name: Cache
- uses: actions/cache@v2
+ uses: actions/cache@v4
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:
@@ -65,6 +59,6 @@ jobs:
DRF: ${{ matrix.drf-version }}
- name: Upload coverage
- uses: codecov/codecov-action@v1
+ uses: codecov/codecov-action@v5
with:
name: Python ${{ matrix.python-version }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index f692d7f62..bb2c53fba 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,24 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: 'v4.0.1'
+ rev: 'v6.0.0'
hooks:
- id: check-merge-conflict
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.13.2
+ hooks:
+ - id: ruff
+ types_or: [ python, pyi ]
+ args: [--select, I, --fix,]
+ files: "^tests/|^rest_framework_simplejwt/"
+ - id: ruff-format
+ types_or: [python, pyi]
+ files: "^tests/|^rest_framework_simplejwt/"
- repo: https://github.com/asottile/yesqa
- rev: v1.2.3
+ rev: v1.5.0
hooks:
- id: yesqa
-- repo: https://github.com/pycqa/isort
- rev: '5.9.3'
- hooks:
- - id: isort
- args: ["--profile", "black"]
-- repo: https://github.com/psf/black
- rev: '21.9b0'
- hooks:
- - id: black
- language_version: python3 # Should be a command that runs python3.6+
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: 'v4.0.1'
+ rev: 'v6.0.0'
hooks:
- id: end-of-file-fixer
exclude: >-
@@ -48,12 +48,12 @@ repos:
- id: detect-private-key
exclude: ^tests/
- repo: https://github.com/asottile/pyupgrade
- rev: 'v2.28.0'
+ rev: 'v3.20.0'
hooks:
- id: pyupgrade
- args: ['--py37-plus', '--keep-mock']
+ args: ['--py39-plus', '--keep-mock']
-- repo: git://github.com/Lucas-C/pre-commit-hooks-markup
+- repo: https://github.com/Lucas-C/pre-commit-hooks-markup
rev: v1.0.1
hooks:
- id: rst-linter
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 8896b7862..843783c61 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -2,15 +2,17 @@ version: 2
# Set the version of Python and other tools you might need
build:
- os: ubuntu-20.04
+ os: ubuntu-22.04
tools:
- python: "3.9"
+ python: "3.12"
python:
install:
# Install dependencies from setup.py .
- - method: setuptools
+ - method: pip
path: .
+ extra_requirements:
+ - dev
# Build documentation in the docs/ directory with Sphinx
sphinx:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6877bcb96..3504ffd2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,137 @@
-## Unreleased
+## [Unreleased]
+
+### Changed
+- **BREAKING:** In `serializers.py`, when a user linked to a token is missing or deleted, the code now raises `AuthenticationFailed("no_active_account")` instead of allowing `DoesNotExist` to propagate.
+ - Response changed from **404 Not Found** → **401 Unauthorized**.
+ - Improves security by not leaking whether a user/token exists.
+ - Follows RFC 7235, where authentication failures should return 401.
+ - Clearer for clients: signals an auth issue instead of suggesting the endpoint is missing.
+
+
+## 5.5.1
+
+Missing Migration for rest_framework_simplejwt.token_blacklist app. A previously missing migration (0013_blacklist) has now been added. This issue arose because the migration file was mistakenly not generated earlier. This migration was never part of an official release, but users following the latest master branch may have encountered it.
+
+**Notes for Users**
+If you previously ran makemigrations in production and have a 0013_blacklist migration in your django_migrations table, follow these steps before upgrading:
+
+1. Roll back to the last known migration:
+```bash
+python manage.py migrate rest_framework_simplejwt.token_blacklist 0012
+```
+2. Upgrade djangorestframework-simplejwt to the latest version.
+3. Apply the migrations correctly:
+```bash
+python manage.py migrate
+```
+**Important**: If other migrations depend on 0013_blacklist, be cautious when removing it. You may need to adjust or regenerate dependent migrations to ensure database integrity.
+
+* fix: add missing migration for token_blacklist app by @juanbailon in https://github.com/jazzband/djangorestframework-simplejwt/pull/894
+* :globe_with_meridians: Fix typos and improve clarity in es_AR translations by @fabianfalon in https://github.com/jazzband/djangorestframework-simplejwt/pull/876
+* docs: Add warning in docs for `for_user` usage by @vgrozdanic in https://github.com/jazzband/djangorestframework-simplejwt/pull/872
+* feat: log warning if token is being created for inactive user by @vgrozdanic in https://github.com/jazzband/djangorestframework-simplejwt/pull/873
+* ref: full tracebacks on exceptions by @vgrozdanic in https://github.com/jazzband/djangorestframework-simplejwt/pull/870
+* #858 New i18n messages by @Cloves23 in https://github.com/jazzband/djangorestframework-simplejwt/pull/879
+* Repair the type annotations in the TokenViewBase class. by @triplepoint in https://github.com/jazzband/djangorestframework-simplejwt/pull/880
+* fix: Token.outstand forces users to install blacklist app by @Andrew-Chen-Wang in https://github.com/jazzband/djangorestframework-simplejwt/pull/884
+* fix: PytestConfigWarning Unknown config option: python_paths by @vgrozdanic in https://github.com/jazzband/djangorestframework-simplejwt/pull/886
+* fix: Do not copy `iat` claim from refresh token by @vgrozdanic in https://github.com/jazzband/djangorestframework-simplejwt/pull/888
+* fix: add missing migration for token_blacklist app by @juanbailon in https://github.com/jazzband/djangorestframework-simplejwt/pull/894
+* Update Persian translations (fa, fa_IR) for Django application by @mahdirahimi1999 in https://github.com/jazzband/djangorestframework-simplejwt/pull/897
+* fix: always stringify user_id claim in https://github.com/jazzband/djangorestframework-simplejwt/pull/887
+
+## 5.5.0
+* Cap PyJWT version to <2.10.0 to avoid incompatibility with subject claim type requirement by @grayver in https://github.com/jazzband/djangorestframework-simplejwt/pull/843
+* Add specific "token expired" exceptions by @vainu-arto in https://github.com/jazzband/djangorestframework-simplejwt/pull/830
+* Fix user_id type mismatch when user claim is not pk by @jdg-journeyfront in https://github.com/jazzband/djangorestframework-simplejwt/pull/851
+* Caching signing key by @henryfool91 in https://github.com/jazzband/djangorestframework-simplejwt/pull/859
+* Adds new refresh tokens to OutstandingToken db. by @thecarpetjasp in https://github.com/jazzband/djangorestframework-simplejwt/pull/866
+
+## 5.4.0
+* Changed string formatting in views by @Egor-oop in https://github.com/jazzband/djangorestframework-simplejwt/pull/750
+* Enhance BlacklistMixin with Generic Type for Accurate Type Inference by @Dresdn in https://github.com/jazzband/djangorestframework-simplejwt/pull/768
+* Improve type of `Token.for_user` to allow subclasses by @sterliakov in https://github.com/jazzband/djangorestframework-simplejwt/pull/776
+* Fix the `Null` value of the `OutstandingToken` of the `BlacklistMixin.blacklist` by @JaeHyuckSa in https://github.com/jazzband/djangorestframework-simplejwt/pull/806
+* Fix: Disable refresh token for inactive user. by @ajay09 in https://github.com/jazzband/djangorestframework-simplejwt/pull/814
+* Add option to allow inactive user authentication and token generation by @zxkeyy in https://github.com/jazzband/djangorestframework-simplejwt/pull/834
+* Drop Django <4.2, DRF <3.14, Python <3.9 by @Andrew-Chen-Wang in https://github.com/jazzband/djangorestframework-simplejwt/pull/839
+ * Note, many deprecated versions are only officially not supported but probably still work fine.
+* Add support for EdDSA and other algorithms in jwt.algorithms.requires_cryptography (#822) https://github.com/jazzband/djangorestframework-simplejwt/pull/823
+
+## 5.3.1
+
+## What's Changed
+* Remove EOL Python, Django and DRF version support by @KOliver94 in [#754](https://github.com/jazzband/djangorestframework-simplejwt/pull/754)
+* Declare support for type checking (closes #664) by @PedroPerpetua in [#760](https://github.com/jazzband/djangorestframework-simplejwt/pull/760)
+* Remove usages of deprecated datetime.utcnow() and datetime.utcfromtimestamp() in [#765](https://github.com/jazzband/djangorestframework-simplejwt/pull/765)
+
+#### Translation Updates:
+* Update Korean translations by @TGoddessana in https://github.com/jazzband/djangorestframework-simplejwt/pull/753
+
+## 5.3.0
+
+#### Notable Changes:
+* Added support for Python 3.11 by @joshuadavidthomas [#636](https://github.com/jazzband/djangorestframework-simplejwt/pull/636)
+* Added support for Django 4.2 by @johnthagen [#711](https://github.com/jazzband/djangorestframework-simplejwt/pull/711)
+* Added support for DRF 3.14 by @Andrew-Chen-Wang [#623](https://github.com/jazzband/djangorestframework-simplejwt/pull/623)
+* Added Inlang to facilitate community translations by @jannesblobel [#662](https://github.com/jazzband/djangorestframework-simplejwt/pull/662)
+* Revoke access token if user password is changed by @mahdirahimi1999 [#719](https://github.com/jazzband/djangorestframework-simplejwt/pull/719)
+* Added type hints by @abczzz13 [#683](https://github.com/jazzband/djangorestframework-simplejwt/pull/683)
+* Improved testing by @kiraware [#688](https://github.com/jazzband/djangorestframework-simplejwt/pull/688)
+* Removed support for Django 2.2 by @kiraware [#688](https://github.com/jazzband/djangorestframework-simplejwt/pull/688)
+
+#### Documentation:
+* Added write_only=True to TokenBlacklistSerializer's refresh field for better doc generation by @Yaser-Amiri [#699](https://github.com/jazzband/djangorestframework-simplejwt/pull/699)
+* Updated docs on serializer customization by @2ykwang [#668](https://github.com/jazzband/djangorestframework-simplejwt/pull/668)
+
+#### Translation Updates:
+* Updated translations for Persian (fa) language by @mahdirahimi1999 [#723](https://github.com/jazzband/djangorestframework-simplejwt/pull/723) and https://github.com/jazzband/djangorestframework-simplejwt/pull/708
+* Updated translations for Indonesian (id) language by @kiraware [#685](https://github.com/jazzband/djangorestframework-simplejwt/pull/685)
+* Added Arabic language translations by @iamjazzar [#690](https://github.com/jazzband/djangorestframework-simplejwt/pull/690)
+* Added Hebrew language translations by @elam91 [#679](https://github.com/jazzband/djangorestframework-simplejwt/pull/679)
+* Added Slovenian language translations by @banDeveloper [#645](https://github.com/jazzband/djangorestframework-simplejwt/pull/645)
+
+## Version 5.2.2
+
+Major security release
+
+* Revert #605 [#629](https://github.com/jazzband/djangorestframework-simplejwt/pull/629)
+* Fix typo in blacklist_app.rst by @cbscsm [#593](https://github.com/jazzband/djangorestframework-simplejwt/pull/593)
+
+## Version 5.2.1
+
+* Add Swedish translations by @PasinduPrabhashitha [#579](https://github.com/jazzband/djangorestframework-simplejwt/pull/579)
+* Fixed issue #543 by @armenak-baburyan [#586](https://github.com/jazzband/djangorestframework-simplejwt/pull/586)
+* Fix uncaught exception with JWK by @jerr0328 [#600](https://github.com/jazzband/djangorestframework-simplejwt/pull/600)
+* Test on Django 4.1 by @2ykwang [#604](https://github.com/jazzband/djangorestframework-simplejwt/pull/604)
+
+## Version 5.2.0
+
+* Remove the JWTTokenUserAuthentication from the Experimental Features #546 by @byrpatrick [#547](https://github.com/jazzband/djangorestframework-simplejwt/pull/547)
+* Fix leeway type error by @2ykwang [#554](https://github.com/jazzband/djangorestframework-simplejwt/pull/554)
+* Add info on TokenBlacklistView to the docs by @inti7ary [#558](https://github.com/jazzband/djangorestframework-simplejwt/pull/558)
+* Update JWTStatelessUserAuthentication docs by @2ykwang [#561](https://github.com/jazzband/djangorestframework-simplejwt/pull/561)
+* Allow none jti claim token type claim by @denniskeends [#567](https://github.com/jazzband/djangorestframework-simplejwt/pull/567)
+* Allow customizing token JSON encoding by @vainu-arto [#568](https://github.com/jazzband/djangorestframework-simplejwt/pull/568)
+
+## Version 5.1.0
+
+* Add back support for PyJWT 1.7.1 ([#536](https://github.com/jazzband/djangorestframework-simplejwt/pull/536))
+* Make the token serializer configurable ([#521](https://github.com/jazzband/djangorestframework-simplejwt/pull/521))
+* Simplify using custom token classes in serializers ([#517](https://github.com/jazzband/djangorestframework-simplejwt/pull/517))
+* Fix default_app_config deprecation ([#415](https://github.com/jazzband/djangorestframework-simplejwt/pull/415))
+* Add missing integration instructions for drf-yasg ([#505](https://github.com/jazzband/djangorestframework-simplejwt/pull/505))
+* Add blacklist view to log out users ([#306](https://github.com/jazzband/djangorestframework-simplejwt/pull/306))
+* Set default verifying key to empty str ([#487](https://github.com/jazzband/djangorestframework-simplejwt/pull/487))
+* Add docs about TOKEN_USER_CLASS ([#455](https://github.com/jazzband/djangorestframework-simplejwt/pull/440))
+
+Meta:
+* Add auto locale updater ([#456](https://github.com/jazzband/djangorestframework-simplejwt/pull/456))
+
+Translations:
+
+* Added Korean translations ([#501](https://github.com/jazzband/djangorestframework-simplejwt/pull/501))
+* Added Turkish translations ([#508](https://github.com/jazzband/djangorestframework-simplejwt/pull/508))
## Version 5.0.0
@@ -12,8 +145,7 @@
* Updated import list ([#459](https://github.com/jazzband/djangorestframework-simplejwt/pull/459))
* Repair generation of OpenAPI with Spectacular ([#452](https://github.com/jazzband/djangorestframework-simplejwt/pull/452))
* Add "iat" claim to token ([#192](https://github.com/jazzband/djangorestframework-simplejwt/pull/192))
-* Add blacklist view to log out users ([#306](https://github.com/jazzband/djangorestframework-simplejwt/pull/306))
-* updated import list in docs ([#459](https://github.com/jazzband/djangorestframework-simplejwt/pull/459))
+* Add blacklist view to log out users ([#306](https://github.com/jazzband/djangorestframework-simplejwt/pull/306))
## Version 4.8.0
@@ -36,6 +168,7 @@
* Fix invalid syntax in docs for `INSTALLED_APPS` ([#416](https://github.com/jazzband/django-rest-framework-simplejwt/pull/416))
Translations:
+
* Added Dutch translations ([#422](https://github.com/jazzband/django-rest-framework-simplejwt/pull/422))
* Added Ukrainian translations ([#423](https://github.com/jazzband/django-rest-framework-simplejwt/pull/423))
* Added Simplified Chinese translations ([#427](https://github.com/jazzband/django-rest-framework-simplejwt/pull/427))
diff --git a/MANIFEST.in b/MANIFEST.in
index 6b427dfc7..1501b7de9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,6 @@
include README.rst
include LICENSE.txt
+include rest_framework_simplejwt/py.typed
recursive-include rest_framework_simplejwt/locale *.mo
recursive-include rest_framework_simplejwt/locale *.po
recursive-exclude * __pycache__
diff --git a/Makefile b/Makefile
index b095e0e9c..83b7a02e5 100644
--- a/Makefile
+++ b/Makefile
@@ -38,7 +38,6 @@ build-docs:
tests/* \
rest_framework_simplejwt/token_blacklist/* \
rest_framework_simplejwt/backends.py \
- rest_framework_simplejwt/compat.py \
rest_framework_simplejwt/exceptions.py \
rest_framework_simplejwt/settings.py \
rest_framework_simplejwt/state.py
diff --git a/README.rst b/README.rst
index 0dd78628b..733f5e97b 100644
--- a/README.rst
+++ b/README.rst
@@ -27,8 +27,8 @@ Framework `__.
For full documentation, visit `django-rest-framework-simplejwt.readthedocs.io
`__.
-Looking for Maintainers
------------------------
-For more information, see `here
-`__.
+Translations
+------------
+
+Contribute translations directly with PRs or via inlang https://inlang.com/editor/github.com/jazzband/djangorestframework-simplejwt
diff --git a/codecov.yml b/codecov.yml
index d7436ab05..cca09ecbe 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -1,7 +1,11 @@
coverage:
status:
- project: false
- patch: false
+ project:
+ default:
+ informational: true
+ patch:
+ default:
+ informational: true
changes: false
comment: off
diff --git a/docs/blacklist_app.rst b/docs/blacklist_app.rst
index 709c9ceb9..5b65a7ba0 100644
--- a/docs/blacklist_app.rst
+++ b/docs/blacklist_app.rst
@@ -46,6 +46,28 @@ subclass instance and calling the instance's ``blacklist`` method:
This will create unique outstanding token and blacklist records for the token's
"jti" claim or whichever claim is specified by the ``JTI_CLAIM`` setting.
+In a ``urls.py`` file, you can also include a route for ``TokenBlacklistView``:
+
+.. code-block:: python
+
+ from rest_framework_simplejwt.views import TokenBlacklistView
+
+ urlpatterns = [
+ ...
+ path('api/token/blacklist/', TokenBlacklistView.as_view(), name='token_blacklist'),
+ ...
+ ]
+
+It allows API users to blacklist tokens sending them to ``/api/token/blacklist/``, for example using curl:
+
+.. code-block:: bash
+
+ curl \
+ -X POST \
+ -H "Content-Type: application/json" \
+ -d '{"refresh":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoicmVmcmVzaCIsImV4cCI6MTY1MDI5NTEwOCwiaWF0IjoxNjUwMjA4NzA4LCJqdGkiOiJhYTY3ZDUxNzkwMGY0MTEyYTY5NTE0MTNmNWQ4NDk4NCIsInVzZXJfaWQiOjF9.tcj1_OcO1BRDfFyw4miHD7mqFdWKxmP7BJDRmxwCzrg"}' \
+ http://localhost:8000/api/token/blacklist/
+
The blacklist app also provides a management command, ``flushexpiredtokens``,
which will delete any tokens from the outstanding list and blacklist that have
expired. You should set up a cron job on your server or hosting platform which
diff --git a/docs/creating_tokens_manually.rst b/docs/creating_tokens_manually.rst
index 5c42b0cee..8dc5caa04 100644
--- a/docs/creating_tokens_manually.rst
+++ b/docs/creating_tokens_manually.rst
@@ -6,11 +6,19 @@ Creating tokens manually
Sometimes, you may wish to manually create a token for a user. This could be
done as follows:
+.. warning::
+ The ``for_user`` method does not check if the user is active. If you need to verify the user's status,
+ this check needs to be done before creating the tokens.
+
.. code-block:: python
from rest_framework_simplejwt.tokens import RefreshToken
+ from rest_framework_simplejwt.exceptions import AuthenticationFailed
def get_tokens_for_user(user):
+ if not user.is_active:
+ raise AuthenticationFailed("User is not active")
+
refresh = RefreshToken.for_user(user)
return {
diff --git a/docs/customizing_token_claims.rst b/docs/customizing_token_claims.rst
index a763f0f4b..ad693386b 100644
--- a/docs/customizing_token_claims.rst
+++ b/docs/customizing_token_claims.rst
@@ -25,8 +25,16 @@ generated by the ``TokenObtainPairView``:
return token
- class MyTokenObtainPairView(TokenObtainPairView):
- serializer_class = MyTokenObtainPairSerializer
+.. code-block:: python
+
+ # Django project settings.py
+ ...
+
+ SIMPLE_JWT = {
+ # It will work instead of the default serializer(TokenObtainPairSerializer).
+ "TOKEN_OBTAIN_SERIALIZER": "my_app.serializers.MyTokenObtainPairSerializer",
+ # ...
+ }
Note that the example above will cause the customized claims to be present in
both refresh *and* access tokens which are generated by the view. This follows
diff --git a/docs/development_and_contributing.rst b/docs/development_and_contributing.rst
index 015a37995..b85724224 100644
--- a/docs/development_and_contributing.rst
+++ b/docs/development_and_contributing.rst
@@ -32,10 +32,8 @@ directory:
.. code-block:: bash
pyenv install 3.9.x
- pyenv install 3.8.x
cat > .python-version <
+ plugin.readResources({ ...args, ...env, pluginConfig }),
+ writeResources: (args) =>
+ plugin.writeResources({ ...args, ...env, pluginConfig }),
+ };
+}
diff --git a/pytest.ini b/pytest.ini
index f5fdc0ec9..85e8da7f9 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,6 +1,6 @@
[pytest]
addopts= -v --showlocals --durations 10
-python_paths= .
+pythonpath= .
xfail_strict=true
[pytest-watch]
diff --git a/rest_framework_simplejwt/__init__.py b/rest_framework_simplejwt/__init__.py
index 6929b410c..b001cf5d3 100644
--- a/rest_framework_simplejwt/__init__.py
+++ b/rest_framework_simplejwt/__init__.py
@@ -1,7 +1,7 @@
-from pkg_resources import DistributionNotFound, get_distribution
+from importlib.metadata import PackageNotFoundError, version
try:
- __version__ = get_distribution("djangorestframework_simplejwt").version
-except DistributionNotFound:
+ __version__ = version("djangorestframework_simplejwt")
+except PackageNotFoundError:
# package is not installed
__version__ = None
diff --git a/rest_framework_simplejwt/authentication.py b/rest_framework_simplejwt/authentication.py
index f7ad03537..706196600 100644
--- a/rest_framework_simplejwt/authentication.py
+++ b/rest_framework_simplejwt/authentication.py
@@ -1,16 +1,27 @@
+from typing import Optional, TypeVar
+
from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AbstractBaseUser
from django.utils.translation import gettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING, authentication
+from rest_framework.request import Request
from .exceptions import AuthenticationFailed, InvalidToken, TokenError
+from .models import TokenUser
from .settings import api_settings
+from .tokens import Token
+from .utils import get_md5_hash_password
AUTH_HEADER_TYPES = api_settings.AUTH_HEADER_TYPES
if not isinstance(api_settings.AUTH_HEADER_TYPES, (list, tuple)):
AUTH_HEADER_TYPES = (AUTH_HEADER_TYPES,)
-AUTH_HEADER_TYPE_BYTES = {h.encode(HTTP_HEADER_ENCODING) for h in AUTH_HEADER_TYPES}
+AUTH_HEADER_TYPE_BYTES: set[bytes] = {
+ h.encode(HTTP_HEADER_ENCODING) for h in AUTH_HEADER_TYPES
+}
+
+AuthUser = TypeVar("AuthUser", AbstractBaseUser, TokenUser)
class JWTAuthentication(authentication.BaseAuthentication):
@@ -22,11 +33,15 @@ class JWTAuthentication(authentication.BaseAuthentication):
www_authenticate_realm = "api"
media_type = "application/json"
- def __init__(self, *args, **kwargs):
+ default_error_messages = {
+ "password_changed": _("The user's password has been changed."),
+ }
+
+ def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.user_model = get_user_model()
- def authenticate(self, request):
+ def authenticate(self, request: Request) -> Optional[tuple[AuthUser, Token]]:
header = self.get_header(request)
if header is None:
return None
@@ -39,13 +54,13 @@ def authenticate(self, request):
return self.get_user(validated_token), validated_token
- def authenticate_header(self, request):
+ def authenticate_header(self, request: Request) -> str:
return '{} realm="{}"'.format(
AUTH_HEADER_TYPES[0],
self.www_authenticate_realm,
)
- def get_header(self, request):
+ def get_header(self, request: Request) -> bytes:
"""
Extracts the header containing the JSON web token from the given
request.
@@ -58,7 +73,7 @@ def get_header(self, request):
return header
- def get_raw_token(self, header):
+ def get_raw_token(self, header: bytes) -> Optional[bytes]:
"""
Extracts an unvalidated JSON web token from the given "Authorization"
header value.
@@ -81,7 +96,7 @@ def get_raw_token(self, header):
return parts[1]
- def get_validated_token(self, raw_token):
+ def get_validated_token(self, raw_token: bytes) -> Token:
"""
Validates an encoded JSON web token and returns a validated token
wrapper object.
@@ -106,28 +121,46 @@ def get_validated_token(self, raw_token):
}
)
- def get_user(self, validated_token):
+ def get_user(self, validated_token: Token) -> AuthUser:
"""
Attempts to find and return a user using the given validated token.
"""
try:
user_id = validated_token[api_settings.USER_ID_CLAIM]
- except KeyError:
- raise InvalidToken(_("Token contained no recognizable user identification"))
+ except KeyError as e:
+ raise InvalidToken(
+ _("Token contained no recognizable user identification")
+ ) from e
try:
user = self.user_model.objects.get(**{api_settings.USER_ID_FIELD: user_id})
- except self.user_model.DoesNotExist:
- raise AuthenticationFailed(_("User not found"), code="user_not_found")
+ except self.user_model.DoesNotExist as e:
+ raise AuthenticationFailed(
+ _("User not found"), code="user_not_found"
+ ) from e
- if not user.is_active:
+ if api_settings.CHECK_USER_IS_ACTIVE and not user.is_active:
raise AuthenticationFailed(_("User is inactive"), code="user_inactive")
+ if api_settings.CHECK_REVOKE_TOKEN:
+ if validated_token.get(
+ api_settings.REVOKE_TOKEN_CLAIM
+ ) != get_md5_hash_password(user.password):
+ raise AuthenticationFailed(
+ self.default_error_messages["password_changed"],
+ code="password_changed",
+ )
+
return user
-class JWTTokenUserAuthentication(JWTAuthentication):
- def get_user(self, validated_token):
+class JWTStatelessUserAuthentication(JWTAuthentication):
+ """
+ An authentication plugin that authenticates requests through a JSON web
+ token provided in a request header without performing a database lookup to obtain a user instance.
+ """
+
+ def get_user(self, validated_token: Token) -> AuthUser:
"""
Returns a stateless user object which is backed by the given validated
token.
@@ -140,7 +173,10 @@ def get_user(self, validated_token):
return api_settings.TOKEN_USER_CLASS(validated_token)
-def default_user_authentication_rule(user):
+JWTTokenUserAuthentication = JWTStatelessUserAuthentication
+
+
+def default_user_authentication_rule(user: Optional[AuthUser]) -> bool:
# Prior to Django 1.10, inactive users could be authenticated with the
# default `ModelBackend`. As of Django 1.10, the `ModelBackend`
# prevents inactive users from authenticating. App designers can still
@@ -148,4 +184,6 @@ def default_user_authentication_rule(user):
# `AllowAllUsersModelBackend`. However, we explicitly prevent inactive
# users from authenticating to enforce a reasonable policy and provide
# sensible backwards compatibility with older Django versions.
- return user is not None and user.is_active
+ return user is not None and (
+ not api_settings.CHECK_USER_IS_ACTIVE or user.is_active
+ )
diff --git a/rest_framework_simplejwt/backends.py b/rest_framework_simplejwt/backends.py
index 40ceee486..0967fc188 100644
--- a/rest_framework_simplejwt/backends.py
+++ b/rest_framework_simplejwt/backends.py
@@ -1,31 +1,54 @@
+import json
+from collections.abc import Iterable
+from datetime import timedelta
+from functools import cached_property
+from typing import Any, Optional, Union
+
import jwt
from django.utils.translation import gettext_lazy as _
-from jwt import InvalidAlgorithmError, InvalidTokenError, PyJWKClient, algorithms
+from jwt import (
+ ExpiredSignatureError,
+ InvalidAlgorithmError,
+ InvalidTokenError,
+ algorithms,
+)
-from .exceptions import TokenBackendError
+from .exceptions import TokenBackendError, TokenBackendExpiredToken
+from .tokens import Token
from .utils import format_lazy
-ALLOWED_ALGORITHMS = (
+try:
+ from jwt import PyJWKClient, PyJWKClientError
+
+ JWK_CLIENT_AVAILABLE = True
+except ImportError:
+ JWK_CLIENT_AVAILABLE = False
+
+ALLOWED_ALGORITHMS = {
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
-)
+ "ES256",
+ "ES384",
+ "ES512",
+}.union(algorithms.requires_cryptography)
class TokenBackend:
def __init__(
self,
- algorithm,
- signing_key=None,
- verifying_key="",
- audience=None,
- issuer=None,
- jwk_url: str = None,
- leeway=0,
- ):
+ algorithm: str,
+ signing_key: Optional[str] = None,
+ verifying_key: str = "",
+ audience: Union[str, Iterable, None] = None,
+ issuer: Optional[str] = None,
+ jwk_url: Optional[str] = None,
+ leeway: Union[float, int, timedelta, None] = None,
+ json_encoder: Optional[type[json.JSONEncoder]] = None,
+ ) -> None:
self._validate_algorithm(algorithm)
self.algorithm = algorithm
@@ -34,10 +57,30 @@ def __init__(
self.audience = audience
self.issuer = issuer
- self.jwks_client = PyJWKClient(jwk_url) if jwk_url else None
+ if JWK_CLIENT_AVAILABLE:
+ self.jwks_client = PyJWKClient(jwk_url) if jwk_url else None
+ else:
+ self.jwks_client = None
+
self.leeway = leeway
+ self.json_encoder = json_encoder
+
+ @cached_property
+ def prepared_signing_key(self) -> Any:
+ return self._prepare_key(self.signing_key)
- def _validate_algorithm(self, algorithm):
+ @cached_property
+ def prepared_verifying_key(self) -> Any:
+ return self._prepare_key(self.verifying_key)
+
+ def _prepare_key(self, key: Optional[str]) -> Any:
+ # Support for PyJWT 1.7.1 or empty signing key
+ if key is None or not getattr(jwt.PyJWS, "get_algorithm_by_name", None):
+ return key
+ jws_alg = jwt.PyJWS().get_algorithm_by_name(self.algorithm)
+ return jws_alg.prepare_key(key)
+
+ def _validate_algorithm(self, algorithm: str) -> None:
"""
Ensure that the nominated algorithm is recognized, and that cryptography is installed for those
algorithms that require it
@@ -54,16 +97,36 @@ def _validate_algorithm(self, algorithm):
)
)
- def get_verifying_key(self, token):
+ def get_leeway(self) -> timedelta:
+ if self.leeway is None:
+ return timedelta(seconds=0)
+ elif isinstance(self.leeway, (int, float)):
+ return timedelta(seconds=self.leeway)
+ elif isinstance(self.leeway, timedelta):
+ return self.leeway
+ else:
+ raise TokenBackendError(
+ format_lazy(
+ _(
+ "Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+ ),
+ type(self.leeway),
+ )
+ )
+
+ def get_verifying_key(self, token: Token) -> Any:
if self.algorithm.startswith("HS"):
- return self.signing_key
+ return self.prepared_signing_key
if self.jwks_client:
- return self.jwks_client.get_signing_key_from_jwt(token).key
+ try:
+ return self.jwks_client.get_signing_key_from_jwt(token).key
+ except PyJWKClientError as e:
+ raise TokenBackendError(_("Token is invalid")) from e
- return self.verifying_key
+ return self.prepared_verifying_key
- def encode(self, payload):
+ def encode(self, payload: dict[str, Any]) -> str:
"""
Returns an encoded token for the given payload dictionary.
"""
@@ -73,14 +136,19 @@ def encode(self, payload):
if self.issuer is not None:
jwt_payload["iss"] = self.issuer
- token = jwt.encode(jwt_payload, self.signing_key, algorithm=self.algorithm)
+ token = jwt.encode(
+ jwt_payload,
+ self.prepared_signing_key,
+ algorithm=self.algorithm,
+ json_encoder=self.json_encoder,
+ )
if isinstance(token, bytes):
# For PyJWT <= 1.7.1
return token.decode("utf-8")
# For PyJWT >= 2.0.0a1
return token
- def decode(self, token, verify=True):
+ def decode(self, token: Token, verify: bool = True) -> dict[str, Any]:
"""
Performs a validation of the given token and returns its payload
dictionary.
@@ -95,13 +163,15 @@ def decode(self, token, verify=True):
algorithms=[self.algorithm],
audience=self.audience,
issuer=self.issuer,
- leeway=self.leeway,
+ leeway=self.get_leeway(),
options={
"verify_aud": self.audience is not None,
"verify_signature": verify,
},
)
- except InvalidAlgorithmError as ex:
- raise TokenBackendError(_("Invalid algorithm specified")) from ex
- except InvalidTokenError:
- raise TokenBackendError(_("Token is invalid or expired"))
+ except InvalidAlgorithmError as e:
+ raise TokenBackendError(_("Invalid algorithm specified")) from e
+ except ExpiredSignatureError as e:
+ raise TokenBackendExpiredToken(_("Token is expired")) from e
+ except InvalidTokenError as e:
+ raise TokenBackendError(_("Token is invalid")) from e
diff --git a/rest_framework_simplejwt/compat.py b/rest_framework_simplejwt/compat.py
deleted file mode 100644
index 463d472c5..000000000
--- a/rest_framework_simplejwt/compat.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import warnings
-
-try:
- from django.urls import reverse, reverse_lazy
-except ImportError:
- from django.core.urlresolvers import reverse, reverse_lazy # NOQA
-
-
-class RemovedInDjango20Warning(DeprecationWarning):
- pass
-
-
-class CallableBool: # pragma: no cover
- """
- An boolean-like object that is also callable for backwards compatibility.
- """
-
- do_not_call_in_templates = True
-
- def __init__(self, value):
- self.value = value
-
- def __bool__(self):
- return self.value
-
- def __call__(self):
- warnings.warn(
- "Using user.is_authenticated() and user.is_anonymous() as a method "
- "is deprecated. Remove the parentheses to use it as an attribute.",
- RemovedInDjango20Warning,
- stacklevel=2,
- )
- return self.value
-
- def __nonzero__(self): # Python 2 compatibility
- return self.value
-
- def __repr__(self):
- return "CallableBool(%r)" % self.value
-
- def __eq__(self, other):
- return self.value == other
-
- def __ne__(self, other):
- return self.value != other
-
- def __or__(self, other):
- return bool(self.value or other)
-
- def __hash__(self):
- return hash(self.value)
-
-
-CallableFalse = CallableBool(False)
-CallableTrue = CallableBool(True)
diff --git a/rest_framework_simplejwt/exceptions.py b/rest_framework_simplejwt/exceptions.py
index 47d03238c..8cc58e976 100644
--- a/rest_framework_simplejwt/exceptions.py
+++ b/rest_framework_simplejwt/exceptions.py
@@ -1,3 +1,5 @@
+from typing import Any, Optional, Union
+
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions, status
@@ -6,12 +8,27 @@ class TokenError(Exception):
pass
+class ExpiredTokenError(TokenError):
+ pass
+
+
class TokenBackendError(Exception):
pass
+class TokenBackendExpiredToken(TokenBackendError):
+ pass
+
+
class DetailDictMixin:
- def __init__(self, detail=None, code=None):
+ default_detail: str
+ default_code: str
+
+ def __init__(
+ self,
+ detail: Union[dict[str, Any], str, None] = None,
+ code: Optional[str] = None,
+ ) -> None:
"""
Builds a detail dictionary for the error to give more information to API
users.
@@ -26,7 +43,7 @@ def __init__(self, detail=None, code=None):
if code is not None:
detail_dict["code"] = code
- super().__init__(detail_dict)
+ super().__init__(detail_dict) # type: ignore
class AuthenticationFailed(DetailDictMixin, exceptions.AuthenticationFailed):
diff --git a/rest_framework_simplejwt/locale/ar/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/ar/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..374342e24
Binary files /dev/null and b/rest_framework_simplejwt/locale/ar/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/ar/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/ar/LC_MESSAGES/django.po
new file mode 100644
index 000000000..d65afec93
--- /dev/null
+++ b/rest_framework_simplejwt/locale/ar/LC_MESSAGES/django.po
@@ -0,0 +1,163 @@
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , 2019.
+msgid ""
+msgstr ""
+"Project-Id-Version: djangorestframework_simplejwt\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
+"Last-Translator: Ahmed Jazzar \n"
+"Language: ar\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 "
+"&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
+
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
+msgid "Authorization header must contain two space-delimited values"
+msgstr "يجب أن يحتوي رأس التفويض على قيمتين مفصولتين بمسافات"
+
+#: authentication.py:119
+msgid "Given token not valid for any token type"
+msgstr "تأشيرة المرور غير صالحة لأي نوع من أنواع التأشيرات"
+
+#: authentication.py:132 authentication.py:171
+msgid "Token contained no recognizable user identification"
+msgstr "لا تحتوي تأشيرة المرور على هوية مستخدم يمكن التعرف عليها"
+
+#: authentication.py:139
+msgid "User not found"
+msgstr "لم يتم العثور على المستخدم"
+
+#: authentication.py:143
+msgid "User is inactive"
+msgstr "الحساب غير مفعل"
+
+#: backends.py:90
+msgid "Unrecognized algorithm type '{}'"
+msgstr "نوع الخوارزمية غير معروف '{}'"
+
+#: backends.py:96
+msgid "You must have cryptography installed to use {}."
+msgstr "يجب أن يكون لديك تشفير مثبت لاستخدام {}."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+"نوع غير معروف '{}'. يجب أن تكون 'leeway' عددًا صحيحًا أو عددًا نسبيًا أو فرق وقت."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "تأشيرة المرور غير صالحة أو منتهية الصلاحية"
+
+#: backends.py:173
+msgid "Invalid algorithm specified"
+msgstr "تم تحديد خوارزمية غير صالحة"
+
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "تأشيرة المرور غير صالحة أو منتهية الصلاحية"
+
+#: exceptions.py:55
+msgid "Token is invalid or expired"
+msgstr "تأشيرة المرور غير صالحة أو منتهية الصلاحية"
+
+#: serializers.py:37
+msgid "No active account found with the given credentials"
+msgstr "لم يتم العثور على حساب نشط للبيانات المقدمة"
+
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "لم يتم العثور على حساب نشط للبيانات المقدمة"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "التأشيرة مدرجة في القائمة السوداء"
+
+#: settings.py:78
+msgid ""
+"The '{}' setting has been removed. Please refer to '{}' for available "
+"settings."
+msgstr ""
+"تمت إزالة الإعداد '{}'. يرجى الرجوع إلى '{}' للتعرف على الإعدادات المتاحة."
+
+#: token_blacklist/admin.py:79
+msgid "jti"
+msgstr "jti"
+
+#: token_blacklist/admin.py:85
+msgid "user"
+msgstr "المستخدم"
+
+#: token_blacklist/admin.py:91
+msgid "created at"
+msgstr "أنشئت في"
+
+#: token_blacklist/admin.py:97
+msgid "expires at"
+msgstr "تنتهي في"
+
+#: token_blacklist/apps.py:7
+msgid "Token Blacklist"
+msgstr "قائمة تأشيرات المرور السوداء"
+
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
+msgid "Cannot create token with no type or lifetime"
+msgstr "لا يمكن إنشاء تأشيرة مرور بدون نوع أو عمر"
+
+#: tokens.py:127
+msgid "Token has no id"
+msgstr "التأشيرة ليس لها معرف"
+
+#: tokens.py:139
+msgid "Token has no type"
+msgstr "التأشيرة ليس لها نوع"
+
+#: tokens.py:142
+msgid "Token has wrong type"
+msgstr "التأشيرة لها نوع خاطئ"
+
+#: tokens.py:201
+msgid "Token has no '{}' claim"
+msgstr "التأشيرة ليس لديها مطالبة '{}'"
+
+#: tokens.py:206
+msgid "Token '{}' claim has expired"
+msgstr "انتهى عمر المطالبة بالتأشيرة '{}'"
diff --git a/rest_framework_simplejwt/locale/cs/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/cs/LC_MESSAGES/django.po
index 16fd7ad4c..620775fc0 100644
--- a/rest_framework_simplejwt/locale/cs/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/cs/LC_MESSAGES/django.po
@@ -4,72 +4,103 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"Last-Translator: Lukáš Rod \n"
"Language: cs\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr "Autorizační hlavička musí obsahovat dvě hodnoty oddělené mezerou"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Daný token není validní pro žádný typ tokenu"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Token neobsahoval žádnou rozpoznatelnou identifikaci uživatele"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Uživatel nenalezen"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Uživatel není aktivní"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Nerozpoznaný typ algoritmu '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr ""
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Token není validní nebo vypršela jeho platnost"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr ""
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Token není validní nebo vypršela jeho platnost"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Token není validní nebo vypršela jeho platnost"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Žádný aktivní účet s danými údaji nebyl nalezen"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Žádný aktivní účet s danými údaji nebyl nalezen"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token je na černé listině"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
msgstr "Nastavení '{}' bylo odstraněno. Dostupná nastavení jsou v '{}'"
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "uživatel"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "vytvořený v"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "platí do"
@@ -77,30 +108,52 @@ msgstr "platí do"
msgid "Token Blacklist"
msgstr "Token Blacklist"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Nelze vytvořit token bez zadaného typu nebo životnosti"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Token nemá žádný identifikátor"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Token nemá žádný typ"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Token má špatný typ"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Token nemá žádnou hodnotu '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Hodnota tokenu '{}' vypršela"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Token je na černé listině"
diff --git a/rest_framework_simplejwt/locale/de_CH/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/de/LC_MESSAGES/django.mo
similarity index 100%
rename from rest_framework_simplejwt/locale/de_CH/LC_MESSAGES/django.mo
rename to rest_framework_simplejwt/locale/de/LC_MESSAGES/django.mo
diff --git a/rest_framework_simplejwt/locale/de_CH/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/de/LC_MESSAGES/django.po
similarity index 55%
rename from rest_framework_simplejwt/locale/de_CH/LC_MESSAGES/django.po
rename to rest_framework_simplejwt/locale/de/LC_MESSAGES/django.po
index 5b70353ff..60e4df2cf 100644
--- a/rest_framework_simplejwt/locale/de_CH/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/de/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"Last-Translator: rene \n"
"Language: de_CH\n"
"MIME-Version: 1.0\n"
@@ -12,48 +12,79 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"Der Authorizationheader muss zwei leerzeichen-getrennte Werte enthalten"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Der Token ist für keinen Tokentyp gültig"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Token enthält keine erkennbare Benutzeridentifikation"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Benutzer nicht gefunden"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Inaktiver Benutzer"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Unerkannter Algorithmustyp '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr ""
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Ungültiger oder abgelaufener Token"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr ""
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Ungültiger oder abgelaufener Token"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Ungültiger oder abgelaufener Token"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Kein aktiver Account mit diesen Zugangsdaten gefunden"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Kein aktiver Account mit diesen Zugangsdaten gefunden"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token steht auf der Blacklist"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -61,19 +92,19 @@ msgstr ""
"Die Einstellung '{}' wurde gelöscht. Bitte beachte '{}' für verfügbare "
"Einstellungen."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "Benutzer"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "erstellt am"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "läuft ab am"
@@ -81,30 +112,52 @@ msgstr "läuft ab am"
msgid "Token Blacklist"
msgstr "Token Blacklist"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Ein Token ohne Typ oder Lebensdauer kann nicht erstellt werden"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Token hat keine Id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Token hat keinen Typ"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Token hat den falschen Typ"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Token hat kein '{}' Recht"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Das Tokenrecht '{}' ist abgelaufen"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Token steht auf der Blacklist"
diff --git a/rest_framework_simplejwt/locale/es/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/es/LC_MESSAGES/django.mo
index a25a73868..803232a04 100644
Binary files a/rest_framework_simplejwt/locale/es/LC_MESSAGES/django.mo and b/rest_framework_simplejwt/locale/es/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/es/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/es/LC_MESSAGES/django.po
index 6e647115c..20d54f556 100644
--- a/rest_framework_simplejwt/locale/es/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/es/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"Last-Translator: zeack \n"
"Language: es\n"
"MIME-Version: 1.0\n"
@@ -12,48 +12,79 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"El encabezado 'Authorization' debe contener valores delimitados por espacios"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "El token dado no es valido para ningun tipo de token"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "El token no contenía identificación de usuario reconocible"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Usuario no encontrado"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "El usuario está inactivo"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Tipo de algoritmo no reconocido '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr "Debe tener criptografía instalada para usar {}."
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "El token es inválido o ha expirado"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "Algoritmo especificado no válido"
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "El token es inválido o ha expirado"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "El token es inválido o ha expirado"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
-msgstr "La combination de credenciales no tiene una cuenta activa"
+msgstr "La combinación de credenciales no tiene una cuenta activa"
+
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "La combinación de credenciales no tiene una cuenta activa"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "El token está en lista negra"
-#: settings.py:63
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -61,19 +92,19 @@ msgstr ""
"La configuración '{}' fue removida. Por favor, refiérase a '{}' para "
"consultar las disponibles."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "usuario"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "creado en"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "expira en"
@@ -81,30 +112,52 @@ msgstr "expira en"
msgid "Token Blacklist"
msgstr "Lista negra de Tokens"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "No se puede crear un token sin tipo o de tan larga vida"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "El token no tiene id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "El token no tiene tipo"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "El token tiene un tipo incorrecto"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "El token no tiene el privilegio '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "El privilegio '{}' del token ha expirado"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "El token está en lista negra"
diff --git a/rest_framework_simplejwt/locale/es_AR/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/es_AR/LC_MESSAGES/django.mo
index 968365d9e..0dc8aa18f 100644
Binary files a/rest_framework_simplejwt/locale/es_AR/LC_MESSAGES/django.mo and b/rest_framework_simplejwt/locale/es_AR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/es_AR/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/es_AR/LC_MESSAGES/django.po
index ebec2a3de..c0a193227 100644
--- a/rest_framework_simplejwt/locale/es_AR/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/es_AR/LC_MESSAGES/django.po
@@ -3,63 +3,93 @@
#
# Translators:
# Ariel Torti , 2020.
-#
+# Fabian Falón , 2024.
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
-"Last-Translator: Ariel Torti \n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
+"Last-Translator: Fabian Falon \n"
"Language: es_AR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr "La contraseña del usuario ha sido cambiada."
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"El header de autorización debe contener dos valores delimitados por espacio"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
-msgstr "El token dado no es válido para ningún tipo de token"
+msgstr "El token proporcionado no es válido para ningún tipo."
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
-msgstr "El token no contiene ninguna identificación de usuario"
+msgstr "El token no contiene ninguna identificación de usuario válida."
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
-msgstr "Usuario no encontrado"
+msgstr "No se encontró el usuario."
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "El usuario está inactivo"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Tipo de algoritmo no reconocido '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr ""
-#: backends.py:74
-msgid "Invalid algorithm specified"
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
msgstr ""
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "El token es inválido o ha expirado"
+
+#: backends.py:173
+msgid "Invalid algorithm specified"
+msgstr "Se especificó un algoritmo no válido."
+
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "El token es inválido o ha expirado"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "El token es inválido o ha expirado"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
-msgstr ""
-"No se encontró una cuenta de usuario activa para las credenciales dadas"
+msgstr "No se encontró una cuenta activa con las credenciales proporcionadas."
+
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "No se encontró una cuenta activa para el token proporcionado"
-#: settings.py:63
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "El token ha sido revocado"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -67,19 +97,19 @@ msgstr ""
"La configuración '{}' fue removida. Por favor, refiérase a '{}' para "
"consultar las configuraciones disponibles."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "usuario"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "creado en"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "expira en"
@@ -87,30 +117,52 @@ msgstr "expira en"
msgid "Token Blacklist"
msgstr "Lista negra de Tokens"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "No es posible crear un token sin tipo o tiempo de vida"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "El token no tiene id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "El token no tiene tipo"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "El token tiene un tipo incorrecto"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "El token no tiene el privilegio '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "El privilegio '{}' del token ha expirado"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "El token está en la lista negra"
diff --git a/rest_framework_simplejwt/locale/es_CL/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/es_CL/LC_MESSAGES/django.po
index 2869d6b0d..abde07514 100644
--- a/rest_framework_simplejwt/locale/es_CL/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/es_CL/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"Last-Translator: Alfonso Pola \n"
"Language: es_CL\n"
"MIME-Version: 1.0\n"
@@ -12,49 +12,81 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"El header de autorización debe contener dos valores delimitados por espacio"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "El token provisto no es válido para ningún tipo de token"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "El token no contiene identificación de usuario reconocible"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Usuario no encontrado"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "El usuario está inactivo"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Tipo de algoritmo no reconocido '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr ""
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Token inválido o expirado"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr ""
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Token inválido o expirado"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Token inválido o expirado"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr ""
"No se encontró una cuenta de usuario activa para las credenciales provistas"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr ""
+"No se encontró una cuenta de usuario activa para las credenciales provistas"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token está en la blacklist"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -62,19 +94,19 @@ msgstr ""
"La configuración '{}' fue removida. Por favor, refiérase a '{}' para "
"configuraciones disponibles."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "Usuario"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "creado en"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "expira en"
@@ -82,30 +114,52 @@ msgstr "expira en"
msgid "Token Blacklist"
msgstr "Token Blacklist"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "No es posible crear un token sin tipo o tiempo de vida"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Token no tiene id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Token no tiene tipo"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Token tiene tipo erróneo"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Token no tiene privilegio '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "El provilegio '{}' del token está expirado"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Token está en la blacklist"
diff --git a/rest_framework_simplejwt/locale/fa/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/fa/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..145b393f9
Binary files /dev/null and b/rest_framework_simplejwt/locale/fa/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/fa/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/fa/LC_MESSAGES/django.po
new file mode 100644
index 000000000..5cd24c1da
--- /dev/null
+++ b/rest_framework_simplejwt/locale/fa/LC_MESSAGES/django.po
@@ -0,0 +1,158 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , 2023.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-03-27 15:19+0330\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: Mahdi Rahimi \n"
+"Language: fa\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr "رمز عبور کاربر تغییر کرده است"
+
+#: authentication.py:93
+msgid "Authorization header must contain two space-delimited values"
+msgstr "هدر اعتبارسنجی باید شامل دو مقدار جدا شده با فاصله باشد"
+
+#: authentication.py:119
+msgid "Given token not valid for any token type"
+msgstr "توکن داده شده برای هیچ نوع توکنی معتبر نمیباشد"
+
+#: authentication.py:132 authentication.py:171
+msgid "Token contained no recognizable user identification"
+msgstr "توکن شامل هیچ شناسه قابل تشخیصی از کاربر نیست"
+
+#: authentication.py:139
+msgid "User not found"
+msgstr "کاربر یافت نشد"
+
+#: authentication.py:143
+msgid "User is inactive"
+msgstr "کاربر غیرفعال است"
+
+#: backends.py:90
+msgid "Unrecognized algorithm type '{}'"
+msgstr "نوع الگوریتم ناشناخته '{}'"
+
+#: backends.py:96
+msgid "You must have cryptography installed to use {}."
+msgstr "برای استفاده از {} باید رمزنگاری را نصب کرده باشید."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr "نوع ناشناخته '{}'، 'leeway' باید از نوع int، float یا timedelta باشد."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+msgid "Token is invalid"
+msgstr "توکن نامعتبر است"
+
+#: backends.py:173
+msgid "Invalid algorithm specified"
+msgstr "الگوریتم نامعتبر مشخص شده است"
+
+#: backends.py:175 tokens.py:67
+msgid "Token is expired"
+msgstr "توکن منقضی شده است"
+
+#: exceptions.py:55
+msgid "Token is invalid or expired"
+msgstr "توکن نامعتبر است یا منقضی شده است"
+
+#: serializers.py:37
+msgid "No active account found with the given credentials"
+msgstr "هیچ اکانت فعالی برای اطلاعات داده شده یافت نشد"
+
+#: serializers.py:113 serializers.py:183
+msgid "No active account found for the given token."
+msgstr "هیچ اکانت فعالی برای این توکن یافت نشد"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "توکن در لیست سیاه قرار گرفته است"
+
+#: settings.py:78
+msgid ""
+"The '{}' setting has been removed. Please refer to '{}' for available "
+"settings."
+msgstr "تنظیمات '{}' حذف شده است. لطفاً به '{}' برای تنظیمات موجود مراجعه کنید."
+
+#: token_blacklist/admin.py:79
+msgid "jti"
+msgstr "شناسه توکن (jti)"
+
+#: token_blacklist/admin.py:85
+msgid "user"
+msgstr "کاربر"
+
+#: token_blacklist/admin.py:91
+msgid "created at"
+msgstr "زمان ایجاد"
+
+#: token_blacklist/admin.py:97
+msgid "expires at"
+msgstr "زمان انقضا"
+
+#: token_blacklist/apps.py:7
+msgid "Token Blacklist"
+msgstr "لیست سیاه توکن"
+
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr "توکن برجسته"
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr "توکنهای برجسته"
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr "توکن برای %(user)s (%(jti)s)"
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr "توکن لیست سیاه شده"
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr "توکنهای لیست سیاه شده"
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr "توکن لیست سیاه شده برای %(user)s"
+
+#: tokens.py:53
+msgid "Cannot create token with no type or lifetime"
+msgstr "توکن بدون نوع یا طول عمر قابل ایجاد نیست"
+
+#: tokens.py:127
+msgid "Token has no id"
+msgstr "توکن id ندارد"
+
+#: tokens.py:139
+msgid "Token has no type"
+msgstr "توکن نوعی ندارد"
+
+#: tokens.py:142
+msgid "Token has wrong type"
+msgstr "توکن دارای نوع نادرستی است"
+
+#: tokens.py:201
+msgid "Token has no '{}' claim"
+msgstr "توکن دارای '{}' claim نمیباشد"
+
+#: tokens.py:206
+msgid "Token '{}' claim has expired"
+msgstr "'{}' claim توکن منقضی شده"
diff --git a/rest_framework_simplejwt/locale/fa_IR/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/fa_IR/LC_MESSAGES/django.mo
index 5af197097..e76a81308 100644
Binary files a/rest_framework_simplejwt/locale/fa_IR/LC_MESSAGES/django.mo and b/rest_framework_simplejwt/locale/fa_IR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/fa_IR/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/fa_IR/LC_MESSAGES/django.po
index f39b6b162..adb04da96 100644
--- a/rest_framework_simplejwt/locale/fa_IR/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/fa_IR/LC_MESSAGES/django.po
@@ -4,72 +4,97 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
-"Last-Translator: Hirad Daneshvar \n"
+"POT-Creation-Date: 2025-03-27 15:19+0330\n"
+"Last-Translator: Mahdi Rahimi \n"
"Language: fa_IR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr "رمز عبور کاربر تغییر کرده است"
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr "هدر اعتبارسنجی باید شامل دو مقدار جدا شده با فاصله باشد"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "توکن داده شده برای هیچ نوع توکنی معتبر نمیباشد"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "توکن شامل هیچ شناسه قابل تشخیصی از کاربر نیست"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "کاربر یافت نشد"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "کاربر غیرفعال است"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "نوع الگوریتم ناشناخته '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
-msgstr ""
+msgstr "برای استفاده از {} باید رمزنگاری را نصب کرده باشید."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr "نوع ناشناخته '{}'، 'leeway' باید از نوع int، float یا timedelta باشد."
-#: backends.py:74
+#: backends.py:125 backends.py:177 tokens.py:69
+msgid "Token is invalid"
+msgstr "توکن نامعتبر است"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
-msgstr ""
+msgstr "الگوریتم نامعتبر مشخص شده است"
+
+#: backends.py:175 tokens.py:67
+msgid "Token is expired"
+msgstr "توکن منقضی شده است"
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "توکن نامعتبر است یا منقضی شده است"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "هیچ اکانت فعالی برای اطلاعات داده شده یافت نشد"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+msgid "No active account found for the given token."
+msgstr "هیچ اکانت فعالی برای توکن داده شده یافت نشد"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "توکن به لیست سیاه رفته است"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
msgstr "تنظیمات '{}' حذف شده است. لطفا به '{}' برای تنظیمات موجود مراجعه کنید."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
-msgstr "jti"
+msgstr "شناسه توکن (jti)"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "کاربر"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "زمان ایجاد"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "زمان انقضا"
@@ -77,30 +102,52 @@ msgstr "زمان انقضا"
msgid "Token Blacklist"
msgstr "لیست سیاه توکن"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr "توکن برجسته"
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr "توکنهای برجسته"
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr "توکن برای %(user)s (%(jti)s)"
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr "توکن در لیست سیاه"
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr "توکنهای لیست سیاه"
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr "توکن لیست سیاه برای %(user)s"
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "توکن بدون هیچ نوع و طول عمر قابل ساخت نیست"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "توکن id ندارد"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "توکن نوع ندارد"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "توکن نوع اشتباهی دارد"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "توکن دارای '{}' claim نمیباشد"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "'{}' claim توکن منقضی شده"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "توکن به لیست سیاه رفته است"
diff --git a/rest_framework_simplejwt/locale/fr/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/fr/LC_MESSAGES/django.po
index fd2cd1c16..f750ea26a 100644
--- a/rest_framework_simplejwt/locale/fr/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/fr/LC_MESSAGES/django.po
@@ -4,56 +4,87 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"Last-Translator: Stéphane Malta e Sousa \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"L'en-tête 'Authorization' doit contenir deux valeurs séparées par des espaces"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Le type de jeton fourni n'est pas valide"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr ""
"Le jeton ne contient aucune information permettant d'identifier l'utilisateur"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "L'utilisateur n'a pas été trouvé"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "L'utilisateur est désactivé"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Type d'algorithme non reconnu '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr "Vous devez installer cryptography afin d'utiliser {}."
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Le jeton est invalide ou expiré"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "L'algorithme spécifié est invalide"
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Le jeton est invalide ou expiré"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Le jeton est invalide ou expiré"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Aucun compte actif n'a été trouvé avec les identifiants fournis"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Aucun compte actif n'a été trouvé avec les identifiants fournis"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Le jeton a été banni"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -61,19 +92,19 @@ msgstr ""
"Le paramètre '{}' a été supprimé. Voir '{}' pour la liste des paramètres "
"disponibles."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "Utilisateur"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "Créé le"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "Expire le"
@@ -81,30 +112,52 @@ msgstr "Expire le"
msgid "Token Blacklist"
msgstr "Liste des jetons bannis"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Ne peut pas créer de jeton sans type ni durée de vie"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Le jeton n'a pas d'id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Le jeton n'a pas de type"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Le jeton a un type erroné"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Le jeton n'a pas le privilège '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Le privilège '{}' du jeton a expiré"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Le jeton a été banni"
diff --git a/rest_framework_simplejwt/locale/he_IL/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/he_IL/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..ca0741422
Binary files /dev/null and b/rest_framework_simplejwt/locale/he_IL/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/he_IL/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/he_IL/LC_MESSAGES/django.po
new file mode 100644
index 000000000..3d07c2903
--- /dev/null
+++ b/rest_framework_simplejwt/locale/he_IL/LC_MESSAGES/django.po
@@ -0,0 +1,164 @@
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: djangorestframework_simplejwt\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
+"PO-Revision-Date: \n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: he\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : n>10 && n%10==0 ? "
+"2 : 3);\n"
+"X-Generator: Poedit 3.2.2\n"
+
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
+msgid "Authorization header must contain two space-delimited values"
+msgstr "Authorization Header חייבת להכיל שני ערכים מופרדים ברווח"
+
+#: authentication.py:119
+msgid "Given token not valid for any token type"
+msgstr "המזהה הנתון אינו תקף עבור אף סיווג"
+
+#: authentication.py:132 authentication.py:171
+msgid "Token contained no recognizable user identification"
+msgstr "המזהה לא הכיל זיהוי משתמש שניתן לזהות"
+
+#: authentication.py:139
+msgid "User not found"
+msgstr "משתמש לא נמצא"
+
+#: authentication.py:143
+msgid "User is inactive"
+msgstr "המשתמש אינו פעיל"
+
+#: backends.py:90
+msgid "Unrecognized algorithm type '{}'"
+msgstr "סוג אלגוריתם לא מזוהה '{}'"
+
+#: backends.py:96
+msgid "You must have cryptography installed to use {}."
+msgstr "עליך להתקין קריפטוגרפיה כדי להשתמש ב-{}."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr "סוג לא מזוהה '{}', 'leeway' חייב להיות מסוג int, float או timedelta."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "המזהה אינו חוקי או שפג תוקפו"
+
+#: backends.py:173
+msgid "Invalid algorithm specified"
+msgstr "צוין אלגוריתם לא חוקי"
+
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "המזהה אינו חוקי או שפג תוקפו"
+
+#: exceptions.py:55
+msgid "Token is invalid or expired"
+msgstr "המזהה אינו חוקי או שפג תוקפו"
+
+#: serializers.py:37
+msgid "No active account found with the given credentials"
+msgstr "לא נמצא חשבון עם פרטי זיהוי אלו"
+
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "לא נמצא חשבון עם פרטי זיהוי אלו"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "המזהה ברשימה השחורה."
+
+#: settings.py:78
+msgid ""
+"The '{}' setting has been removed. Please refer to '{}' for available "
+"settings."
+msgstr "ההגדרה '{}' הוסרה. בבקשה קראו כאן: '{}' בשביל לראות הגדרות אפשרויות"
+
+#: token_blacklist/admin.py:79
+msgid "jti"
+msgstr "jti"
+
+#: token_blacklist/admin.py:85
+msgid "user"
+msgstr "משתמש"
+
+#: token_blacklist/admin.py:91
+msgid "created at"
+msgstr "נוצר בשעה"
+
+#: token_blacklist/admin.py:97
+msgid "expires at"
+msgstr "פג תוקף בשעה"
+
+#: token_blacklist/apps.py:7
+msgid "Token Blacklist"
+msgstr "רשימה שחורה של מזהים"
+
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
+msgid "Cannot create token with no type or lifetime"
+msgstr "לא ניתן ליצור מזהה ללא סוג או אורך חיים"
+
+#: tokens.py:127
+msgid "Token has no id"
+msgstr "למזהה אין מספר זיהוי"
+
+#: tokens.py:139
+msgid "Token has no type"
+msgstr "למזהה אין סוג"
+
+#: tokens.py:142
+msgid "Token has wrong type"
+msgstr "למזהה יש סוג לא נכון"
+
+#: tokens.py:201
+msgid "Token has no '{}' claim"
+msgstr "למזהה אין '{}'"
+
+#: tokens.py:206
+msgid "Token '{}' claim has expired"
+msgstr "מזהה '{}' פג תוקף"
diff --git a/rest_framework_simplejwt/locale/id_ID/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/id_ID/LC_MESSAGES/django.mo
index 33f3f669f..4a56a2ee9 100644
Binary files a/rest_framework_simplejwt/locale/id_ID/LC_MESSAGES/django.mo and b/rest_framework_simplejwt/locale/id_ID/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/id_ID/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/id_ID/LC_MESSAGES/django.po
index bab7fc973..bc8cdb9d8 100644
--- a/rest_framework_simplejwt/locale/id_ID/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/id_ID/LC_MESSAGES/django.po
@@ -1,58 +1,94 @@
# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR , 2020.
+#
+# Translators:
+# , 2020
+# Kira , 2023
msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
-"Last-Translator: oon arfiandwi \n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
+"PO-Revision-Date: 2023-03-09 08:14+0000\n"
+"Last-Translator: Kira \n"
"Language: id_ID\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr "Header otorisasi harus berisi dua nilai yang dipisahkan spasi"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Token yang diberikan tidak valid untuk semua jenis token"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Token tidak mengandung identifikasi pengguna yang dapat dikenali"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Pengguna tidak ditemukan"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Pengguna tidak aktif"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Jenis algoritma tidak dikenal '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
-msgstr "Anda harus memasang kriptografi untuk menggunakan {}."
+msgstr "Anda harus memasang cryptography untuk menggunakan {}."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+"Tipe '{}' tidak dikenali, 'leeway' harus bertipe int, float, atau timedelta."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Token tidak valid atau kedaluwarsa"
-#: backends.py:74
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "Algoritma yang ditentukan tidak valid"
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Token tidak valid atau kedaluwarsa"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Token tidak valid atau kedaluwarsa"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Tidak ada akun aktif yang ditemukan dengan kredensial yang diberikan"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Tidak ada akun aktif yang ditemukan dengan kredensial yang diberikan"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token masuk daftar hitam"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -60,19 +96,19 @@ msgstr ""
"Setelan '{}' telah dihapus. Silakan merujuk ke '{}' untuk pengaturan yang "
"tersedia."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "pengguna"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "created at"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "kedaluwarsa pada"
@@ -80,30 +116,52 @@ msgstr "kedaluwarsa pada"
msgid "Token Blacklist"
msgstr "Daftar Hitam Token"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Tidak dapat membuat token tanpa tipe atau masa pakai"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Token tidak memiliki id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Token tidak memiliki tipe"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Jenis token salah"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Token tidak memiliki klaim '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Klaim token '{}' telah kedaluwarsa"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Token masuk daftar hitam"
diff --git a/rest_framework_simplejwt/locale/it_IT/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/it_IT/LC_MESSAGES/django.po
index ec5212282..ce8e30739 100644
--- a/rest_framework_simplejwt/locale/it_IT/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/it_IT/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"PO-Revision-Date: \n"
"Last-Translator: Adriano Di Dio <95adriano@gmail.com>\n"
"Language-Team: \n"
@@ -15,48 +15,79 @@ msgstr ""
"X-Generator: Poedit 2.0.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"L'header di autorizzazione deve contenere due valori delimitati da uno spazio"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Il token dato non è valido per qualsiasi tipo di token"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Il token non conteneva nessuna informazione riconoscibile dell'utente"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Utente non trovato"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Utente non attivo"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Algoritmo di tipo '{}' non riconosciuto"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr "Devi avere installato cryptography per usare '{}'."
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Il token non è valido o è scaduto"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "L'algoritmo specificato non è valido"
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Il token non è valido o è scaduto"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Il token non è valido o è scaduto"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Nessun account attivo trovato con queste credenziali"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Nessun account attivo trovato con queste credenziali"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Il token è stato inserito nella blacklist"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -64,19 +95,19 @@ msgstr ""
"L'impostazione '{}' è stata rimossa. Per favore utilizza '{}' per "
"visualizzare le impostazioni valide."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "utente"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "creato il"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "scade il"
@@ -84,30 +115,52 @@ msgstr "scade il"
msgid "Token Blacklist"
msgstr "Blacklist dei token"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Impossibile creare un token senza tipo o durata"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Il token non ha un id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Il token non ha un tipo"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Il token ha un tipo sbagliato"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Il token non contiene il parametro '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Il parametro '{}' del token è scaduto"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Il token è stato inserito nella blacklist"
diff --git a/rest_framework_simplejwt/locale/kk/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/kk/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..a3cc991a6
Binary files /dev/null and b/rest_framework_simplejwt/locale/kk/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/kk/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/kk/LC_MESSAGES/django.po
new file mode 100644
index 000000000..22a638a73
--- /dev/null
+++ b/rest_framework_simplejwt/locale/kk/LC_MESSAGES/django.po
@@ -0,0 +1,158 @@
+# This file is distributed under the same license as the djangorestframework_simplejwt package.
+# Dulat Kushibayev , 2025.
+msgid ""
+msgstr ""
+"Project-Id-Version: djangorestframework_simplejwt\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-06-02 02:51+0300\n"
+"PO-Revision-Date: 2025-06-02 00:51+0000\n"
+"Last-Translator: Dulat Kushibayev \n"
+"Language-Team: \n"
+"Language: kk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n!=1);\n"
+
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr "Пайдаланушының құпия сөзі өзгертілді."
+
+#: authentication.py:93
+msgid "Authorization header must contain two space-delimited values"
+msgstr "Авторизация тақырыптамасында бос орынмен бөлінген екі мән болуы керек"
+
+#: authentication.py:119
+msgid "Given token not valid for any token type"
+msgstr "Берілген токен ешбір токен түріне жарамсыз"
+
+#: authentication.py:132 authentication.py:171
+msgid "Token contained no recognizable user identification"
+msgstr "Токенде пайдаланушы идентификаторы жоқ"
+
+#: authentication.py:139
+msgid "User not found"
+msgstr "Пайдаланушы табылмады"
+
+#: authentication.py:143
+msgid "User is inactive"
+msgstr "Пайдаланушы өшірулі"
+
+#: backends.py:90
+msgid "Unrecognized algorithm type '{}'"
+msgstr "Белгісіз алгоритм түрі '{}'"
+
+#: backends.py:96
+msgid "You must have cryptography installed to use {}."
+msgstr "{} пайдалану үшін cryptography орнатылған болуы керек."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+"Белгісіз түр '{}', 'leeway' мәні int, float немесе timedelta түрінде болуы "
+"керек."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+msgid "Token is invalid"
+msgstr "Токен жарамсыз"
+
+#: backends.py:173
+msgid "Invalid algorithm specified"
+msgstr "Көрсетілген алгоритм жарамсыз"
+
+#: backends.py:175 tokens.py:67
+msgid "Token is expired"
+msgstr "Токеннің мерзімі өткен"
+
+#: exceptions.py:55
+msgid "Token is invalid or expired"
+msgstr "Токен жарамсыз немесе мерзімі өткен"
+
+#: serializers.py:37
+msgid "No active account found with the given credentials"
+msgstr "Көрсетілген тіркелгі деректері бойынша есептік жазба табылмады."
+
+#: serializers.py:113 serializers.py:183
+msgid "No active account found for the given token."
+msgstr "Көрсетілген токен бойынша есептік жазба табылмады."
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Токен қара тізімге енгізілген"
+
+#: settings.py:78
+msgid ""
+"The '{}' setting has been removed. Please refer to '{}' for available "
+"settings."
+msgstr "'{}' параметрі жойылды. Қол жетімді параметрлер үшін '{}' қараңыз."
+
+#: token_blacklist/admin.py:79
+msgid "jti"
+msgstr "jti"
+
+#: token_blacklist/admin.py:85
+msgid "user"
+msgstr "пайдаланушы"
+
+#: token_blacklist/admin.py:91
+msgid "created at"
+msgstr "құрылған уақыты"
+
+#: token_blacklist/admin.py:97
+msgid "expires at"
+msgstr "мерзімі аяқталатын уақыт"
+
+#: token_blacklist/apps.py:7
+msgid "Token Blacklist"
+msgstr "Токендердің қара тізімі"
+
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr "Қолданыстағы токен"
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr "Қолданыстағы токендер"
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr "%(user)s үшін токен (%(jti)s)"
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr "Қара тізімдегі токен"
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr "Қара тізімдегі токендер"
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr "%(user)s пайдаланушысына тиесілі қара тізімдегі токен"
+
+#: tokens.py:53
+msgid "Cannot create token with no type or lifetime"
+msgstr "Түрі немесе жарамдылық мерзімі көрсетілмеген токенді жасау мүмкін емес"
+
+#: tokens.py:127
+msgid "Token has no id"
+msgstr "Токенде идентификатор жоқ"
+
+#: tokens.py:139
+msgid "Token has no type"
+msgstr "Токенде түрі көрсетілмеген"
+
+#: tokens.py:142
+msgid "Token has wrong type"
+msgstr "Токеннің түрі қате"
+
+#: tokens.py:201
+msgid "Token has no '{}' claim"
+msgstr "Токенде '{}' талап мерзімі жоқ"
+
+#: tokens.py:206
+msgid "Token '{}' claim has expired"
+msgstr "Токеннің '{}' талап мерзімі аяқталған"
diff --git a/rest_framework_simplejwt/locale/ko_KR/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/ko_KR/LC_MESSAGES/django.mo
index efc22ceb7..da76a4076 100644
Binary files a/rest_framework_simplejwt/locale/ko_KR/LC_MESSAGES/django.mo and b/rest_framework_simplejwt/locale/ko_KR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/ko_KR/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/ko_KR/LC_MESSAGES/django.po
index 2ea681584..07003205b 100644
--- a/rest_framework_simplejwt/locale/ko_KR/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/ko_KR/LC_MESSAGES/django.po
@@ -4,73 +4,106 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-01-05 06:48+0900\n"
-"Last-Translator: 양영광 \n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
+"Last-Translator: 정재균 \n"
"Language: ko_KR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-#: authentication.py:78
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr "사용자의 비밀번호가 바뀌었습니다."
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr "인증 헤더에는 공백으로 구분 된 두 개의 값이 포함되어야 합니다"
-#: authentication.py:104
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "이 토큰은 모든 타입의 토큰에 대해 유효하지 않습니다"
-#: authentication.py:116 authentication.py:138
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "토큰에 사용자 식별자가 포함되어 있지 않습니다"
-#: authentication.py:121
+#: authentication.py:139
msgid "User not found"
-msgstr "찾을 수 없는 사용자"
+msgstr "찾을 수 없는 사용자입니다"
-#: authentication.py:124
+#: authentication.py:143
msgid "User is inactive"
-msgstr "비활성화된 사용자"
+msgstr "비활성화된 사용자입니다"
-#: backends.py:51
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
-msgstr "알 수 없는 알고리즘 유형 '{}'"
+msgstr "'{}' 는 알 수 없는 알고리즘 유형입니다"
-#: backends.py:57
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr "{}를 사용하려면 암호화가 설치되어 있어야 합니다."
-#: backends.py:109
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+"알 수 없는 타입 '{}', 'leeway' 값은 반드시 int, float 또는 timedelta 타입이어"
+"야 합니다."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "유효하지 않거나 만료된 토큰입니다"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "잘못된 알고리즘이 지정되었습니다"
-#: backends.py:111 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "유효하지 않거나 만료된 토큰입니다"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
-msgstr "유효하지 않거나 만료된 토큰"
+msgstr "유효하지 않거나 만료된 토큰입니다"
-#: serializers.py:29
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "지정된 자격 증명에 해당하는 활성화된 사용자를 찾을 수 없습니다"
-#: settings.py:62
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "지정된 자격 증명에 해당하는 활성화된 사용자를 찾을 수 없습니다"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "블랙리스트에 추가된 토큰입니다"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
msgstr "'{}' 설정이 제거되었습니다. 사용 가능한 설정은 '{}'을 참조하십시오."
-#: token_blacklist/admin.py:68
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:74
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "사용자"
-#: token_blacklist/admin.py:80
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "생성 시간"
-#: token_blacklist/admin.py:86
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "만료 시간"
@@ -78,30 +111,52 @@ msgstr "만료 시간"
msgid "Token Blacklist"
msgstr "토큰 블랙리스트"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "타입 또는 수명이 없는 토큰을 생성할 수 없습니다"
-#: tokens.py:97
+#: tokens.py:127
msgid "Token has no id"
-msgstr "토큰에 식별자가 주어지지 않음"
+msgstr "토큰에 식별자가 주어지지 않았습니다"
-#: tokens.py:108
+#: tokens.py:139
msgid "Token has no type"
-msgstr "토큰 타입이 주어지지 않음"
+msgstr "토큰 타입이 주어지지 않았습니다"
-#: tokens.py:111
+#: tokens.py:142
msgid "Token has wrong type"
-msgstr "잘못된 토큰 타입"
+msgstr "잘못된 토큰 타입입니다"
-#: tokens.py:163
+#: tokens.py:201
msgid "Token has no '{}' claim"
-msgstr "토큰에 '{}' 클레임이 없음"
+msgstr "토큰에 '{}' 클레임이 없습니다"
-#: tokens.py:167
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "토큰 '{}' 클레임이 만료되었습니다"
-
-#: tokens.py:217
-msgid "Token is blacklisted"
-msgstr "블랙리스트에 추가된 토큰"
diff --git a/rest_framework_simplejwt/locale/nl_NL/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/nl_NL/LC_MESSAGES/django.po
index 856e1f97f..31b1172dc 100644
--- a/rest_framework_simplejwt/locale/nl_NL/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/nl_NL/LC_MESSAGES/django.po
@@ -4,74 +4,105 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-06-17 11:06+0200\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"Last-Translator: rene \n"
"Language: nl_NL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"Authorisatie header moet twee waarden bevatten, gescheiden door een spatie"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Het token is voor geen enkel token-type geldig"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Token bevat geen herkenbare gebruikersidentificatie"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Gebruiker niet gevonden"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Gebruiker is inactief"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Niet herkend algoritme type '{}"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr ""
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Token is niet geldig of verlopen"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr ""
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Token is niet geldig of verlopen"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Token is niet geldig of verlopen"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Geen actief account gevonden voor deze gegevens"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Geen actief account gevonden voor deze gegevens"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token is ge-blacklist"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
msgstr ""
"De '{}' instelling bestaat niet meer. Zie '{}' for beschikbareinstellingen."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "gebruiker"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "aangemaakt op"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "verloopt op"
@@ -79,30 +110,52 @@ msgstr "verloopt op"
msgid "Token Blacklist"
msgstr "Token Blacklist"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Kan geen token maken zonder type of levensduur"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Token heeft geen id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Token heeft geen type"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Token heeft het verkeerde type"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Token heeft geen '{}' recht"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Token '{}' recht is verlopen"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Token is ge-blacklist"
diff --git a/rest_framework_simplejwt/locale/pl_PL/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/pl_PL/LC_MESSAGES/django.po
index 425320a6f..6f088501c 100644
--- a/rest_framework_simplejwt/locale/pl_PL/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/pl_PL/LC_MESSAGES/django.po
@@ -4,73 +4,104 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"Last-Translator: Mateusz Slisz \n"
"Language: pl_PL\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr "Nagłówek autoryzacji musi zawierać dwie wartości rodzielone spacjami"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Podany token jest błędny dla każdego typu tokena"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Token nie zawierał rozpoznawalnej identyfikacji użytkownika"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Użytkownik nie znaleziony"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Użytkownik jest nieaktywny"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Nierozpoznany typ algorytmu '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr ""
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Token jest niepoprawny lub wygasł"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr ""
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Token jest niepoprawny lub wygasł"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Token jest niepoprawny lub wygasł"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Nie znaleziono aktywnego konta dla podanych danych uwierzytelniających"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Nie znaleziono aktywnego konta dla podanych danych uwierzytelniających"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token znajduję się na czarnej liście"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
msgstr ""
"Ustawienie '{}' zostało usunięte. Dostępne ustawienia znajdują sie w '{}'"
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "użytkownik"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "stworzony w"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "wygasa o"
@@ -78,30 +109,52 @@ msgstr "wygasa o"
msgid "Token Blacklist"
msgstr "Token Blacklist"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Nie można utworzyć tokena bez podanego typu lub żywotności"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Token nie posiada numeru identyfikacyjnego"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Token nie posiada typu"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Token posiada zły typ"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Token nie posiada upoważnienia '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Upoważnienie tokena '{}' wygasło"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Token znajduję się na czarnej liście"
diff --git a/rest_framework_simplejwt/locale/pt_BR/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/pt_BR/LC_MESSAGES/django.mo
index 7592fcb28..e7d0c23f3 100644
Binary files a/rest_framework_simplejwt/locale/pt_BR/LC_MESSAGES/django.mo and b/rest_framework_simplejwt/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/pt_BR/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/pt_BR/LC_MESSAGES/django.po
index e36cecfc2..3c8258974 100644
--- a/rest_framework_simplejwt/locale/pt_BR/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/pt_BR/LC_MESSAGES/django.po
@@ -4,56 +4,82 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
-"Last-Translator: Bruno Ducraux \n"
+"POT-Creation-Date: 2025-02-28 15:08-0300\n"
+"Last-Translator: Cloves Oliveira \n"
"Language: pt_BR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr "A senha do usuário foi alterada."
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"Cabeçalho de autorização deve conter dois valores delimitados por espaço"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "O token informado não é válido para qualquer tipo de token"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "O token não continha nenhuma identificação reconhecível do usuário"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Usuário não encontrado"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Usuário está inativo"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Tipo de algoritmo '{}' não reconhecido"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr "Você deve ter criptografia instalada para usar {}."
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+"Tipo '{}' não reconhecido, 'leeway' deve ser do tipo int, float ou timedelta."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+msgid "Token is invalid"
+msgstr "O token é inválido"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "Algoritmo inválido especificado"
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+msgid "Token is expired"
+msgstr "Token expirado"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
-msgstr "O token é inválido ou expirado"
+msgstr "Token inválido ou expirado"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Usuário e/ou senha incorreto(s)"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+msgid "No active account found for the given token."
+msgstr "Nenhuma conta ativa encontrada para o token fornecido."
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token está na blacklist"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -61,19 +87,19 @@ msgstr ""
"A configuração '{}' foi removida. Por favor, consulte '{}' para disponível "
"definições."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "usuário"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "criado em"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "expira em"
@@ -81,30 +107,52 @@ msgstr "expira em"
msgid "Token Blacklist"
msgstr "Lista negra de Tokens"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr "Token pendente"
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr "Tokens pendentes"
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr "Token para %(user)s (%(jti)s)"
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr "Token na lista negra"
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr "Tokens na lista negra"
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr "Token na lista negra para %(user)s"
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Não é possível criar token sem tipo ou tempo de vida"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "Token não tem id"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Token não tem nenhum tipo"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Token tem tipo errado"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Token não tem '{}' privilégio"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "O privilégio '{}' do token expirou"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Token está na blacklist"
diff --git a/rest_framework_simplejwt/locale/ro/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/ro/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..d87cddd11
Binary files /dev/null and b/rest_framework_simplejwt/locale/ro/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/ro/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/ro/LC_MESSAGES/django.po
new file mode 100644
index 000000000..1ff81c5f2
--- /dev/null
+++ b/rest_framework_simplejwt/locale/ro/LC_MESSAGES/django.po
@@ -0,0 +1,165 @@
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR Daniel Cuznetov , 2022.
+msgid ""
+msgstr ""
+"Project-Id-Version: djangorestframework_simplejwt\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
+"Last-Translator: Daniel Cuznetov \n"
+"Language: ro\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
+msgid "Authorization header must contain two space-delimited values"
+msgstr ""
+"Header-ul(antetul) de autorizare trebuie să conțină două valori separate "
+"prin spațiu"
+
+#: authentication.py:119
+msgid "Given token not valid for any token type"
+msgstr "Tokenul dat nu este valid pentru niciun tip de token"
+
+#: authentication.py:132 authentication.py:171
+msgid "Token contained no recognizable user identification"
+msgstr "Tokenul nu conține date de identificare a utilizatorului"
+
+#: authentication.py:139
+msgid "User not found"
+msgstr "Utilizatorul nu a fost găsit"
+
+#: authentication.py:143
+msgid "User is inactive"
+msgstr "Utilizatorul este inactiv"
+
+#: backends.py:90
+msgid "Unrecognized algorithm type '{}'"
+msgstr "Tipul de algoritm '{}' nu este recunoscut"
+
+#: backends.py:96
+msgid "You must have cryptography installed to use {}."
+msgstr "Trebuie să aveți instalată criptografia pentru a utiliza {}."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+"Tipul '{}' nu este recunoscut, 'leeway' trebuie să fie de tip int, float sau "
+"timedelta."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Token nu este valid sau a expirat"
+
+#: backends.py:173
+msgid "Invalid algorithm specified"
+msgstr "Algoritm nevalid specificat"
+
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Token nu este valid sau a expirat"
+
+#: exceptions.py:55
+msgid "Token is invalid or expired"
+msgstr "Token nu este valid sau a expirat"
+
+#: serializers.py:37
+msgid "No active account found with the given credentials"
+msgstr "Nu a fost găsit cont activ cu aceste date de autentificare"
+
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Nu a fost găsit cont activ cu aceste date de autentificare"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Tokenul este în listă de tokenuri blocate"
+
+#: settings.py:78
+msgid ""
+"The '{}' setting has been removed. Please refer to '{}' for available "
+"settings."
+msgstr ""
+"Setarea '{}' a fost ștearsă. Referați la '{}' pentru setări disponibile."
+
+#: token_blacklist/admin.py:79
+msgid "jti"
+msgstr "jti"
+
+#: token_blacklist/admin.py:85
+msgid "user"
+msgstr "utilizator"
+
+#: token_blacklist/admin.py:91
+msgid "created at"
+msgstr "creat la"
+
+#: token_blacklist/admin.py:97
+msgid "expires at"
+msgstr "expiră la"
+
+#: token_blacklist/apps.py:7
+msgid "Token Blacklist"
+msgstr "Listă de token-uri blocate"
+
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
+msgid "Cannot create token with no type or lifetime"
+msgstr "Nu se poate crea token fără tip sau durată de viață"
+
+#: tokens.py:127
+msgid "Token has no id"
+msgstr "Tokenul nu are id"
+
+#: tokens.py:139
+msgid "Token has no type"
+msgstr "Tokenul nu are tip"
+
+#: tokens.py:142
+msgid "Token has wrong type"
+msgstr "Tokenul are tipul greșit"
+
+#: tokens.py:201
+msgid "Token has no '{}' claim"
+msgstr "Tokenul nu are reclamația '{}'"
+
+#: tokens.py:206
+msgid "Token '{}' claim has expired"
+msgstr "Reclamația tokenului '{}' a expirat"
diff --git a/rest_framework_simplejwt/locale/ru_RU/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/ru_RU/LC_MESSAGES/django.po
index afafee8b6..6f6a7a5e2 100644
--- a/rest_framework_simplejwt/locale/ru_RU/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/ru_RU/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-02-22 17:30+0100\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"PO-Revision-Date: \n"
"Last-Translator: Sergey Ozeranskiy \n"
"Language-Team: \n"
@@ -12,52 +12,83 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
-"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"X-Generator: Poedit 2.2.1\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr ""
"Заголовок авторизации должен содержать два значения, разделенных пробелом"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Данный токен недействителен для любого типа токена"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Токен не содержит идентификатор пользователя"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Пользователь не найден"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Пользователь неактивен"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Нераспознанный тип алгоритма '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr ""
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Токен недействителен или просрочен"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr ""
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Токен недействителен или просрочен"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Токен недействителен или просрочен"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Не найдено активной учетной записи с указанными данными"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Не найдено активной учетной записи с указанными данными"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Токен занесен в черный список"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
@@ -65,19 +96,19 @@ msgstr ""
"Параметр '{}' был удален. Пожалуйста, обратитесь к '{}' для просмотра "
"доступных настроек."
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "пользователь"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "создан"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "истекает"
@@ -85,30 +116,52 @@ msgstr "истекает"
msgid "Token Blacklist"
msgstr "Token Blacklist"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Невозможно создать токен без типа или времени жизни"
-#: tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "У токена нет идентификатора"
-#: tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "Токен не имеет типа"
-#: tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Токен имеет неправильный тип"
-#: tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Токен не содержит '{}'"
-#: tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Токен имеет просроченное значение '{}'"
-
-#: tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Токен занесен в черный список"
diff --git a/rest_framework_simplejwt/locale/sl/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/sl/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..a5c125a2d
Binary files /dev/null and b/rest_framework_simplejwt/locale/sl/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/sl/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/sl/LC_MESSAGES/django.po
new file mode 100644
index 000000000..2a7634337
--- /dev/null
+++ b/rest_framework_simplejwt/locale/sl/LC_MESSAGES/django.po
@@ -0,0 +1,167 @@
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , 2022.
+msgid ""
+msgstr ""
+"Project-Id-Version: djangorestframework_simplejwt\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
+"PO-Revision-Date: \n"
+"Last-Translator: Urban Prevc \n"
+"Language-Team: \n"
+"Language: sl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.0.6\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
+msgid "Authorization header must contain two space-delimited values"
+msgstr ""
+"Glava 'Authorization' mora vsebovati dve vrednosti, ločeni s presledkom"
+
+#: authentication.py:119
+msgid "Given token not valid for any token type"
+msgstr "Podan žeton ni veljaven za nobeno vrsto žetona"
+
+#: authentication.py:132 authentication.py:171
+msgid "Token contained no recognizable user identification"
+msgstr "Žeton ni vseboval prepoznavne identifikacije uporabnika"
+
+#: authentication.py:139
+msgid "User not found"
+msgstr "Uporabnik ni najden"
+
+#: authentication.py:143
+msgid "User is inactive"
+msgstr "Uporabnik je neaktiven"
+
+#: backends.py:90
+msgid "Unrecognized algorithm type '{}'"
+msgstr "Neprepoznana vrsta algoritma'{}'"
+
+#: backends.py:96
+msgid "You must have cryptography installed to use {}."
+msgstr "Za uporabo '{}' je potrebna namestitev 'cryptography'."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+"Neprepoznana vrsta '{}', 'leeway' mora biti vrste int, float ali timedelta."
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Žeton je neveljaven ali potekel"
+
+#: backends.py:173
+msgid "Invalid algorithm specified"
+msgstr "Naveden algoritem je neveljaven"
+
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Žeton je neveljaven ali potekel"
+
+#: exceptions.py:55
+msgid "Token is invalid or expired"
+msgstr "Žeton je neveljaven ali potekel"
+
+#: serializers.py:37
+msgid "No active account found with the given credentials"
+msgstr "Aktiven račun s podanimi poverilnicami ni najden"
+
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Aktiven račun s podanimi poverilnicami ni najden"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Žeton je na črnem seznamu"
+
+#: settings.py:78
+msgid ""
+"The '{}' setting has been removed. Please refer to '{}' for available "
+"settings."
+msgstr ""
+"Nastavitev '{}' je bila odstranjena. Prosimo, oglejte si '{}' za "
+"razpoložljive nastavitve."
+
+#: token_blacklist/admin.py:79
+msgid "jti"
+msgstr "jti"
+
+#: token_blacklist/admin.py:85
+msgid "user"
+msgstr "uporabnik"
+
+#: token_blacklist/admin.py:91
+msgid "created at"
+msgstr "ustvarjen ob"
+
+#: token_blacklist/admin.py:97
+msgid "expires at"
+msgstr "poteče ob"
+
+#: token_blacklist/apps.py:7
+msgid "Token Blacklist"
+msgstr "Črni seznam žetonov"
+
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
+msgid "Cannot create token with no type or lifetime"
+msgstr "Ni mogoče ustvariti žetona brez vrste ali življenjske dobe"
+
+#: tokens.py:127
+msgid "Token has no id"
+msgstr "Žetonu manjka id"
+
+#: tokens.py:139
+msgid "Token has no type"
+msgstr "Žetonu manjka vrsta"
+
+#: tokens.py:142
+msgid "Token has wrong type"
+msgstr "Žeton je napačne vrste"
+
+#: tokens.py:201
+msgid "Token has no '{}' claim"
+msgstr "Žeton nima '{}' zahtevka"
+
+#: tokens.py:206
+msgid "Token '{}' claim has expired"
+msgstr "'{}' zahtevek žetona je potekel"
diff --git a/rest_framework_simplejwt/locale/sv/LC_MESSAGES/django.mo b/rest_framework_simplejwt/locale/sv/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..5c75a354d
Binary files /dev/null and b/rest_framework_simplejwt/locale/sv/LC_MESSAGES/django.mo differ
diff --git a/rest_framework_simplejwt/locale/sv/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/sv/LC_MESSAGES/django.po
new file mode 100644
index 000000000..d9520ddea
--- /dev/null
+++ b/rest_framework_simplejwt/locale/sv/LC_MESSAGES/django.po
@@ -0,0 +1,161 @@
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR Pasindu , 2022.
+msgid ""
+msgstr ""
+"Project-Id-Version: djangorestframework_simplejwt\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
+"Last-Translator: Pasindu \n"
+"Language: sv\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
+msgid "Authorization header must contain two space-delimited values"
+msgstr "Auktoriseringshuvudet måste innehålla två mellanslagsavgränsade värden"
+
+#: authentication.py:119
+msgid "Given token not valid for any token type"
+msgstr "Givet token är inte giltigt för någon tokentyp"
+
+#: authentication.py:132 authentication.py:171
+msgid "Token contained no recognizable user identification"
+msgstr "Token innehöll ingen identifiering av användaren"
+
+#: authentication.py:139
+msgid "User not found"
+msgstr "Användaren hittades inte"
+
+#: authentication.py:143
+msgid "User is inactive"
+msgstr "Användaren är inaktiv"
+
+#: backends.py:90
+msgid "Unrecognized algorithm type '{}'"
+msgstr "Okänd algoritmtyp '{}'"
+
+#: backends.py:96
+msgid "You must have cryptography installed to use {}."
+msgstr "Du måste ha kryptografi installerad för att kunna använda {}."
+
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Token är ogiltig eller har löpt ut"
+
+#: backends.py:173
+msgid "Invalid algorithm specified"
+msgstr "Ogiltig algoritm har angetts"
+
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Token är ogiltig eller har löpt ut"
+
+#: exceptions.py:55
+msgid "Token is invalid or expired"
+msgstr "Token är ogiltig eller har löpt ut"
+
+#: serializers.py:37
+msgid "No active account found with the given credentials"
+msgstr "Inget aktivt konto hittades med de angivna användaruppgifterna"
+
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Inget aktivt konto hittades med de angivna användaruppgifterna"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token är svartlistad"
+
+#: settings.py:78
+msgid ""
+"The '{}' setting has been removed. Please refer to '{}' for available "
+"settings."
+msgstr ""
+"Inställningen '{}' har tagits bort. Se '{}' för tillgängliga inställningar"
+
+#: token_blacklist/admin.py:79
+msgid "jti"
+msgstr "jti"
+
+#: token_blacklist/admin.py:85
+msgid "user"
+msgstr "användare"
+
+#: token_blacklist/admin.py:91
+msgid "created at"
+msgstr "skapad vid"
+
+#: token_blacklist/admin.py:97
+msgid "expires at"
+msgstr "går ut kl"
+
+#: token_blacklist/apps.py:7
+msgid "Token Blacklist"
+msgstr "Token svartlist"
+
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
+msgid "Cannot create token with no type or lifetime"
+msgstr "Kan inte skapa token utan typ eller livslängd"
+
+#: tokens.py:127
+msgid "Token has no id"
+msgstr "Token har inget id"
+
+#: tokens.py:139
+msgid "Token has no type"
+msgstr "Token har ingen typ"
+
+#: tokens.py:142
+msgid "Token has wrong type"
+msgstr "Token har fel typ"
+
+#: tokens.py:201
+msgid "Token has no '{}' claim"
+msgstr "Token har inget '{}'-anspråk"
+
+#: tokens.py:206
+msgid "Token '{}' claim has expired"
+msgstr "Token '{}'-anspråket har löpt ut"
diff --git a/rest_framework_simplejwt/locale/tr/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/tr/LC_MESSAGES/django.po
index ba7060c7d..91f135247 100644
--- a/rest_framework_simplejwt/locale/tr/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/tr/LC_MESSAGES/django.po
@@ -4,7 +4,7 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-01-13 23:05+0300\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"Last-Translator: Şuayip Üzülmez \n"
"Language: tr\n"
"MIME-Version: 1.0\n"
@@ -12,65 +12,98 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-#: authentication.py:78
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
-msgstr "Yetkilendirme header'i boşlukla sınırlandırılmış iki değer bulundurmak zorunda"
+msgstr ""
+"Yetkilendirme header'i boşlukla sınırlandırılmış iki değer bulundurmak "
+"zorunda"
-#: authentication.py:104
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Verilen token hiçbir token tipi için geçerli değil"
-#: authentication.py:116 authentication.py:138
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Token tanınabilir bir kullanıcı kimliği içermiyor"
-#: authentication.py:121
+#: authentication.py:139
msgid "User not found"
msgstr "Kullanıcı bulunamadı"
-#: authentication.py:124
+#: authentication.py:143
msgid "User is inactive"
msgstr "Kullanıcı etkin değil"
-#: backends.py:47
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Tanınmayan algortima tipi '{}'"
-#: backends.py:53
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr "{} kullanmak için cryptography yüklemeniz gerekiyor."
-#: backends.py:105
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Token geçersiz veya süresi geçmiş"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "Geçersiz algoritma belirtildi"
-#: backends.py:107 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Token geçersiz veya süresi geçmiş"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Token geçersiz veya süresi geçmiş"
-#: serializers.py:29
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Verilen kimlik bilgileriyle aktif bir hesap bulunamadı"
-#: settings.py:62
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Verilen kimlik bilgileriyle aktif bir hesap bulunamadı"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Token kara listeye alınmış"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
msgstr "'{}' ayarı kaldırıldı. Mevcut ayarlar için '{}' adresini ziyaret edin."
-#: token_blacklist/admin.py:68
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:74
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "kullanıcı"
-#: token_blacklist/admin.py:80
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "oluşturulma tarihi"
-#: token_blacklist/admin.py:86
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "sona erme tarihi"
@@ -78,30 +111,52 @@ msgstr "sona erme tarihi"
msgid "Token Blacklist"
msgstr "Token Kara Listesi"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Tipi veya geçerlilik süresi olmayan token oluşturulamaz"
-#: tokens.py:97
+#: tokens.py:127
msgid "Token has no id"
msgstr "Token'in id'si yok"
-#: tokens.py:108
+#: tokens.py:139
msgid "Token has no type"
msgstr "Token'in tipi yok"
-#: tokens.py:111
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "Token'in tipi yanlış"
-#: tokens.py:163
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "Token'in '{}' claim'i yok"
-#: tokens.py:167
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Token'in '{}' claim'i sona erdi"
-
-#: tokens.py:217
-msgid "Token is blacklisted"
-msgstr "Token kara listeye alınmış"
diff --git a/rest_framework_simplejwt/locale/uk_UA/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/uk_UA/LC_MESSAGES/django.po
index 13bdf0192..a4657ce80 100644
--- a/rest_framework_simplejwt/locale/uk_UA/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/uk_UA/LC_MESSAGES/django.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-06-17 12:32+0300\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Artiukhov Artem \n"
"Language-Team: LANGUAGE \n"
@@ -15,99 +15,149 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-#: rest_framework_simplejwt/authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr "Авторизаційний заголовок має містити два значення розділені пробілом"
-#: rest_framework_simplejwt/authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "Наданий токен не відповідає жодному типу ключа"
-#: rest_framework_simplejwt/authentication.py:111
-#: rest_framework_simplejwt/authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "Наданий токен не мітить жодної ідентифікаційної інформації"
-#: rest_framework_simplejwt/authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "Користувач не знайдений"
-#: rest_framework_simplejwt/authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "Користувач неактивний"
-#: rest_framework_simplejwt/backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "Тип алгоритму '{}' не розпізнаний"
-#: rest_framework_simplejwt/backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr "Встановіть модуль cryptography щоб використовувати {}"
-#: rest_framework_simplejwt/backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "Токен некоректний або термін його дії вичерпаний"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "Вказаний невірний алгоритм"
-#: rest_framework_simplejwt/backends.py:76
-#: rest_framework_simplejwt/exceptions.py:38
-#: rest_framework_simplejwt/tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "Токен некоректний або термін його дії вичерпаний"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "Токен некоректний або термін його дії вичерпаний"
-#: rest_framework_simplejwt/serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "Не знайдено жодного облікового запису по наданих облікових даних"
-#: rest_framework_simplejwt/settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "Не знайдено жодного облікового запису по наданих облікових даних"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "Токен занесений у чорний список"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
msgstr "Налаштування '{}' видалене. Подивіться у '{}' для інших доступних"
-#: rest_framework_simplejwt/token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: rest_framework_simplejwt/token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "користувач"
-#: rest_framework_simplejwt/token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "створений о"
-#: rest_framework_simplejwt/token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "дійстний по"
-#: rest_framework_simplejwt/token_blacklist/apps.py:7
+#: token_blacklist/apps.py:7
msgid "Token Blacklist"
msgstr "Чорний список токенів"
-#: rest_framework_simplejwt/tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "Неможливо створити токен без типу або строку дії"
-#: rest_framework_simplejwt/tokens.py:98
+#: tokens.py:127
msgid "Token has no id"
msgstr "У ключі доступу не міститься id"
-#: rest_framework_simplejwt/tokens.py:109
+#: tokens.py:139
msgid "Token has no type"
msgstr "У ключі доступу не міститься тип"
-#: rest_framework_simplejwt/tokens.py:112
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "токен позначений невірним типом"
-#: rest_framework_simplejwt/tokens.py:149
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "У токені не міститься '{}' заголовку"
-#: rest_framework_simplejwt/tokens.py:153
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "Заголовок '{}' токена не дійсний"
-
-#: rest_framework_simplejwt/tokens.py:192
-msgid "Token is blacklisted"
-msgstr "Токен занесений у чорний список"
diff --git a/rest_framework_simplejwt/locale/zh_Hans/LC_MESSAGES/django.po b/rest_framework_simplejwt/locale/zh_Hans/LC_MESSAGES/django.po
index 3ba45f0f3..93224f6e4 100644
--- a/rest_framework_simplejwt/locale/zh_Hans/LC_MESSAGES/django.po
+++ b/rest_framework_simplejwt/locale/zh_Hans/LC_MESSAGES/django.po
@@ -2,12 +2,11 @@
# Copyright (C) 2021
# This file is distributed under the same license as the Simple JWT package.
# zengqiu , 2021.
-
msgid ""
msgstr ""
"Project-Id-Version: djangorestframework_simplejwt\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-06-23 13:29+0800\n"
+"POT-Creation-Date: 2025-02-28 15:09-0300\n"
"PO-Revision-Date: 2021-06-23 13:29+080\n"
"Last-Translator: zengqiu \n"
"Language: zh_Hans\n"
@@ -16,67 +15,96 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
-#: authentication.py:79
+#: authentication.py:37 serializers.py:114 serializers.py:184
+msgid "The user's password has been changed."
+msgstr ""
+
+#: authentication.py:93
msgid "Authorization header must contain two space-delimited values"
msgstr "授权头必须包含两个用空格分隔的值"
-#: authentication.py:100
+#: authentication.py:119
msgid "Given token not valid for any token type"
msgstr "此令牌对任何类型的令牌无效"
-#: authentication.py:111 authentication.py:133
+#: authentication.py:132 authentication.py:171
msgid "Token contained no recognizable user identification"
msgstr "令牌未包含用户标识符"
-#: authentication.py:116
+#: authentication.py:139
msgid "User not found"
msgstr "未找到该用户"
-#: authentication.py:119
+#: authentication.py:143
msgid "User is inactive"
msgstr "该用户已禁用"
-#: backends.py:37
+#: backends.py:90
msgid "Unrecognized algorithm type '{}'"
msgstr "未知算法类型 '{}'"
-#: backends.py:40
+#: backends.py:96
msgid "You must have cryptography installed to use {}."
msgstr "你必须安装 cryptography 才能使用 {}。"
-#: backends.py:74
+#: backends.py:111
+msgid ""
+"Unrecognized type '{}', 'leeway' must be of type int, float or timedelta."
+msgstr ""
+
+#: backends.py:125 backends.py:177 tokens.py:69
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is invalid"
+msgstr "令牌无效或已过期"
+
+#: backends.py:173
msgid "Invalid algorithm specified"
msgstr "指定的算法无效"
-#: backends.py:76 exceptions.py:38 tokens.py:44
+#: backends.py:175 tokens.py:67
+#, fuzzy
+#| msgid "Token is invalid or expired"
+msgid "Token is expired"
+msgstr "令牌无效或已过期"
+
+#: exceptions.py:55
msgid "Token is invalid or expired"
msgstr "令牌无效或已过期"
-#: serializers.py:24
+#: serializers.py:37
msgid "No active account found with the given credentials"
msgstr "找不到指定凭据对应的有效用户"
-#: settings.py:63
+#: serializers.py:113 serializers.py:183
+#, fuzzy
+#| msgid "No active account found with the given credentials"
+msgid "No active account found for the given token."
+msgstr "找不到指定凭据对应的有效用户"
+
+#: serializers.py:249 tokens.py:280
+msgid "Token is blacklisted"
+msgstr "令牌已被加入黑名单"
+
+#: settings.py:78
msgid ""
"The '{}' setting has been removed. Please refer to '{}' for available "
"settings."
-msgstr ""
-"'{}' 配置已被移除。 请参阅 '{}' 获取可用的"
-"配置。"
+msgstr "'{}' 配置已被移除。 请参阅 '{}' 获取可用的配置。"
-#: token_blacklist/admin.py:72
+#: token_blacklist/admin.py:79
msgid "jti"
msgstr "jti"
-#: token_blacklist/admin.py:77
+#: token_blacklist/admin.py:85
msgid "user"
msgstr "用户"
-#: token_blacklist/admin.py:82
+#: token_blacklist/admin.py:91
msgid "created at"
msgstr "创建时间"
-#: token_blacklist/admin.py:87
+#: token_blacklist/admin.py:97
msgid "expires at"
msgstr "过期时间"
@@ -84,30 +112,52 @@ msgstr "过期时间"
msgid "Token Blacklist"
msgstr "令牌黑名单"
-#: tokens.py:30
+#: token_blacklist/models.py:19
+msgid "Outstanding Token"
+msgstr ""
+
+#: token_blacklist/models.py:20
+msgid "Outstanding Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:32
+#, python-format
+msgid "Token for %(user)s (%(jti)s)"
+msgstr ""
+
+#: token_blacklist/models.py:45
+msgid "Blacklisted Token"
+msgstr ""
+
+#: token_blacklist/models.py:46
+msgid "Blacklisted Tokens"
+msgstr ""
+
+#: token_blacklist/models.py:57
+#, python-format
+msgid "Blacklisted token for %(user)s"
+msgstr ""
+
+#: tokens.py:53
msgid "Cannot create token with no type or lifetime"
msgstr "无法创建没有类型或生存期的令牌"
-#: tokens.py:96
+#: tokens.py:127
msgid "Token has no id"
msgstr "令牌没有标识符"
-#: tokens.py:107
+#: tokens.py:139
msgid "Token has no type"
msgstr "令牌没有类型"
-#: tokens.py:110
+#: tokens.py:142
msgid "Token has wrong type"
msgstr "令牌类型错误"
-#: tokens.py:147
+#: tokens.py:201
msgid "Token has no '{}' claim"
msgstr "令牌没有 '{}' 声明"
-#: tokens.py:151
+#: tokens.py:206
msgid "Token '{}' claim has expired"
msgstr "令牌 '{}' 声明已过期"
-
-#: tokens.py:194
-msgid "Token is blacklisted"
-msgstr "令牌已被加入黑名单"
diff --git a/rest_framework_simplejwt/models.py b/rest_framework_simplejwt/models.py
index 391c1162a..859dfd3ae 100644
--- a/rest_framework_simplejwt/models.py
+++ b/rest_framework_simplejwt/models.py
@@ -1,17 +1,21 @@
+from typing import TYPE_CHECKING, Any, Optional, Union
+
from django.contrib.auth import models as auth_models
from django.db.models.manager import EmptyManager
from django.utils.functional import cached_property
-from .compat import CallableFalse, CallableTrue
from .settings import api_settings
+if TYPE_CHECKING:
+ from .tokens import Token
+
class TokenUser:
"""
A dummy user class modeled after django.contrib.auth.models.AnonymousUser.
- Used in conjunction with the `JWTTokenUserAuthentication` backend to
+ Used in conjunction with the `JWTStatelessUserAuthentication` backend to
implement single sign-on functionality across services which share the same
- secret key. `JWTTokenUserAuthentication` will return an instance of this
+ secret key. `JWTStatelessUserAuthentication` will return an instance of this
class instead of a `User` model instance. Instances of this class act as
stateless user objects which are backed by validated tokens.
"""
@@ -23,83 +27,89 @@ class instead of a `User` model instance. Instances of this class act as
_groups = EmptyManager(auth_models.Group)
_user_permissions = EmptyManager(auth_models.Permission)
- def __init__(self, token):
+ def __init__(self, token: "Token") -> None:
self.token = token
- def __str__(self):
+ def __str__(self) -> str:
return f"TokenUser {self.id}"
@cached_property
- def id(self):
+ def id(self) -> str:
return self.token[api_settings.USER_ID_CLAIM]
@cached_property
- def pk(self):
+ def pk(self) -> str:
return self.id
@cached_property
- def username(self):
+ def username(self) -> str:
return self.token.get("username", "")
@cached_property
- def is_staff(self):
+ def is_staff(self) -> bool:
return self.token.get("is_staff", False)
@cached_property
- def is_superuser(self):
+ def is_superuser(self) -> bool:
return self.token.get("is_superuser", False)
- def __eq__(self, other):
+ def __eq__(self, other: object) -> bool:
+ if not isinstance(other, TokenUser):
+ return NotImplemented
return self.id == other.id
- def __ne__(self, other):
+ def __ne__(self, other: object) -> bool:
return not self.__eq__(other)
- def __hash__(self):
+ def __hash__(self) -> int:
return hash(self.id)
- def save(self):
+ def save(self) -> None:
raise NotImplementedError("Token users have no DB representation")
- def delete(self):
+ def delete(self) -> None:
raise NotImplementedError("Token users have no DB representation")
- def set_password(self, raw_password):
+ def set_password(self, raw_password: str) -> None:
raise NotImplementedError("Token users have no DB representation")
- def check_password(self, raw_password):
+ def check_password(self, raw_password: str) -> None:
raise NotImplementedError("Token users have no DB representation")
@property
- def groups(self):
+ def groups(self) -> auth_models.Group:
return self._groups
@property
- def user_permissions(self):
+ def user_permissions(self) -> auth_models.Permission:
return self._user_permissions
- def get_group_permissions(self, obj=None):
+ def get_group_permissions(self, obj: Optional[object] = None) -> set:
return set()
- def get_all_permissions(self, obj=None):
+ def get_all_permissions(self, obj: Optional[object] = None) -> set:
return set()
- def has_perm(self, perm, obj=None):
+ def has_perm(self, perm: str, obj: Optional[object] = None) -> bool:
return False
- def has_perms(self, perm_list, obj=None):
+ def has_perms(self, perm_list: list[str], obj: Optional[object] = None) -> bool:
return False
- def has_module_perms(self, module):
+ def has_module_perms(self, module: str) -> bool:
return False
@property
- def is_anonymous(self):
- return CallableFalse
+ def is_anonymous(self) -> bool:
+ return False
@property
- def is_authenticated(self):
- return CallableTrue
+ def is_authenticated(self) -> bool:
+ return True
- def get_username(self):
+ def get_username(self) -> str:
return self.username
+
+ def __getattr__(self, attr: str) -> Optional[Any]:
+ """This acts as a backup attribute getter for custom claims defined in Token serializers."""
+ return self.token.get(attr, None)
diff --git a/rest_framework_simplejwt/py.typed b/rest_framework_simplejwt/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/rest_framework_simplejwt/serializers.py b/rest_framework_simplejwt/serializers.py
index 8e98ceda6..ab76a0ff7 100644
--- a/rest_framework_simplejwt/serializers.py
+++ b/rest_framework_simplejwt/serializers.py
@@ -1,19 +1,26 @@
+from typing import Any, Optional, TypeVar
+
from django.conf import settings
-from django.contrib.auth import authenticate, get_user_model
-from django.contrib.auth.models import update_last_login
+from django.contrib.auth import _clean_credentials, authenticate, get_user_model
+from django.contrib.auth.models import AbstractBaseUser, update_last_login
from django.utils.translation import gettext_lazy as _
from rest_framework import exceptions, serializers
-from rest_framework.exceptions import ValidationError
+from rest_framework.exceptions import AuthenticationFailed, ValidationError
+from rest_framework.request import Request
+from .models import TokenUser
from .settings import api_settings
-from .tokens import RefreshToken, SlidingToken, UntypedToken
+from .tokens import RefreshToken, SlidingToken, Token, UntypedToken
+from .utils import get_md5_hash_password
+
+AuthUser = TypeVar("AuthUser", AbstractBaseUser, TokenUser)
if api_settings.BLACKLIST_AFTER_ROTATION:
from .token_blacklist.models import BlacklistedToken
class PasswordField(serializers.CharField):
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, **kwargs) -> None:
kwargs.setdefault("style", {})
kwargs["style"]["input_type"] = "password"
@@ -24,18 +31,19 @@ def __init__(self, *args, **kwargs):
class TokenObtainSerializer(serializers.Serializer):
username_field = get_user_model().USERNAME_FIELD
+ token_class: Optional[type[Token]] = None
default_error_messages = {
"no_active_account": _("No active account found with the given credentials")
}
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
- self.fields[self.username_field] = serializers.CharField()
+ self.fields[self.username_field] = serializers.CharField(write_only=True)
self.fields["password"] = PasswordField()
- def validate(self, attrs):
+ def validate(self, attrs: dict[str, Any]) -> dict[Any, Any]:
authenticate_kwargs = {
self.username_field: attrs[self.username_field],
"password": attrs["password"],
@@ -48,6 +56,9 @@ def validate(self, attrs):
self.user = authenticate(**authenticate_kwargs)
if not api_settings.USER_AUTHENTICATION_RULE(self.user):
+ api_settings.ON_LOGIN_FAILED(
+ _clean_credentials(attrs), self.context.get("request")
+ )
raise exceptions.AuthenticationFailed(
self.error_messages["no_active_account"],
"no_active_account",
@@ -56,18 +67,14 @@ def validate(self, attrs):
return {}
@classmethod
- def get_token(cls, user):
- raise NotImplementedError(
- "Must implement `get_token` method for `TokenObtainSerializer` subclasses"
- )
+ def get_token(cls, user: AuthUser) -> Token:
+ return cls.token_class.for_user(user) # type: ignore
class TokenObtainPairSerializer(TokenObtainSerializer):
- @classmethod
- def get_token(cls, user):
- return RefreshToken.for_user(user)
+ token_class = RefreshToken
- def validate(self, attrs):
+ def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
data = super().validate(attrs)
refresh = self.get_token(self.user)
@@ -76,17 +83,15 @@ def validate(self, attrs):
data["access"] = str(refresh.access_token)
if api_settings.UPDATE_LAST_LOGIN:
- update_last_login(None, self.user)
+ api_settings.ON_LOGIN_SUCCESS(self.user, self.context.get("request"))
return data
class TokenObtainSlidingSerializer(TokenObtainSerializer):
- @classmethod
- def get_token(cls, user):
- return SlidingToken.for_user(user)
+ token_class = SlidingToken
- def validate(self, attrs):
+ def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
data = super().validate(attrs)
token = self.get_token(self.user)
@@ -94,7 +99,7 @@ def validate(self, attrs):
data["token"] = str(token)
if api_settings.UPDATE_LAST_LOGIN:
- update_last_login(None, self.user)
+ api_settings.ON_LOGIN_SUCCESS(self.user, self.context.get("request"))
return data
@@ -102,12 +107,54 @@ def validate(self, attrs):
class TokenRefreshSerializer(serializers.Serializer):
refresh = serializers.CharField()
access = serializers.CharField(read_only=True)
+ token_class = RefreshToken
- def validate(self, attrs):
- refresh = RefreshToken(attrs["refresh"])
+ default_error_messages = {
+ "no_active_account": _("No active account found for the given token."),
+ "password_changed": _("The user's password has been changed."),
+ }
- data = {"access": str(refresh.access_token)}
+ def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
+ refresh = self.token_class(attrs["refresh"])
+
+ user_id = refresh.payload.get(api_settings.USER_ID_CLAIM, None)
+ if user_id:
+ try:
+ user = get_user_model().objects.get(
+ **{api_settings.USER_ID_FIELD: user_id}
+ )
+ except get_user_model().DoesNotExist:
+ # This handles the case where the user has been deleted.
+ raise AuthenticationFailed(
+ self.error_messages["no_active_account"], "no_active_account"
+ )
+
+ if not api_settings.USER_AUTHENTICATION_RULE(user):
+ raise AuthenticationFailed(
+ self.error_messages["no_active_account"], "no_active_account"
+ )
+
+ if api_settings.CHECK_REVOKE_TOKEN:
+ if refresh.payload.get(
+ api_settings.REVOKE_TOKEN_CLAIM
+ ) != get_md5_hash_password(user.password):
+ # If the password has changed, we blacklist the token
+ # to prevent any further use.
+ if (
+ "rest_framework_simplejwt.token_blacklist"
+ in settings.INSTALLED_APPS
+ ):
+ try:
+ refresh.blacklist()
+ except AttributeError:
+ pass
+
+ raise AuthenticationFailed(
+ self.error_messages["password_changed"],
+ code="password_changed",
+ )
+ data = {"access": str(refresh.access_token)}
if api_settings.ROTATE_REFRESH_TOKENS:
if api_settings.BLACKLIST_AFTER_ROTATION:
try:
@@ -121,6 +168,7 @@ def validate(self, attrs):
refresh.set_jti()
refresh.set_exp()
refresh.set_iat()
+ refresh.outstand()
data["refresh"] = str(refresh)
@@ -129,9 +177,51 @@ def validate(self, attrs):
class TokenRefreshSlidingSerializer(serializers.Serializer):
token = serializers.CharField()
+ token_class = SlidingToken
- def validate(self, attrs):
- token = SlidingToken(attrs["token"])
+ default_error_messages = {
+ "no_active_account": _("No active account found for the given token."),
+ "password_changed": _("The user's password has been changed."),
+ }
+
+ def validate(self, attrs: dict[str, Any]) -> dict[str, str]:
+ token = self.token_class(attrs["token"])
+ user_id = token.payload.get(api_settings.USER_ID_CLAIM, None)
+ if user_id:
+ try:
+ user = get_user_model().objects.get(
+ **{api_settings.USER_ID_FIELD: user_id}
+ )
+ except get_user_model().DoesNotExist:
+ # This handles the case where the user has been deleted.
+ raise AuthenticationFailed(
+ self.error_messages["no_active_account"], "no_active_account"
+ )
+
+ if not api_settings.USER_AUTHENTICATION_RULE(user):
+ raise AuthenticationFailed(
+ self.error_messages["no_active_account"], "no_active_account"
+ )
+
+ if api_settings.CHECK_REVOKE_TOKEN:
+ if token.payload.get(
+ api_settings.REVOKE_TOKEN_CLAIM
+ ) != get_md5_hash_password(user.password):
+ # If the password has changed, we blacklist the token
+ # to prevent any further use.
+ if (
+ "rest_framework_simplejwt.token_blacklist"
+ in settings.INSTALLED_APPS
+ ):
+ try:
+ token.blacklist()
+ except AttributeError:
+ pass
+
+ raise AuthenticationFailed(
+ self.error_messages["password_changed"],
+ code="password_changed",
+ )
# Check that the timestamp in the "refresh_exp" claim has not
# passed
@@ -145,9 +235,9 @@ def validate(self, attrs):
class TokenVerifySerializer(serializers.Serializer):
- token = serializers.CharField()
+ token = serializers.CharField(write_only=True)
- def validate(self, attrs):
+ def validate(self, attrs: dict[str, None]) -> dict[Any, Any]:
token = UntypedToken(attrs["token"])
if (
@@ -156,18 +246,27 @@ def validate(self, attrs):
):
jti = token.get(api_settings.JTI_CLAIM)
if BlacklistedToken.objects.filter(token__jti=jti).exists():
- raise ValidationError("Token is blacklisted")
+ raise ValidationError(_("Token is blacklisted"))
return {}
class TokenBlacklistSerializer(serializers.Serializer):
- refresh = serializers.CharField()
+ refresh = serializers.CharField(write_only=True)
+ token_class = RefreshToken
- def validate(self, attrs):
- refresh = RefreshToken(attrs["refresh"])
+ def validate(self, attrs: dict[str, Any]) -> dict[Any, Any]:
+ refresh = self.token_class(attrs["refresh"])
try:
refresh.blacklist()
except AttributeError:
pass
return {}
+
+
+def default_on_login_success(user: AuthUser, request: Optional[Request]) -> None:
+ update_last_login(None, user)
+
+
+def default_on_login_failed(credentials: dict, request: Optional[Request]) -> None:
+ pass
diff --git a/rest_framework_simplejwt/settings.py b/rest_framework_simplejwt/settings.py
index e6463b4f7..5c4d9fc4a 100644
--- a/rest_framework_simplejwt/settings.py
+++ b/rest_framework_simplejwt/settings.py
@@ -1,4 +1,5 @@
from datetime import timedelta
+from typing import Any
from django.conf import settings
from django.test.signals import setting_changed
@@ -20,6 +21,7 @@
"VERIFYING_KEY": "",
"AUDIENCE": None,
"ISSUER": None,
+ "JSON_ENCODER": None,
"JWK_URL": None,
"LEEWAY": 0,
"AUTH_HEADER_TYPES": ("Bearer",),
@@ -27,6 +29,8 @@
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
"USER_AUTHENTICATION_RULE": "rest_framework_simplejwt.authentication.default_user_authentication_rule",
+ "ON_LOGIN_SUCCESS": "rest_framework_simplejwt.serializers.default_on_login_success",
+ "ON_LOGIN_FAILED": "rest_framework_simplejwt.serializers.default_on_login_failed",
"AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",),
"TOKEN_TYPE_CLAIM": "token_type",
"JTI_CLAIM": "jti",
@@ -34,12 +38,24 @@
"SLIDING_TOKEN_REFRESH_EXP_CLAIM": "refresh_exp",
"SLIDING_TOKEN_LIFETIME": timedelta(minutes=5),
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
+ "TOKEN_OBTAIN_SERIALIZER": "rest_framework_simplejwt.serializers.TokenObtainPairSerializer",
+ "TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSerializer",
+ "TOKEN_VERIFY_SERIALIZER": "rest_framework_simplejwt.serializers.TokenVerifySerializer",
+ "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",
+ "CHECK_REVOKE_TOKEN": False,
+ "REVOKE_TOKEN_CLAIM": "hash_password",
+ "CHECK_USER_IS_ACTIVE": True,
}
IMPORT_STRINGS = (
"AUTH_TOKEN_CLASSES",
+ "JSON_ENCODER",
"TOKEN_USER_CLASS",
"USER_AUTHENTICATION_RULE",
+ "ON_LOGIN_SUCCESS",
+ "ON_LOGIN_FAILED",
)
REMOVED_SETTINGS = (
@@ -51,7 +67,7 @@
class APISettings(_APISettings): # pragma: no cover
- def __check_user_settings(self, user_settings):
+ def __check_user_settings(self, user_settings: dict[str, Any]) -> dict[str, Any]:
SETTINGS_DOC = "https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html"
for setting in REMOVED_SETTINGS:
@@ -72,7 +88,7 @@ def __check_user_settings(self, user_settings):
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
-def reload_api_settings(*args, **kwargs): # pragma: no cover
+def reload_api_settings(*args, **kwargs) -> None: # pragma: no cover
global api_settings
setting, value = kwargs["setting"], kwargs["value"]
diff --git a/rest_framework_simplejwt/state.py b/rest_framework_simplejwt/state.py
index 4fe94faef..2637df233 100644
--- a/rest_framework_simplejwt/state.py
+++ b/rest_framework_simplejwt/state.py
@@ -9,4 +9,5 @@
api_settings.ISSUER,
api_settings.JWK_URL,
api_settings.LEEWAY,
+ api_settings.JSON_ENCODER,
)
diff --git a/rest_framework_simplejwt/token_blacklist/admin.py b/rest_framework_simplejwt/token_blacklist/admin.py
index b346ef5d9..a524403b2 100644
--- a/rest_framework_simplejwt/token_blacklist/admin.py
+++ b/rest_framework_simplejwt/token_blacklist/admin.py
@@ -1,8 +1,17 @@
+from datetime import datetime
+from typing import Any, Optional, TypeVar
+
from django.contrib import admin
+from django.contrib.auth.models import AbstractBaseUser
+from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
+from rest_framework.request import Request
+from ..models import TokenUser
from .models import BlacklistedToken, OutstandingToken
+AuthUser = TypeVar("AuthUser", AbstractBaseUser, TokenUser)
+
class OutstandingTokenAdmin(admin.ModelAdmin):
list_display = (
@@ -17,7 +26,7 @@ class OutstandingTokenAdmin(admin.ModelAdmin):
)
ordering = ("user",)
- def get_queryset(self, *args, **kwargs):
+ def get_queryset(self, *args, **kwargs) -> QuerySet:
qs = super().get_queryset(*args, **kwargs)
return qs.select_related("user")
@@ -25,16 +34,18 @@ def get_queryset(self, *args, **kwargs):
# Read-only behavior defined below
actions = None
- def get_readonly_fields(self, *args, **kwargs):
+ def get_readonly_fields(self, *args, **kwargs) -> list[Any]:
return [f.name for f in self.model._meta.fields]
- def has_add_permission(self, *args, **kwargs):
+ def has_add_permission(self, *args, **kwargs) -> bool:
return False
- def has_delete_permission(self, *args, **kwargs):
+ def has_delete_permission(self, *args, **kwargs) -> bool:
return False
- def has_change_permission(self, request, obj=None):
+ def has_change_permission(
+ self, request: Request, obj: Optional[object] = None
+ ) -> bool:
return request.method in ["GET", "HEAD"] and super().has_change_permission(
request, obj
)
@@ -57,34 +68,34 @@ class BlacklistedTokenAdmin(admin.ModelAdmin):
)
ordering = ("token__user",)
- def get_queryset(self, *args, **kwargs):
+ def get_queryset(self, *args, **kwargs) -> QuerySet:
qs = super().get_queryset(*args, **kwargs)
return qs.select_related("token__user")
- def token_jti(self, obj):
+ def token_jti(self, obj: BlacklistedToken) -> str:
return obj.token.jti
- token_jti.short_description = _("jti")
- token_jti.admin_order_field = "token__jti"
+ token_jti.short_description = _("jti") # type: ignore
+ token_jti.admin_order_field = "token__jti" # type: ignore
- def token_user(self, obj):
+ def token_user(self, obj: BlacklistedToken) -> AuthUser:
return obj.token.user
- token_user.short_description = _("user")
- token_user.admin_order_field = "token__user"
+ token_user.short_description = _("user") # type: ignore
+ token_user.admin_order_field = "token__user" # type: ignore
- def token_created_at(self, obj):
+ def token_created_at(self, obj: BlacklistedToken) -> datetime:
return obj.token.created_at
- token_created_at.short_description = _("created at")
- token_created_at.admin_order_field = "token__created_at"
+ token_created_at.short_description = _("created at") # type: ignore
+ token_created_at.admin_order_field = "token__created_at" # type: ignore
- def token_expires_at(self, obj):
+ def token_expires_at(self, obj: BlacklistedToken) -> datetime:
return obj.token.expires_at
- token_expires_at.short_description = _("expires at")
- token_expires_at.admin_order_field = "token__expires_at"
+ token_expires_at.short_description = _("expires at") # type: ignore
+ token_expires_at.admin_order_field = "token__expires_at" # type: ignore
admin.site.register(BlacklistedToken, BlacklistedTokenAdmin)
diff --git a/rest_framework_simplejwt/token_blacklist/management/commands/flushexpiredtokens.py b/rest_framework_simplejwt/token_blacklist/management/commands/flushexpiredtokens.py
index 8340830a0..a268a422c 100644
--- a/rest_framework_simplejwt/token_blacklist/management/commands/flushexpiredtokens.py
+++ b/rest_framework_simplejwt/token_blacklist/management/commands/flushexpiredtokens.py
@@ -8,5 +8,5 @@
class Command(BaseCommand):
help = "Flushes any expired tokens in the outstanding token list"
- def handle(self, *args, **kwargs):
+ def handle(self, *args, **kwargs) -> None:
OutstandingToken.objects.filter(expires_at__lte=aware_utcnow()).delete()
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0001_initial.py b/rest_framework_simplejwt/token_blacklist/migrations/0001_initial.py
index b80d9f0a7..88baa7d93 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0001_initial.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0001_initial.py
@@ -4,7 +4,6 @@
class Migration(migrations.Migration):
-
initial = True
dependencies = [
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0002_outstandingtoken_jti_hex.py b/rest_framework_simplejwt/token_blacklist/migrations/0002_outstandingtoken_jti_hex.py
index 56df7d1a8..59af197e6 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0002_outstandingtoken_jti_hex.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0002_outstandingtoken_jti_hex.py
@@ -2,7 +2,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("token_blacklist", "0001_initial"),
]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0003_auto_20171017_2007.py b/rest_framework_simplejwt/token_blacklist/migrations/0003_auto_20171017_2007.py
index 336827ba4..1b753499a 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0003_auto_20171017_2007.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0003_auto_20171017_2007.py
@@ -22,7 +22,6 @@ def reverse_populate_jti_hex(apps, schema_editor): # pragma: no cover
class Migration(migrations.Migration):
-
dependencies = [
("token_blacklist", "0002_outstandingtoken_jti_hex"),
]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0004_auto_20171017_2013.py b/rest_framework_simplejwt/token_blacklist/migrations/0004_auto_20171017_2013.py
index b11bdc33d..f5ba7e9ff 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0004_auto_20171017_2013.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0004_auto_20171017_2013.py
@@ -2,7 +2,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("token_blacklist", "0003_auto_20171017_2007"),
]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0005_remove_outstandingtoken_jti.py b/rest_framework_simplejwt/token_blacklist/migrations/0005_remove_outstandingtoken_jti.py
index 14e6af147..2b140ed9f 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0005_remove_outstandingtoken_jti.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0005_remove_outstandingtoken_jti.py
@@ -2,7 +2,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("token_blacklist", "0004_auto_20171017_2013"),
]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0006_auto_20171017_2113.py b/rest_framework_simplejwt/token_blacklist/migrations/0006_auto_20171017_2113.py
index abaef538f..10f9f1e2a 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0006_auto_20171017_2113.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0006_auto_20171017_2113.py
@@ -2,7 +2,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("token_blacklist", "0005_remove_outstandingtoken_jti"),
]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0007_auto_20171017_2214.py b/rest_framework_simplejwt/token_blacklist/migrations/0007_auto_20171017_2214.py
index 87961ddb8..5fa602fd1 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0007_auto_20171017_2214.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0007_auto_20171017_2214.py
@@ -4,7 +4,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("token_blacklist", "0006_auto_20171017_2113"),
]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0008_migrate_to_bigautofield.py b/rest_framework_simplejwt/token_blacklist/migrations/0008_migrate_to_bigautofield.py
index 46f516e23..9a7735729 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0008_migrate_to_bigautofield.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0008_migrate_to_bigautofield.py
@@ -2,7 +2,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("token_blacklist", "0007_auto_20171017_2214"),
]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0010_fix_migrate_to_bigautofield.py b/rest_framework_simplejwt/token_blacklist/migrations/0010_fix_migrate_to_bigautofield.py
index 4046e8bf5..b6d595df9 100644
--- a/rest_framework_simplejwt/token_blacklist/migrations/0010_fix_migrate_to_bigautofield.py
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0010_fix_migrate_to_bigautofield.py
@@ -8,7 +8,6 @@
class Migration(migrations.Migration):
-
dependencies = [
("token_blacklist", "0008_migrate_to_bigautofield"),
]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0012_alter_outstandingtoken_user.py b/rest_framework_simplejwt/token_blacklist/migrations/0012_alter_outstandingtoken_user.py
new file mode 100644
index 000000000..91d5e0645
--- /dev/null
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0012_alter_outstandingtoken_user.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.2.10 on 2022-01-24 06:42
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ("token_blacklist", "0011_linearizes_history"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="outstandingtoken",
+ name="user",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ ]
diff --git a/rest_framework_simplejwt/token_blacklist/migrations/0013_alter_blacklistedtoken_options_and_more.py b/rest_framework_simplejwt/token_blacklist/migrations/0013_alter_blacklistedtoken_options_and_more.py
new file mode 100644
index 000000000..9212c7cf4
--- /dev/null
+++ b/rest_framework_simplejwt/token_blacklist/migrations/0013_alter_blacklistedtoken_options_and_more.py
@@ -0,0 +1,27 @@
+# Generated by Django 5.1.7 on 2025-03-23 06:56
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("token_blacklist", "0012_alter_outstandingtoken_user"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="blacklistedtoken",
+ options={
+ "verbose_name": "Blacklisted Token",
+ "verbose_name_plural": "Blacklisted Tokens",
+ },
+ ),
+ migrations.AlterModelOptions(
+ name="outstandingtoken",
+ options={
+ "ordering": ("user",),
+ "verbose_name": "Outstanding Token",
+ "verbose_name_plural": "Outstanding Tokens",
+ },
+ ),
+ ]
diff --git a/rest_framework_simplejwt/token_blacklist/models.py b/rest_framework_simplejwt/token_blacklist/models.py
index b646cd23a..30e9ee38d 100644
--- a/rest_framework_simplejwt/token_blacklist/models.py
+++ b/rest_framework_simplejwt/token_blacklist/models.py
@@ -1,11 +1,12 @@
from django.conf import settings
from django.db import models
+from django.utils.translation import gettext_lazy as _
class OutstandingToken(models.Model):
id = models.BigAutoField(primary_key=True, serialize=False)
user = models.ForeignKey(
- settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=True, blank=True
+ settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True
)
jti = models.CharField(unique=True, max_length=255)
@@ -15,6 +16,8 @@ class OutstandingToken(models.Model):
expires_at = models.DateTimeField()
class Meta:
+ verbose_name = _("Outstanding Token")
+ verbose_name_plural = _("Outstanding Tokens")
# Work around for a bug in Django:
# https://code.djangoproject.com/ticket/19422
#
@@ -25,11 +28,11 @@ class Meta:
)
ordering = ("user",)
- def __str__(self):
- return "Token for {} ({})".format(
- self.user,
- self.jti,
- )
+ def __str__(self) -> str:
+ return _("Token for %(user)s (%(jti)s)") % {
+ "user": self.user,
+ "jti": self.jti,
+ }
class BlacklistedToken(models.Model):
@@ -39,6 +42,8 @@ class BlacklistedToken(models.Model):
blacklisted_at = models.DateTimeField(auto_now_add=True)
class Meta:
+ verbose_name = _("Blacklisted Token")
+ verbose_name_plural = _("Blacklisted Tokens")
# Work around for a bug in Django:
# https://code.djangoproject.com/ticket/19422
#
@@ -48,5 +53,5 @@ class Meta:
"rest_framework_simplejwt.token_blacklist" not in settings.INSTALLED_APPS
)
- def __str__(self):
- return f"Blacklisted token for {self.token.user}"
+ def __str__(self) -> str:
+ return _("Blacklisted token for %(user)s") % {"user": self.token.user}
diff --git a/rest_framework_simplejwt/tokens.py b/rest_framework_simplejwt/tokens.py
index 28b49bda9..11a05770a 100644
--- a/rest_framework_simplejwt/tokens.py
+++ b/rest_framework_simplejwt/tokens.py
@@ -1,14 +1,37 @@
-from datetime import timedelta
+from datetime import datetime, timedelta
+from typing import TYPE_CHECKING, Any, Generic, Optional, TypeVar
from uuid import uuid4
from django.conf import settings
+from django.contrib.auth import get_user_model
+from django.contrib.auth.models import AbstractBaseUser
from django.utils.module_loading import import_string
from django.utils.translation import gettext_lazy as _
-from .exceptions import TokenBackendError, TokenError
+from .exceptions import (
+ ExpiredTokenError,
+ TokenBackendError,
+ TokenBackendExpiredToken,
+ TokenError,
+)
+from .models import TokenUser
from .settings import api_settings
from .token_blacklist.models import BlacklistedToken, OutstandingToken
-from .utils import aware_utcnow, datetime_from_epoch, datetime_to_epoch, format_lazy
+from .utils import (
+ aware_utcnow,
+ datetime_from_epoch,
+ datetime_to_epoch,
+ format_lazy,
+ get_md5_hash_password,
+ logger,
+)
+
+if TYPE_CHECKING:
+ from .backends import TokenBackend
+
+T = TypeVar("T", bound="Token")
+
+AuthUser = TypeVar("AuthUser", AbstractBaseUser, TokenUser)
class Token:
@@ -17,10 +40,10 @@ class Token:
new JWT.
"""
- token_type = None
- lifetime = None
+ token_type: Optional[str] = None
+ lifetime: Optional[timedelta] = None
- def __init__(self, token=None, verify=True):
+ def __init__(self, token: Optional["Token"] = None, verify: bool = True) -> None:
"""
!!!! IMPORTANT !!!! MUST raise a TokenError with a user-facing error
message if the given token is invalid, expired, or otherwise not safe
@@ -40,8 +63,10 @@ def __init__(self, token=None, verify=True):
# Decode token
try:
self.payload = token_backend.decode(token, verify=verify)
- except TokenBackendError:
- raise TokenError(_("Token is invalid or expired"))
+ except TokenBackendExpiredToken as e:
+ raise ExpiredTokenError(_("Token is expired")) from e
+ except TokenBackendError as e:
+ raise TokenError(_("Token is invalid")) from e
if verify:
self.verify()
@@ -56,31 +81,31 @@ def __init__(self, token=None, verify=True):
# Set "jti" claim
self.set_jti()
- def __repr__(self):
+ def __repr__(self) -> str:
return repr(self.payload)
- def __getitem__(self, key):
+ def __getitem__(self, key: str):
return self.payload[key]
- def __setitem__(self, key, value):
+ def __setitem__(self, key: str, value: Any) -> None:
self.payload[key] = value
- def __delitem__(self, key):
+ def __delitem__(self, key: str) -> None:
del self.payload[key]
- def __contains__(self, key):
+ def __contains__(self, key: str) -> Any:
return key in self.payload
- def get(self, key, default=None):
+ def get(self, key: str, default: Optional[Any] = None) -> Any:
return self.payload.get(key, default)
- def __str__(self):
+ def __str__(self) -> str:
"""
Signs and returns a token as a base64 encoded string.
"""
return self.get_token_backend().encode(self.payload)
- def verify(self):
+ def verify(self) -> None:
"""
Performs additional validation steps which were not performed when this
token was decoded. This method is part of the "public" API to indicate
@@ -92,25 +117,31 @@ def verify(self):
# claim. We don't want any zombie tokens walking around.
self.check_exp()
- # Ensure token id is present
- if api_settings.JTI_CLAIM not in self.payload:
+ # If the defaults are not None then we should enforce the
+ # requirement of these settings.As above, the spec labels
+ # these as optional.
+ if (
+ api_settings.JTI_CLAIM is not None
+ and api_settings.JTI_CLAIM not in self.payload
+ ):
raise TokenError(_("Token has no id"))
- self.verify_token_type()
+ if api_settings.TOKEN_TYPE_CLAIM is not None:
+ self.verify_token_type()
- def verify_token_type(self):
+ def verify_token_type(self) -> None:
"""
Ensures that the token type claim is present and has the correct value.
"""
try:
token_type = self.payload[api_settings.TOKEN_TYPE_CLAIM]
- except KeyError:
- raise TokenError(_("Token has no type"))
+ except KeyError as e:
+ raise TokenError(_("Token has no type")) from e
if self.token_type != token_type:
raise TokenError(_("Token has wrong type"))
- def set_jti(self):
+ def set_jti(self) -> None:
"""
Populates the configured jti claim of a token with a string where there
is a negligible probability that the same string will be chosen at a
@@ -121,7 +152,12 @@ def set_jti(self):
"""
self.payload[api_settings.JTI_CLAIM] = uuid4().hex
- def set_exp(self, claim="exp", from_time=None, lifetime=None):
+ def set_exp(
+ self,
+ claim: str = "exp",
+ from_time: Optional[datetime] = None,
+ lifetime: Optional[timedelta] = None,
+ ) -> None:
"""
Updates the expiration time of a token.
@@ -136,7 +172,7 @@ def set_exp(self, claim="exp", from_time=None, lifetime=None):
self.payload[claim] = datetime_to_epoch(from_time + lifetime)
- def set_iat(self, claim="iat", at_time=None):
+ def set_iat(self, claim: str = "iat", at_time: Optional[datetime] = None) -> None:
"""
Updates the time at which the token was issued.
@@ -148,7 +184,9 @@ def set_iat(self, claim="iat", at_time=None):
self.payload[claim] = datetime_to_epoch(at_time)
- def check_exp(self, claim="exp", current_time=None):
+ def check_exp(
+ self, claim: str = "exp", current_time: Optional[datetime] = None
+ ) -> None:
"""
Checks whether a timestamp value in the given claim has passed (since
the given datetime value in `current_time`). Raises a TokenError with
@@ -159,39 +197,62 @@ def check_exp(self, claim="exp", current_time=None):
try:
claim_value = self.payload[claim]
- except KeyError:
- raise TokenError(format_lazy(_("Token has no '{}' claim"), claim))
+ except KeyError as e:
+ raise TokenError(format_lazy(_("Token has no '{}' claim"), claim)) from e
claim_time = datetime_from_epoch(claim_value)
- if claim_time <= current_time:
+ leeway = self.get_token_backend().get_leeway()
+ if claim_time <= current_time - leeway:
raise TokenError(format_lazy(_("Token '{}' claim has expired"), claim))
+ def outstand(self) -> Optional[OutstandingToken]:
+ """
+ Ensures this token is included in the outstanding token list and
+ adds it to the outstanding token list if not.
+ """
+ return None
+
@classmethod
- def for_user(cls, user):
+ def for_user(cls: type[T], user: AuthUser) -> T:
"""
Returns an authorization token for the given user that will be provided
after authenticating the user's credentials.
"""
+
+ if hasattr(user, "is_active") and not user.is_active:
+ logger.warning(
+ f"Creating token for inactive user: {user.pk}. If this is not intentional, consider checking the user's status before calling the `for_user` method."
+ )
+
user_id = getattr(user, api_settings.USER_ID_FIELD)
- if not isinstance(user_id, int):
- user_id = str(user_id)
+ user_id = str(user_id)
token = cls()
token[api_settings.USER_ID_CLAIM] = user_id
+ if api_settings.CHECK_REVOKE_TOKEN:
+ token[api_settings.REVOKE_TOKEN_CLAIM] = get_md5_hash_password(
+ user.password
+ )
+
return token
- _token_backend = None
+ _token_backend: Optional["TokenBackend"] = None
- def get_token_backend(self):
+ @property
+ def token_backend(self) -> "TokenBackend":
if self._token_backend is None:
self._token_backend = import_string(
"rest_framework_simplejwt.state.token_backend"
)
return self._token_backend
+ def get_token_backend(self) -> "TokenBackend":
+ # Backward compatibility.
+ return self.token_backend
+
-class BlacklistMixin:
+class BlacklistMixin(Generic[T]):
"""
If the `rest_framework_simplejwt.token_blacklist` app was configured to be
used, tokens created from `BlacklistMixin` subclasses will insert
@@ -199,14 +260,16 @@ class BlacklistMixin:
membership in a token blacklist.
"""
+ payload: dict[str, Any]
+
if "rest_framework_simplejwt.token_blacklist" in settings.INSTALLED_APPS:
- def verify(self, *args, **kwargs):
+ def verify(self, *args, **kwargs) -> None:
self.check_blacklist()
- super().verify(*args, **kwargs)
+ super().verify(*args, **kwargs) # type: ignore
- def check_blacklist(self):
+ def check_blacklist(self) -> None:
"""
Checks if this token is present in the token blacklist. Raises
`TokenError` if so.
@@ -216,18 +279,26 @@ def check_blacklist(self):
if BlacklistedToken.objects.filter(token__jti=jti).exists():
raise TokenError(_("Token is blacklisted"))
- def blacklist(self):
+ def blacklist(self) -> BlacklistedToken:
"""
Ensures this token is included in the outstanding token list and
adds it to the blacklist.
"""
jti = self.payload[api_settings.JTI_CLAIM]
exp = self.payload["exp"]
+ user_id = self.payload.get(api_settings.USER_ID_CLAIM)
+ User = get_user_model()
+ try:
+ user = User.objects.get(**{api_settings.USER_ID_FIELD: user_id})
+ except User.DoesNotExist:
+ user = None
# Ensure outstanding token exists with given jti
token, _ = OutstandingToken.objects.get_or_create(
jti=jti,
defaults={
+ "user": user,
+ "created_at": self.current_time,
"token": str(self),
"expires_at": datetime_from_epoch(exp),
},
@@ -235,12 +306,37 @@ def blacklist(self):
return BlacklistedToken.objects.get_or_create(token=token)
+ def outstand(self) -> Optional[OutstandingToken]:
+ """
+ Ensures this token is included in the outstanding token list and
+ adds it to the outstanding token list if not.
+ """
+ jti = self.payload[api_settings.JTI_CLAIM]
+ exp = self.payload["exp"]
+ user_id = self.payload.get(api_settings.USER_ID_CLAIM)
+ User = get_user_model()
+ try:
+ user = User.objects.get(**{api_settings.USER_ID_FIELD: user_id})
+ except User.DoesNotExist:
+ user = None
+
+ # Ensure outstanding token exists with given jti
+ return OutstandingToken.objects.get_or_create(
+ jti=jti,
+ defaults={
+ "user": user,
+ "created_at": self.current_time,
+ "token": str(self),
+ "expires_at": datetime_from_epoch(exp),
+ },
+ )
+
@classmethod
- def for_user(cls, user):
+ def for_user(cls: type[T], user: AuthUser) -> T:
"""
Adds this token to the outstanding token list.
"""
- token = super().for_user(user)
+ token = super().for_user(user) # type: ignore
jti = token[api_settings.JTI_CLAIM]
exp = token["exp"]
@@ -256,11 +352,11 @@ def for_user(cls, user):
return token
-class SlidingToken(BlacklistMixin, Token):
+class SlidingToken(BlacklistMixin["SlidingToken"], Token):
token_type = "sliding"
lifetime = api_settings.SLIDING_TOKEN_LIFETIME
- def __init__(self, *args, **kwargs):
+ def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if self.token is None:
@@ -272,7 +368,12 @@ def __init__(self, *args, **kwargs):
)
-class RefreshToken(BlacklistMixin, Token):
+class AccessToken(Token):
+ token_type = "access"
+ lifetime = api_settings.ACCESS_TOKEN_LIFETIME
+
+
+class RefreshToken(BlacklistMixin["RefreshToken"], Token):
token_type = "refresh"
lifetime = api_settings.REFRESH_TOKEN_LIFETIME
no_copy_claims = (
@@ -284,16 +385,18 @@ class RefreshToken(BlacklistMixin, Token):
# we wouldn't want to copy either one.
api_settings.JTI_CLAIM,
"jti",
+ "iat",
)
+ access_token_class = AccessToken
@property
- def access_token(self):
+ def access_token(self) -> AccessToken:
"""
Returns an access token created from this refresh token. Copies all
claims present in this refresh token to the new access token except
those claims listed in the `no_copy_claims` attribute.
"""
- access = AccessToken()
+ access = self.access_token_class()
# Use instantiation time of refresh token as relative timestamp for
# access token "exp" claim. This ensures that both a refresh and
@@ -310,16 +413,11 @@ def access_token(self):
return access
-class AccessToken(Token):
- token_type = "access"
- lifetime = api_settings.ACCESS_TOKEN_LIFETIME
-
-
class UntypedToken(Token):
token_type = "untyped"
lifetime = timedelta(seconds=0)
- def verify_token_type(self):
+ def verify_token_type(self) -> None:
"""
Untyped tokens do not verify the "token_type" claim. This is useful
when performing general validation of a token's signature and other
diff --git a/rest_framework_simplejwt/utils.py b/rest_framework_simplejwt/utils.py
index 48e950ad6..202f12e9e 100644
--- a/rest_framework_simplejwt/utils.py
+++ b/rest_framework_simplejwt/utils.py
@@ -1,32 +1,51 @@
+import hashlib
+import logging
from calendar import timegm
-from datetime import datetime
+from datetime import datetime, timezone
+from typing import Callable
from django.conf import settings
from django.utils.functional import lazy
-from django.utils.timezone import is_naive, make_aware, utc
-def make_utc(dt):
- if settings.USE_TZ and is_naive(dt):
- return make_aware(dt, timezone=utc)
+def get_md5_hash_password(password: str) -> str:
+ """
+ Returns MD5 hash of the given password
+ """
+ return hashlib.md5(password.encode()).hexdigest().upper()
+
+
+def make_utc(dt: datetime) -> datetime:
+ if settings.USE_TZ and dt.tzinfo is None:
+ return dt.replace(tzinfo=timezone.utc)
return dt
-def aware_utcnow():
- return make_utc(datetime.utcnow())
+def aware_utcnow() -> datetime:
+ dt = datetime.now(tz=timezone.utc)
+ if not settings.USE_TZ:
+ dt = dt.replace(tzinfo=None)
+
+ return dt
-def datetime_to_epoch(dt):
+def datetime_to_epoch(dt: datetime) -> int:
return timegm(dt.utctimetuple())
-def datetime_from_epoch(ts):
- return make_utc(datetime.utcfromtimestamp(ts))
+def datetime_from_epoch(ts: float) -> datetime:
+ dt = datetime.fromtimestamp(ts, tz=timezone.utc)
+ if not settings.USE_TZ:
+ dt = dt.replace(tzinfo=None)
+
+ return dt
-def format_lazy(s, *args, **kwargs):
+def format_lazy(s: str, *args, **kwargs) -> str:
return s.format(*args, **kwargs)
-format_lazy = lazy(format_lazy, str)
+format_lazy: Callable = lazy(format_lazy, str)
+
+logger = logging.getLogger("rest_framework_simplejwt")
diff --git a/rest_framework_simplejwt/views.py b/rest_framework_simplejwt/views.py
index 7a13eb59d..f0c257dc6 100644
--- a/rest_framework_simplejwt/views.py
+++ b/rest_framework_simplejwt/views.py
@@ -1,32 +1,51 @@
+from typing import Optional
+
+from django.utils.module_loading import import_string
from rest_framework import generics, status
+from rest_framework.request import Request
from rest_framework.response import Response
+from rest_framework.serializers import BaseSerializer
-from . import serializers
from .authentication import AUTH_HEADER_TYPES
from .exceptions import InvalidToken, TokenError
+from .settings import api_settings
class TokenViewBase(generics.GenericAPIView):
permission_classes = ()
authentication_classes = ()
- serializer_class = None
+ serializer_class: Optional[type[BaseSerializer]] = None
+ _serializer_class = ""
www_authenticate_realm = "api"
- def get_authenticate_header(self, request):
+ def get_serializer_class(self) -> type[BaseSerializer]:
+ """
+ If serializer_class is set, use it directly. Otherwise get the class from settings.
+ """
+
+ if self.serializer_class:
+ return self.serializer_class
+ try:
+ return import_string(self._serializer_class)
+ except ImportError as e:
+ msg = f"Could not import serializer '{self._serializer_class}'"
+ raise ImportError(msg) from e
+
+ def get_authenticate_header(self, request: Request) -> str:
return '{} realm="{}"'.format(
AUTH_HEADER_TYPES[0],
self.www_authenticate_realm,
)
- def post(self, request, *args, **kwargs):
+ def post(self, request: Request, *args, **kwargs) -> Response:
serializer = self.get_serializer(data=request.data)
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
- raise InvalidToken(e.args[0])
+ raise InvalidToken(e.args[0]) from e
return Response(serializer.validated_data, status=status.HTTP_200_OK)
@@ -37,7 +56,7 @@ class TokenObtainPairView(TokenViewBase):
token pair to prove the authentication of those credentials.
"""
- serializer_class = serializers.TokenObtainPairSerializer
+ _serializer_class = api_settings.TOKEN_OBTAIN_SERIALIZER
token_obtain_pair = TokenObtainPairView.as_view()
@@ -49,7 +68,7 @@ class TokenRefreshView(TokenViewBase):
token if the refresh token is valid.
"""
- serializer_class = serializers.TokenRefreshSerializer
+ _serializer_class = api_settings.TOKEN_REFRESH_SERIALIZER
token_refresh = TokenRefreshView.as_view()
@@ -61,7 +80,7 @@ class TokenObtainSlidingView(TokenViewBase):
prove the authentication of those credentials.
"""
- serializer_class = serializers.TokenObtainSlidingSerializer
+ _serializer_class = api_settings.SLIDING_TOKEN_OBTAIN_SERIALIZER
token_obtain_sliding = TokenObtainSlidingView.as_view()
@@ -73,7 +92,7 @@ class TokenRefreshSlidingView(TokenViewBase):
token's refresh period has not expired.
"""
- serializer_class = serializers.TokenRefreshSlidingSerializer
+ _serializer_class = api_settings.SLIDING_TOKEN_REFRESH_SERIALIZER
token_refresh_sliding = TokenRefreshSlidingView.as_view()
@@ -85,7 +104,7 @@ class TokenVerifyView(TokenViewBase):
information about a token's fitness for a particular use.
"""
- serializer_class = serializers.TokenVerifySerializer
+ _serializer_class = api_settings.TOKEN_VERIFY_SERIALIZER
token_verify = TokenVerifyView.as_view()
@@ -97,7 +116,7 @@ class TokenBlacklistView(TokenViewBase):
`rest_framework_simplejwt.token_blacklist` app installed.
"""
- serializer_class = serializers.TokenBlacklistSerializer
+ _serializer_class = api_settings.TOKEN_BLACKLIST_SERIALIZER
token_blacklist = TokenBlacklistView.as_view()
diff --git a/scripts/i18n_updater.py b/scripts/i18n_updater.py
new file mode 100644
index 000000000..bea42c762
--- /dev/null
+++ b/scripts/i18n_updater.py
@@ -0,0 +1,58 @@
+import contextlib
+import os
+import subprocess
+
+
+def get_list_of_files(dir_name: str, extension: str):
+ file_list = os.listdir(dir_name)
+
+ result = []
+ for entry in file_list:
+ full_path = os.path.join(dir_name, entry)
+ if os.path.isdir(full_path):
+ result = result + get_list_of_files(full_path, extension)
+ else:
+ if entry[-len(extension) : len(entry)] == extension:
+ result.append(full_path)
+
+ return result
+
+
+@contextlib.contextmanager
+def cache_creation():
+ # DO NOT cache the line number; the file may change
+ cache: dict[str, str] = {}
+ for file in get_list_of_files("./", ".po"):
+ if os.path.isdir(file):
+ continue
+
+ with open(file) as f:
+ for line in f.readlines():
+ if line.startswith('"POT-Creation-Date: '):
+ cache[file] = line
+ break
+ yield
+ for file, line_cache in cache.items():
+ with open(file, "r+") as f:
+ lines = f.readlines()
+ # clear file
+ f.seek(0)
+ f.truncate()
+
+ # find line
+ index = [
+ lines.index(x) for x in lines if x.startswith('"POT-Creation-Date: ')
+ ][0]
+
+ lines[index] = line_cache
+ f.writelines(lines)
+
+
+def main():
+ with cache_creation():
+ subprocess.run(["django-admin", "makemessages", "-a"])
+ subprocess.run(["django-admin", "compilemessages"])
+
+
+if __name__ == "__main__":
+ main()
diff --git a/setup.py b/setup.py
index 3d4e7e235..675f07f67 100755
--- a/setup.py
+++ b/setup.py
@@ -6,6 +6,7 @@
extras_require = {
"test": [
"cryptography",
+ "freezegun",
"pytest-cov",
"pytest-django",
"pytest-xdist",
@@ -13,12 +14,19 @@
"tox",
],
"lint": [
- "flake8",
- "pep8",
- "isort",
+ "ruff",
+ "yesqa",
+ "pyupgrade",
+ "pre-commit",
+ ],
+ "typing": [
+ "mypy",
+ "django-stubs",
+ "djangorestframework-stubs",
+ "types-pytz",
],
"doc": [
- "Sphinx>=1.6.5,<2",
+ "Sphinx",
"sphinx_rtd_theme>=0.1.9",
],
"dev": [
@@ -30,12 +38,16 @@
"python-jose": [
"python-jose==3.3.0",
],
+ "crypto": [
+ "cryptography>=3.3.1",
+ ],
}
extras_require["dev"] = (
extras_require["dev"]
+ extras_require["test"]
+ extras_require["lint"]
+ + extras_require["typing"]
+ extras_require["doc"]
+ extras_require["python-jose"]
)
@@ -52,11 +64,11 @@
author="David Sanders",
author_email="davesque@gmail.com",
install_requires=[
- "django",
- "djangorestframework",
- "pyjwt>=2,<3",
+ "django>=4.2",
+ "djangorestframework>=3.14",
+ "pyjwt>=1.7.1",
],
- python_requires=">=3.7",
+ python_requires=">=3.9",
extras_require=extras_require,
packages=find_packages(exclude=["tests", "tests.*", "licenses", "requirements"]),
include_package_data=True,
@@ -65,18 +77,20 @@
"Development Status :: 5 - Production/Stable",
"Environment :: Web Environment",
"Framework :: Django",
- "Framework :: Django :: 2.2",
- "Framework :: Django :: 3.1",
- "Framework :: Django :: 3.2",
+ "Framework :: Django :: 4.2",
+ "Framework :: Django :: 5.0",
+ "Framework :: Django :: 5.1",
+ "Framework :: Django :: 5.2",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Topic :: Internet :: WWW/HTTP",
],
)
diff --git a/tests/keys.py b/tests/keys.py
index 09a977de0..aa93d7d95 100644
--- a/tests/keys.py
+++ b/tests/keys.py
@@ -110,3 +110,18 @@
E01hmaHk9xlOpo73IjUxhXUCAwEAAQ==
-----END PUBLIC KEY-----
"""
+
+ES256_PRIVATE_KEY = """
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIMtBPxiLHcJCrAGdz4jHvTtAh6Rw7351AckG3whXq2WOoAoGCCqGSM49
+AwEHoUQDQgAEMZHyNxbkr7+zqQ1dQk/zug2pwYdztmjhpC+XqK88q5NfIS1cBYYt
+zhHUS4vGpazNqbW8HA3ZIvJRmx4L96O6/w==
+-----END EC PRIVATE KEY-----
+"""
+
+ES256_PUBLIC_KEY = """
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMZHyNxbkr7+zqQ1dQk/zug2pwYdz
+tmjhpC+XqK88q5NfIS1cBYYtzhHUS4vGpazNqbW8HA3ZIvJRmx4L96O6/w==
+-----END PUBLIC KEY-----
+"""
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index a8f4ff4b8..b17a12caa 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -10,6 +10,7 @@
from rest_framework_simplejwt.models import TokenUser
from rest_framework_simplejwt.settings import api_settings
from rest_framework_simplejwt.tokens import AccessToken, SlidingToken
+from rest_framework_simplejwt.utils import get_md5_hash_password
from .utils import override_api_settings
@@ -40,25 +41,19 @@ def test_get_header(self):
)
self.assertEqual(self.backend.get_header(request), self.fake_header)
- # Should work with the x_access_token
- with override_api_settings(AUTH_HEADER_NAME="HTTP_X_ACCESS_TOKEN"):
- # Should pull correct header off request when using X_ACCESS_TOKEN
- request = self.factory.get(
- "/test-url/", HTTP_X_ACCESS_TOKEN=self.fake_header
- )
- self.assertEqual(self.backend.get_header(request), self.fake_header)
+ @override_api_settings(AUTH_HEADER_NAME="HTTP_X_ACCESS_TOKEN")
+ def test_get_header_x_access_token(self):
+ # Should pull correct header off request when using X_ACCESS_TOKEN
+ request = self.factory.get("/test-url/", HTTP_X_ACCESS_TOKEN=self.fake_header)
+ self.assertEqual(self.backend.get_header(request), self.fake_header)
- # Should work for unicode headers when using
- request = self.factory.get(
- "/test-url/", HTTP_X_ACCESS_TOKEN=self.fake_header.decode("utf-8")
- )
- self.assertEqual(self.backend.get_header(request), self.fake_header)
+ # Should work for unicode headers when using
+ request = self.factory.get(
+ "/test-url/", HTTP_X_ACCESS_TOKEN=self.fake_header.decode("utf-8")
+ )
+ self.assertEqual(self.backend.get_header(request), self.fake_header)
def test_get_raw_token(self):
- # Should return None if header lacks correct type keyword
- with override_api_settings(AUTH_HEADER_TYPES="JWT"):
- reload(authentication)
- self.assertIsNone(self.backend.get_raw_token(self.fake_header))
reload(authentication)
# Should return None if an empty AUTHORIZATION header is sent
@@ -74,14 +69,21 @@ def test_get_raw_token(self):
# Otherwise, should return unvalidated token in header
self.assertEqual(self.backend.get_raw_token(self.fake_header), self.fake_token)
+ @override_api_settings(AUTH_HEADER_TYPES="JWT")
+ def test_get_raw_token_incorrect_header_keyword(self):
+ # Should return None if header lacks correct type keyword
+ # AUTH_HEADER_TYPES is "JWT", but header is "Bearer"
+ reload(authentication)
+ self.assertIsNone(self.backend.get_raw_token(self.fake_header))
+
+ @override_api_settings(AUTH_HEADER_TYPES=("JWT", "Bearer"))
+ def test_get_raw_token_multi_header_keyword(self):
# Should return token if header has one of many valid token types
- with override_api_settings(AUTH_HEADER_TYPES=("JWT", "Bearer")):
- reload(authentication)
- self.assertEqual(
- self.backend.get_raw_token(self.fake_header),
- self.fake_token,
- )
reload(authentication)
+ self.assertEqual(
+ self.backend.get_raw_token(self.fake_header),
+ self.fake_token,
+ )
def test_get_validated_token(self):
# Should raise InvalidToken if token not valid
@@ -96,36 +98,39 @@ def test_get_validated_token(self):
self.backend.get_validated_token(str(token)).payload, token.payload
)
+ @override_api_settings(
+ AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.AccessToken",),
+ )
+ def test_get_validated_token_reject_unknown_token(self):
# Should not accept tokens not included in AUTH_TOKEN_CLASSES
sliding_token = SlidingToken()
- with override_api_settings(
- AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.AccessToken",)
- ):
- with self.assertRaises(InvalidToken) as e:
- self.backend.get_validated_token(str(sliding_token))
-
- messages = e.exception.detail["messages"]
- self.assertEqual(1, len(messages))
- self.assertEqual(
- {
- "token_class": "AccessToken",
- "token_type": "access",
- "message": "Token has wrong type",
- },
- messages[0],
- )
+ with self.assertRaises(InvalidToken) as e:
+ self.backend.get_validated_token(str(sliding_token))
+
+ messages = e.exception.detail["messages"]
+ self.assertEqual(1, len(messages))
+ self.assertEqual(
+ {
+ "token_class": "AccessToken",
+ "token_type": "access",
+ "message": "Token has wrong type",
+ },
+ messages[0],
+ )
+ @override_api_settings(
+ AUTH_TOKEN_CLASSES=(
+ "rest_framework_simplejwt.tokens.AccessToken",
+ "rest_framework_simplejwt.tokens.SlidingToken",
+ ),
+ )
+ def test_get_validated_token_accept_known_token(self):
# Should accept tokens included in AUTH_TOKEN_CLASSES
access_token = AccessToken()
sliding_token = SlidingToken()
- with override_api_settings(
- AUTH_TOKEN_CLASSES=(
- "rest_framework_simplejwt.tokens.AccessToken",
- "rest_framework_simplejwt.tokens.SlidingToken",
- )
- ):
- self.backend.get_validated_token(str(access_token))
- self.backend.get_validated_token(str(sliding_token))
+
+ self.backend.get_validated_token(str(access_token))
+ self.backend.get_validated_token(str(sliding_token))
def test_get_user(self):
payload = {"some_other_id": "foo"}
@@ -156,10 +161,85 @@ def test_get_user(self):
# Otherwise, should return correct user
self.assertEqual(self.backend.get_user(payload).id, u.id)
+ @override_api_settings(
+ CHECK_USER_IS_ACTIVE=False,
+ )
+ def test_get_inactive_user(self):
+ payload = {"some_other_id": "foo"}
+
+ # Should raise error if no recognizable user identification
+ with self.assertRaises(InvalidToken):
+ self.backend.get_user(payload)
+
+ payload[api_settings.USER_ID_CLAIM] = 42
+
+ # Should raise exception if user not found
+ with self.assertRaises(AuthenticationFailed):
+ self.backend.get_user(payload)
+
+ u = User.objects.create_user(username="markhamill")
+ u.is_active = False
+ u.save()
+
+ payload[api_settings.USER_ID_CLAIM] = getattr(u, api_settings.USER_ID_FIELD)
+
+ # should return correct user
+ self.assertEqual(self.backend.get_user(payload).id, u.id)
+
+ @override_api_settings(
+ CHECK_REVOKE_TOKEN=True, REVOKE_TOKEN_CLAIM="revoke_token_claim"
+ )
+ def test_get_user_with_check_revoke_token(self):
+ payload = {"some_other_id": "foo"}
+
+ # Should raise error if no recognizable user identification
+ with self.assertRaises(InvalidToken):
+ self.backend.get_user(payload)
+
+ payload[api_settings.USER_ID_CLAIM] = 42
+
+ # Should raise exception if user not found
+ with self.assertRaises(AuthenticationFailed):
+ self.backend.get_user(payload)
+
+ u = User.objects.create_user(username="markhamill")
+ u.is_active = False
+ u.save()
+
+ payload[api_settings.USER_ID_CLAIM] = getattr(u, api_settings.USER_ID_FIELD)
+
+ # Should raise exception if user is inactive
+ with self.assertRaises(AuthenticationFailed):
+ self.backend.get_user(payload)
+
+ u.is_active = True
+ u.save()
+
+ # Should raise exception if hash password is different
+ with self.assertRaises(AuthenticationFailed):
+ self.backend.get_user(payload)
+
+ if api_settings.CHECK_REVOKE_TOKEN:
+ payload[api_settings.REVOKE_TOKEN_CLAIM] = get_md5_hash_password(u.password)
+
+ # Otherwise, should return correct user
+ self.assertEqual(self.backend.get_user(payload).id, u.id)
+
+ def test_get_user_with_str_user_id_claim(self):
+ """
+ Verify that even though the user id is an int, it can be verified
+ and retrieved if the user id claim value is a string
+ """
+
+ user = User.objects.create_user(username="testuser")
+ payload = {api_settings.USER_ID_CLAIM: str(user.id)}
+ auth_user = self.backend.get_user(payload)
+ self.assertEqual(auth_user.id, user.id)
+
-class TestJWTTokenUserAuthentication(TestCase):
+class TestJWTStatelessUserAuthentication(TestCase):
def setUp(self):
- self.backend = authentication.JWTTokenUserAuthentication()
+ self.backend = authentication.JWTStatelessUserAuthentication()
def test_get_user(self):
payload = {"some_other_id": "foo"}
diff --git a/tests/test_backends.py b/tests/test_backends.py
index d6e16398b..0d7cce944 100644
--- a/tests/test_backends.py
+++ b/tests/test_backends.py
@@ -1,15 +1,31 @@
+import builtins
+import uuid
from datetime import datetime, timedelta
+from importlib import reload
+from json import JSONEncoder
from unittest import mock
from unittest.mock import patch
import jwt
+import pytest
from django.test import TestCase
from jwt import PyJWS, algorithms
+from jwt import __version__ as jwt_version
-from rest_framework_simplejwt.backends import TokenBackend
-from rest_framework_simplejwt.exceptions import TokenBackendError
+from rest_framework_simplejwt.backends import JWK_CLIENT_AVAILABLE, TokenBackend
+from rest_framework_simplejwt.exceptions import (
+ TokenBackendError,
+ TokenBackendExpiredToken,
+)
from rest_framework_simplejwt.utils import aware_utcnow, datetime_to_epoch, make_utc
-from tests.keys import PRIVATE_KEY, PRIVATE_KEY_2, PUBLIC_KEY, PUBLIC_KEY_2
+from tests.keys import (
+ ES256_PRIVATE_KEY,
+ ES256_PUBLIC_KEY,
+ PRIVATE_KEY,
+ PRIVATE_KEY_2,
+ PUBLIC_KEY,
+ PUBLIC_KEY_2,
+)
SECRET = "not_secret"
@@ -21,9 +37,19 @@
LEEWAY = 100
+IS_OLD_JWT = jwt_version == "1.7.1"
+
+
+class UUIDJSONEncoder(JSONEncoder):
+ def default(self, obj):
+ if isinstance(obj, uuid.UUID):
+ return str(obj)
+ return super().default(obj)
+
class TestTokenBackend(TestCase):
def setUp(self):
+ self.realimport = builtins.__import__
self.hmac_token_backend = TokenBackend("HS256", SECRET)
self.hmac_leeway_token_backend = TokenBackend("HS256", SECRET, leeway=LEEWAY)
self.rsa_token_backend = TokenBackend("RS256", PRIVATE_KEY, PUBLIC_KEY)
@@ -31,6 +57,13 @@ def setUp(self):
"RS256", PRIVATE_KEY, PUBLIC_KEY, AUDIENCE, ISSUER
)
self.payload = {"foo": "bar"}
+ self.backends = (
+ self.hmac_token_backend,
+ self.rsa_token_backend,
+ TokenBackend("ES256", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY),
+ TokenBackend("ES384", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY),
+ TokenBackend("ES512", ES256_PRIVATE_KEY, ES256_PUBLIC_KEY),
+ )
def test_init(self):
# Should reject unknown algorithms
@@ -41,18 +74,34 @@ def test_init(self):
@patch.object(algorithms, "has_crypto", new=False)
def test_init_fails_for_rs_algorithms_when_crypto_not_installed(self):
- with self.assertRaisesRegex(
- TokenBackendError, "You must have cryptography installed to use RS256."
- ):
- TokenBackend("RS256", "not_secret")
- with self.assertRaisesRegex(
- TokenBackendError, "You must have cryptography installed to use RS384."
- ):
- TokenBackend("RS384", "not_secret")
- with self.assertRaisesRegex(
- TokenBackendError, "You must have cryptography installed to use RS512."
- ):
- TokenBackend("RS512", "not_secret")
+ for algo in ("RS256", "RS384", "RS512", "ES256"):
+ with self.assertRaisesRegex(
+ TokenBackendError,
+ f"You must have cryptography installed to use {algo}.",
+ ):
+ TokenBackend(algo, "not_secret")
+
+ def test_jwk_client_not_available(self):
+ from rest_framework_simplejwt import backends
+
+ def myimport(name, globals=None, locals=None, fromlist=(), level=0):
+ if name == "jwt" and fromlist == ("PyJWKClient", "PyJWKClientError"):
+ raise ImportError
+ return self.realimport(name, globals, locals, fromlist, level)
+
+ builtins.__import__ = myimport
+
+ # Reload backends, mock jwk client is not available
+ reload(backends)
+
+ self.assertEqual(backends.JWK_CLIENT_AVAILABLE, False)
+ self.assertEqual(backends.TokenBackend("HS256").jwks_client, None)
+
+ builtins.__import__ = self.realimport
+
+ @patch("jwt.encode", mock.Mock(return_value=b"test"))
+ def test_token_encode_should_return_str_for_old_PyJWT(self):
+ self.assertIsInstance(TokenBackend("HS256").encode({}), str)
def test_encode_hmac(self):
# Should return a JSON web token for the given payload
@@ -113,127 +162,110 @@ def test_encode_aud_iss(self):
),
)
- def test_decode_hmac_with_no_expiry(self):
- no_exp_token = jwt.encode(self.payload, SECRET, algorithm="HS256")
+ def test_decode_with_no_expiry(self):
+ for backend in self.backends:
+ with self.subTest("Test decode with no expiry for f{backend.algorithm}"):
+ no_exp_token = jwt.encode(
+ self.payload, backend.signing_key, algorithm=backend.algorithm
+ )
- self.hmac_token_backend.decode(no_exp_token)
+ backend.decode(no_exp_token)
- def test_decode_hmac_with_no_expiry_no_verify(self):
- no_exp_token = jwt.encode(self.payload, SECRET, algorithm="HS256")
+ def test_decode_with_no_expiry_no_verify(self):
+ for backend in self.backends:
+ with self.subTest(
+ "Test decode with no expiry and no verify for f{backend.algorithm}"
+ ):
+ no_exp_token = jwt.encode(
+ self.payload, backend.signing_key, algorithm=backend.algorithm
+ )
- self.assertEqual(
- self.hmac_token_backend.decode(no_exp_token, verify=False),
- self.payload,
- )
+ self.assertEqual(
+ backend.decode(no_exp_token, verify=False),
+ self.payload,
+ )
- def test_decode_hmac_with_expiry(self):
+ def test_decode_with_expiry(self):
self.payload["exp"] = aware_utcnow() - timedelta(seconds=1)
+ for backend in self.backends:
+ with self.subTest("Test decode with expiry for f{backend.algorithm}"):
+ expired_token = jwt.encode(
+ self.payload, backend.signing_key, algorithm=backend.algorithm
+ )
- expired_token = jwt.encode(self.payload, SECRET, algorithm="HS256")
-
- with self.assertRaises(TokenBackendError):
- self.hmac_token_backend.decode(expired_token)
-
- def test_decode_hmac_with_invalid_sig(self):
- self.payload["exp"] = aware_utcnow() + timedelta(days=1)
- token_1 = jwt.encode(self.payload, SECRET, algorithm="HS256")
- self.payload["foo"] = "baz"
- token_2 = jwt.encode(self.payload, SECRET, algorithm="HS256")
-
- token_2_payload = token_2.rsplit(".", 1)[0]
- token_1_sig = token_1.rsplit(".", 1)[-1]
- invalid_token = token_2_payload + "." + token_1_sig
-
- with self.assertRaises(TokenBackendError):
- self.hmac_token_backend.decode(invalid_token)
+ with self.assertRaises(TokenBackendExpiredToken):
+ backend.decode(expired_token)
- def test_decode_hmac_with_invalid_sig_no_verify(self):
- self.payload["exp"] = aware_utcnow() + timedelta(days=1)
- token_1 = jwt.encode(self.payload, SECRET, algorithm="HS256")
- self.payload["foo"] = "baz"
- token_2 = jwt.encode(self.payload, SECRET, algorithm="HS256")
- # Payload copied
- self.payload["exp"] = datetime_to_epoch(self.payload["exp"])
-
- token_2_payload = token_2.rsplit(".", 1)[0]
- token_1_sig = token_1.rsplit(".", 1)[-1]
- invalid_token = token_2_payload + "." + token_1_sig
-
- self.assertEqual(
- self.hmac_token_backend.decode(invalid_token, verify=False),
- self.payload,
- )
-
- def test_decode_hmac_success(self):
- self.payload["exp"] = aware_utcnow() + timedelta(days=1)
- self.payload["foo"] = "baz"
-
- token = jwt.encode(self.payload, SECRET, algorithm="HS256")
- # Payload copied
- self.payload["exp"] = datetime_to_epoch(self.payload["exp"])
-
- self.assertEqual(self.hmac_token_backend.decode(token), self.payload)
-
- def test_decode_rsa_with_no_expiry(self):
- no_exp_token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
-
- self.rsa_token_backend.decode(no_exp_token)
-
- def test_decode_rsa_with_no_expiry_no_verify(self):
- no_exp_token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
-
- self.assertEqual(
- self.hmac_token_backend.decode(no_exp_token, verify=False),
- self.payload,
- )
-
- def test_decode_rsa_with_expiry(self):
+ def test_decode_with_invalid_sig(self):
self.payload["exp"] = aware_utcnow() - timedelta(seconds=1)
-
- expired_token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
-
- with self.assertRaises(TokenBackendError):
- self.rsa_token_backend.decode(expired_token)
-
- def test_decode_rsa_with_invalid_sig(self):
+ for backend in self.backends:
+ with self.subTest(f"Test decode with invalid sig for {backend.algorithm}"):
+ payload = self.payload.copy()
+ payload["exp"] = aware_utcnow() + timedelta(days=1)
+ token_1 = jwt.encode(
+ payload, backend.signing_key, algorithm=backend.algorithm
+ )
+ payload["foo"] = "baz"
+ token_2 = jwt.encode(
+ payload, backend.signing_key, algorithm=backend.algorithm
+ )
+
+ if IS_OLD_JWT:
+ token_1 = token_1.decode("utf-8")
+ token_2 = token_2.decode("utf-8")
+
+ token_2_payload = token_2.rsplit(".", 1)[0]
+ token_1_sig = token_1.rsplit(".", 1)[-1]
+ invalid_token = token_2_payload + "." + token_1_sig
+
+ with self.assertRaises(TokenBackendError):
+ backend.decode(invalid_token)
+
+ def test_decode_with_invalid_sig_no_verify(self):
self.payload["exp"] = aware_utcnow() + timedelta(days=1)
- token_1 = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
- self.payload["foo"] = "baz"
- token_2 = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
-
- token_2_payload = token_2.rsplit(".", 1)[0]
- token_1_sig = token_1.rsplit(".", 1)[-1]
- invalid_token = token_2_payload + "." + token_1_sig
-
- with self.assertRaises(TokenBackendError):
- self.rsa_token_backend.decode(invalid_token)
-
- def test_decode_rsa_with_invalid_sig_no_verify(self):
+ for backend in self.backends:
+ with self.subTest("Test decode with invalid sig for f{backend.algorithm}"):
+ payload = self.payload.copy()
+ token_1 = jwt.encode(
+ payload, backend.signing_key, algorithm=backend.algorithm
+ )
+ payload["foo"] = "baz"
+ token_2 = jwt.encode(
+ payload, backend.signing_key, algorithm=backend.algorithm
+ )
+ if IS_OLD_JWT:
+ token_1 = token_1.decode("utf-8")
+ token_2 = token_2.decode("utf-8")
+ else:
+ # Payload copied
+ payload["exp"] = datetime_to_epoch(payload["exp"])
+
+ token_2_payload = token_2.rsplit(".", 1)[0]
+ token_1_sig = token_1.rsplit(".", 1)[-1]
+ invalid_token = token_2_payload + "." + token_1_sig
+
+ self.assertEqual(
+ backend.decode(invalid_token, verify=False),
+ payload,
+ )
+
+ def test_decode_success(self):
self.payload["exp"] = aware_utcnow() + timedelta(days=1)
- token_1 = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
self.payload["foo"] = "baz"
- token_2 = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
-
- token_2_payload = token_2.rsplit(".", 1)[0]
- token_1_sig = token_1.rsplit(".", 1)[-1]
- invalid_token = token_2_payload + "." + token_1_sig
- # Payload copied
- self.payload["exp"] = datetime_to_epoch(self.payload["exp"])
-
- self.assertEqual(
- self.hmac_token_backend.decode(invalid_token, verify=False),
- self.payload,
- )
-
- def test_decode_rsa_success(self):
- self.payload["exp"] = aware_utcnow() + timedelta(days=1)
- self.payload["foo"] = "baz"
-
- token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
- # Payload copied
- self.payload["exp"] = datetime_to_epoch(self.payload["exp"])
-
- self.assertEqual(self.rsa_token_backend.decode(token), self.payload)
+ for backend in self.backends:
+ with self.subTest("Test decode success for f{backend.algorithm}"):
+ token = jwt.encode(
+ self.payload, backend.signing_key, algorithm=backend.algorithm
+ )
+ if IS_OLD_JWT:
+ token = token.decode("utf-8")
+ payload = self.payload
+ else:
+ # Payload copied
+ payload = self.payload.copy()
+ payload["exp"] = datetime_to_epoch(self.payload["exp"])
+
+ self.assertEqual(backend.decode(token), payload)
def test_decode_aud_iss_success(self):
self.payload["exp"] = aware_utcnow() + timedelta(days=1)
@@ -242,11 +274,18 @@ def test_decode_aud_iss_success(self):
self.payload["iss"] = ISSUER
token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
- # Payload copied
- self.payload["exp"] = datetime_to_epoch(self.payload["exp"])
+ if IS_OLD_JWT:
+ token = token.decode("utf-8")
+ else:
+ # Payload copied
+ self.payload["exp"] = datetime_to_epoch(self.payload["exp"])
self.assertEqual(self.aud_iss_token_backend.decode(token), self.payload)
+ @pytest.mark.skipif(
+ not JWK_CLIENT_AVAILABLE,
+ reason="PyJWT 1.7.1 doesn't have JWK client",
+ )
def test_decode_rsa_aud_iss_jwk_success(self):
self.payload["exp"] = aware_utcnow() + timedelta(days=1)
self.payload["foo"] = "baz"
@@ -262,7 +301,6 @@ def test_decode_rsa_aud_iss_jwk_success(self):
# Payload copied
self.payload["exp"] = datetime_to_epoch(self.payload["exp"])
- mock_jwk_module = mock.MagicMock()
with patch("rest_framework_simplejwt.backends.PyJWKClient") as mock_jwk_module:
mock_jwk_client = mock.MagicMock()
mock_signing_key = mock.MagicMock()
@@ -278,8 +316,43 @@ def test_decode_rsa_aud_iss_jwk_success(self):
self.assertEqual(jwk_token_backend.decode(token), self.payload)
+ @pytest.mark.skipif(
+ not JWK_CLIENT_AVAILABLE,
+ reason="PyJWT 1.7.1 doesn't have JWK client",
+ )
+ def test_decode_jwk_missing_key_raises_tokenbackenderror(self):
+ self.payload["exp"] = aware_utcnow() + timedelta(days=1)
+ self.payload["foo"] = "baz"
+ self.payload["aud"] = AUDIENCE
+ self.payload["iss"] = ISSUER
+
+ token = jwt.encode(
+ self.payload,
+ PRIVATE_KEY_2,
+ algorithm="RS256",
+ headers={"kid": "230498151c214b788dd97f22b85410a5"},
+ )
+
+ with patch("rest_framework_simplejwt.backends.PyJWKClient") as mock_jwk_module:
+ mock_jwk_client = mock.MagicMock()
+
+ mock_jwk_module.return_value = mock_jwk_client
+ mock_jwk_client.get_signing_key_from_jwt.side_effect = jwt.PyJWKClientError(
+ "Unable to find a signing key that matches"
+ )
+
+ # Note the PRIV,PUB care is intentially the original pairing
+ jwk_token_backend = TokenBackend(
+ "RS256", PRIVATE_KEY, PUBLIC_KEY, AUDIENCE, ISSUER, JWK_URL
+ )
+
+ with self.assertRaisesRegex(TokenBackendError, "Token is invalid"):
+ jwk_token_backend.decode(token)
+
def test_decode_when_algorithm_not_available(self):
token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
+ if IS_OLD_JWT:
+ token = token.decode("utf-8")
pyjwt_without_rsa = PyJWS()
pyjwt_without_rsa.unregister_algorithm("RS256")
@@ -295,6 +368,8 @@ def _decode(jwt, key, algorithms, options, audience, issuer, leeway):
def test_decode_when_token_algorithm_does_not_match(self):
token = jwt.encode(self.payload, PRIVATE_KEY, algorithm="RS256")
+ if IS_OLD_JWT:
+ token = token.decode("utf-8")
with self.assertRaisesRegex(TokenBackendError, "Invalid algorithm specified"):
self.hmac_token_backend.decode(token)
@@ -320,3 +395,11 @@ def test_decode_leeway_hmac_success(self):
self.hmac_leeway_token_backend.decode(expired_token),
self.payload,
)
+
+ def test_custom_JSONEncoder(self):
+ backend = TokenBackend("HS256", SECRET, json_encoder=UUIDJSONEncoder)
+ unique = uuid.uuid4()
+ self.payload["uuid"] = unique
+ token = backend.encode(self.payload)
+ decoded = backend.decode(token)
+ self.assertEqual(decoded["uuid"], str(unique))
diff --git a/tests/test_init.py b/tests/test_init.py
new file mode 100644
index 000000000..e99a0f7d2
--- /dev/null
+++ b/tests/test_init.py
@@ -0,0 +1,18 @@
+from importlib import reload
+from importlib.metadata import PackageNotFoundError
+from unittest.mock import Mock, patch
+
+from django.test import SimpleTestCase
+
+
+class TestInit(SimpleTestCase):
+ def test_package_is_not_installed(self):
+ with patch(
+ "importlib.metadata.version", Mock(side_effect=PackageNotFoundError)
+ ):
+ import rest_framework_simplejwt.__init__
+
+ self.assertEqual(rest_framework_simplejwt.__init__.__version__, None)
+
+ # Restore origin package without mock
+ reload(rest_framework_simplejwt.__init__)
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 919a626dd..beee3d552 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -1,8 +1,9 @@
from datetime import timedelta
from django.contrib.auth import get_user_model
+from django.urls import reverse
+from rest_framework.status import HTTP_200_OK, HTTP_401_UNAUTHORIZED
-from rest_framework_simplejwt.compat import reverse
from rest_framework_simplejwt.settings import api_settings
from rest_framework_simplejwt.tokens import AccessToken
@@ -26,7 +27,7 @@ def setUp(self):
def test_no_authorization(self):
res = self.view_get()
- self.assertEqual(res.status_code, 401)
+ self.assertEqual(res.status_code, HTTP_401_UNAUTHORIZED)
self.assertIn("credentials were not provided", res.data["detail"])
def test_wrong_auth_type(self):
@@ -43,9 +44,12 @@ def test_wrong_auth_type(self):
res = self.view_get()
- self.assertEqual(res.status_code, 401)
+ self.assertEqual(res.status_code, HTTP_401_UNAUTHORIZED)
self.assertIn("credentials were not provided", res.data["detail"])
+ @override_api_settings(
+ AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.AccessToken",),
+ )
def test_expired_token(self):
old_lifetime = AccessToken.lifetime
AccessToken.lifetime = timedelta(seconds=0)
@@ -63,14 +67,14 @@ def test_expired_token(self):
access = res.data["access"]
self.authenticate_with_token(api_settings.AUTH_HEADER_TYPES[0], access)
- with override_api_settings(
- AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.AccessToken",)
- ):
- res = self.view_get()
+ res = self.view_get()
- self.assertEqual(res.status_code, 401)
+ self.assertEqual(res.status_code, HTTP_401_UNAUTHORIZED)
self.assertEqual("token_not_valid", res.data["code"])
+ @override_api_settings(
+ AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.SlidingToken",),
+ )
def test_user_can_get_sliding_token_and_use_it(self):
res = self.client.post(
reverse("token_obtain_sliding"),
@@ -83,14 +87,14 @@ def test_user_can_get_sliding_token_and_use_it(self):
token = res.data["token"]
self.authenticate_with_token(api_settings.AUTH_HEADER_TYPES[0], token)
- with override_api_settings(
- AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.SlidingToken",)
- ):
- res = self.view_get()
+ res = self.view_get()
- self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.status_code, HTTP_200_OK)
self.assertEqual(res.data["foo"], "bar")
+ @override_api_settings(
+ AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.AccessToken",),
+ )
def test_user_can_get_access_and_refresh_tokens_and_use_them(self):
res = self.client.post(
reverse("token_obtain_pair"),
@@ -105,12 +109,9 @@ def test_user_can_get_access_and_refresh_tokens_and_use_them(self):
self.authenticate_with_token(api_settings.AUTH_HEADER_TYPES[0], access)
- with override_api_settings(
- AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.AccessToken",)
- ):
- res = self.view_get()
+ res = self.view_get()
- self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.status_code, HTTP_200_OK)
self.assertEqual(res.data["foo"], "bar")
res = self.client.post(
@@ -122,10 +123,7 @@ def test_user_can_get_access_and_refresh_tokens_and_use_them(self):
self.authenticate_with_token(api_settings.AUTH_HEADER_TYPES[0], access)
- with override_api_settings(
- AUTH_TOKEN_CLASSES=("rest_framework_simplejwt.tokens.AccessToken",)
- ):
- res = self.view_get()
+ res = self.view_get()
- self.assertEqual(res.status_code, 200)
+ self.assertEqual(res.status_code, HTTP_200_OK)
self.assertEqual(res.data["foo"], "bar")
diff --git a/tests/test_migrations.py b/tests/test_migrations.py
new file mode 100644
index 000000000..6e8d08b47
--- /dev/null
+++ b/tests/test_migrations.py
@@ -0,0 +1,42 @@
+from io import StringIO
+from typing import Optional
+
+import pytest
+from django.core.management import call_command
+from django.test import TestCase
+
+
+class MigrationTestCase(TestCase):
+ def test_no_missing_migrations(self):
+ """
+ Ensures all model changes are reflected in migrations.
+ If this test fails, there are model changes that require a new migration.
+
+ Behavior:
+ - Passes silently if no migrations are required
+ - Fails with a detailed message if migrations are need
+ """
+
+ output = StringIO()
+
+ # Initialize exception variable to track migration check result
+ exec: Optional[SystemExit] = None
+
+ try:
+ # Check for pending migrations without actually creating them
+ call_command(
+ "makemigrations", "--check", "--dry-run", stdout=output, stderr=output
+ )
+ except SystemExit as e:
+ # Capture the SystemExit if migrations are needed (the command will had ended with exit code 1)
+ exec = e
+
+ # If an exception was raised, verify it indicates no migration changes are required
+ if exec is not None:
+ self.assertEqual(
+ exec.code,
+ 0, # 0 means no migrations needed
+ f"Model changes detected that require migrations!\n"
+ f"Please run `python manage.py makemigrations` to create the necessary migrations.\n\n"
+ f"Detected Changes:\n{output.getvalue()}",
+ )
diff --git a/tests/test_models.py b/tests/test_models.py
index 3a017fbfe..719470639 100644
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,3 +1,6 @@
+from importlib import reload
+from unittest.mock import patch
+
from django.test import TestCase
from rest_framework_simplejwt.models import TokenUser
@@ -9,12 +12,24 @@
class TestTokenUser(TestCase):
def setUp(self):
self.token = AuthToken()
- self.token[api_settings.USER_ID_CLAIM] = 42
+ self.token[api_settings.USER_ID_CLAIM] = "42"
self.token["username"] = "deep-thought"
self.token["some_other_stuff"] = "arstarst"
self.user = TokenUser(self.token)
+ def test_type_checking(self):
+ from rest_framework_simplejwt import models
+
+ with patch("typing.TYPE_CHECKING", True):
+ # Reload models, mock type checking
+ reload(models)
+
+ self.assertEqual(models.TYPE_CHECKING, True)
+
+ # Restore origin module without mock
+ reload(models)
+
def test_username(self):
self.assertEqual(self.user.username, "deep-thought")
@@ -25,13 +40,13 @@ def test_str(self):
self.assertEqual(str(self.user), "TokenUser 42")
def test_id(self):
- self.assertEqual(self.user.id, 42)
+ self.assertEqual(self.user.id, "42")
def test_pk(self):
- self.assertEqual(self.user.pk, 42)
+ self.assertEqual(self.user.pk, "42")
def test_is_staff(self):
- payload = {api_settings.USER_ID_CLAIM: 42}
+ payload = {api_settings.USER_ID_CLAIM: "42"}
user = TokenUser(payload)
self.assertFalse(user.is_staff)
@@ -42,7 +57,7 @@ def test_is_staff(self):
self.assertTrue(user.is_staff)
def test_is_superuser(self):
- payload = {api_settings.USER_ID_CLAIM: 42}
+ payload = {api_settings.USER_ID_CLAIM: "42"}
user = TokenUser(payload)
self.assertFalse(user.is_superuser)
@@ -53,13 +68,19 @@ def test_is_superuser(self):
self.assertTrue(user.is_superuser)
def test_eq(self):
- user1 = TokenUser({api_settings.USER_ID_CLAIM: 1})
- user2 = TokenUser({api_settings.USER_ID_CLAIM: 2})
- user3 = TokenUser({api_settings.USER_ID_CLAIM: 1})
+ user1 = TokenUser({api_settings.USER_ID_CLAIM: "1"})
+ user2 = TokenUser({api_settings.USER_ID_CLAIM: "2"})
+ user3 = TokenUser({api_settings.USER_ID_CLAIM: "1"})
self.assertNotEqual(user1, user2)
self.assertEqual(user1, user3)
+ def test_eq_not_implemented(self):
+ user1 = TokenUser({api_settings.USER_ID_CLAIM: "1"})
+ user2 = "user2"
+
+ self.assertFalse(user1 == user2)
+
def test_hash(self):
self.assertEqual(hash(self.user), hash(self.user.id))
@@ -105,3 +126,6 @@ def test_is_authenticated(self):
def test_get_username(self):
self.assertEqual(self.user.get_username(), "deep-thought")
+
+ def test_get_custom_claims_through_backup_getattr(self):
+ self.assertEqual(self.user.some_other_stuff, "arstarst")
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
index 530f689cd..ed8645e54 100644
--- a/tests/test_serializers.py
+++ b/tests/test_serializers.py
@@ -1,8 +1,10 @@
from datetime import timedelta
+from importlib import reload
from unittest.mock import MagicMock, patch
+from django.conf import settings
from django.contrib.auth import get_user_model
-from django.test import TestCase
+from django.test import TestCase, override_settings
from rest_framework import exceptions as drf_exceptions
from rest_framework_simplejwt.exceptions import TokenError
@@ -76,6 +78,17 @@ def test_it_should_not_validate_if_user_not_found(self):
with self.assertRaises(drf_exceptions.AuthenticationFailed):
s.is_valid()
+ def test_it_should_pass_validate_if_request_not_in_context(self):
+ s = TokenObtainSerializer(
+ context={},
+ data={
+ "username": self.username,
+ "password": self.password,
+ },
+ )
+
+ s.is_valid()
+
def test_it_should_raise_if_user_not_active(self):
self.user.is_active = False
self.user.save()
@@ -91,6 +104,29 @@ def test_it_should_raise_if_user_not_active(self):
with self.assertRaises(drf_exceptions.AuthenticationFailed):
s.is_valid()
+ @override_settings(
+ AUTHENTICATION_BACKENDS=[
+ "django.contrib.auth.backends.AllowAllUsersModelBackend",
+ "django.contrib.auth.backends.ModelBackend",
+ ]
+ )
+ @override_api_settings(
+ CHECK_USER_IS_ACTIVE=False,
+ )
+ def test_it_should_validate_if_user_inactive_but_rule_allows(self):
+ self.user.is_active = False
+ self.user.save()
+
+ s = TokenObtainSerializer(
+ context=MagicMock(),
+ data={
+ TokenObtainSerializer.username_field: self.username,
+ "password": self.password,
+ },
+ )
+
+ self.assertTrue(s.is_valid())
+
class TestTokenObtainSlidingSerializer(TestCase):
def setUp(self):
@@ -151,6 +187,15 @@ def test_it_should_produce_a_json_web_token_when_valid(self):
class TestTokenRefreshSlidingSerializer(TestCase):
+ def setUp(self):
+ self.username = "test_user"
+ self.password = "test_password"
+
+ self.user = User.objects.create_user(
+ username=self.username,
+ password=self.password,
+ )
+
def test_it_should_not_validate_if_token_invalid(self):
token = SlidingToken()
del token["exp"]
@@ -169,7 +214,7 @@ def test_it_should_not_validate_if_token_invalid(self):
with self.assertRaises(TokenError) as e:
s.is_valid()
- self.assertIn("invalid or expired", e.exception.args[0])
+ self.assertIn("expired", e.exception.args[0])
def test_it_should_raise_token_error_if_token_has_no_refresh_exp_claim(self):
token = SlidingToken()
@@ -232,8 +277,125 @@ def test_it_should_update_token_exp_claim_if_everything_ok(self):
self.assertTrue(old_exp < new_exp)
+ def test_it_should_raise_error_for_deleted_users(self):
+ token = SlidingToken.for_user(self.user)
+ self.user.delete()
+
+ s = TokenRefreshSlidingSerializer(data={"token": str(token)})
+
+ # It should raise AuthenticationFailed instead of ObjectDoesNotExist
+ with self.assertRaises(drf_exceptions.AuthenticationFailed) as e:
+ s.is_valid()
+
+ self.assertEqual(e.exception.get_codes(), "no_active_account")
+
+ def test_it_should_raise_error_for_inactive_users(self):
+ token = SlidingToken.for_user(self.user)
+ self.user.is_active = False
+ self.user.save()
+
+ s = TokenRefreshSlidingSerializer(data={"token": str(token)})
+
+ with self.assertRaises(drf_exceptions.AuthenticationFailed) as e:
+ s.is_valid()
+
+ self.assertEqual(e.exception.get_codes(), "no_active_account")
+
+ @override_api_settings(
+ CHECK_REVOKE_TOKEN=True,
+ REVOKE_TOKEN_CLAIM="hash_password",
+ BLACKLIST_AFTER_ROTATION=False,
+ )
+ def test_sliding_token_should_fail_after_password_change(self):
+ """
+ Tests that sliding token refresh fails if CHECK_REVOKE_TOKEN is True and the
+ user's password has changed.
+ """
+ token = SlidingToken.for_user(self.user)
+ self.user.set_password("new_password")
+ self.user.save()
+
+ s = TokenRefreshSlidingSerializer(data={"token": str(token)})
+
+ with self.assertRaises(drf_exceptions.AuthenticationFailed) as e:
+ s.is_valid(raise_exception=True)
+
+ self.assertEqual(e.exception.get_codes(), "password_changed")
+
+ @override_api_settings(
+ CHECK_REVOKE_TOKEN=True,
+ REVOKE_TOKEN_CLAIM="hash_password",
+ BLACKLIST_AFTER_ROTATION=True,
+ )
+ def test_sliding_token_should_blacklist_after_password_change(self):
+ """
+ Tests that if sliding token refresh fails due to a password change, the
+ offending token is blacklisted.
+ """
+ token = SlidingToken.for_user(self.user)
+ self.user.set_password("new_password")
+ self.user.save()
+
+ s = TokenRefreshSlidingSerializer(data={"token": str(token)})
+ with self.assertRaises(drf_exceptions.AuthenticationFailed):
+ s.is_valid(raise_exception=True)
+
+ # Check that the token is now in the blacklist
+ jti = token[api_settings.JTI_CLAIM]
+ self.assertTrue(OutstandingToken.objects.filter(jti=jti).exists())
+ self.assertTrue(BlacklistedToken.objects.filter(token__jti=jti).exists())
+
class TestTokenRefreshSerializer(TestCase):
+ def setUp(self):
+ self.username = "test_user"
+ self.password = "test_password"
+
+ self.user = User.objects.create_user(
+ username=self.username,
+ password=self.password,
+ )
+
+ def test_it_should_raise_error_for_deleted_users(self):
+ refresh = RefreshToken.for_user(self.user)
+ self.user.delete()
+
+ s = TokenRefreshSerializer(data={"refresh": str(refresh)})
+
+ # It should raise AuthenticationFailed instead of ObjectDoesNotExist
+ with self.assertRaises(drf_exceptions.AuthenticationFailed) as e:
+ s.is_valid()
+
+ self.assertEqual(e.exception.get_codes(), "no_active_account")
+
+ def test_it_should_raise_error_for_inactive_users(self):
+ refresh = RefreshToken.for_user(self.user)
+ self.user.is_active = False
+ self.user.save()
+
+ s = TokenRefreshSerializer(data={"refresh": str(refresh)})
+
+ with self.assertRaises(drf_exceptions.AuthenticationFailed) as e:
+ s.is_valid()
+
+ self.assertIn("No active account", e.exception.args[0])
+
+ def test_it_should_return_access_token_for_active_users(self):
+ refresh = RefreshToken.for_user(self.user)
+
+ s = TokenRefreshSerializer(data={"refresh": str(refresh)})
+
+ now = aware_utcnow() - api_settings.ACCESS_TOKEN_LIFETIME / 2
+ with patch("rest_framework_simplejwt.tokens.aware_utcnow") as fake_aware_utcnow:
+ fake_aware_utcnow.return_value = now
+ s.is_valid()
+
+ access = AccessToken(s.validated_data["access"])
+
+ self.assertEqual(
+ access["exp"], datetime_to_epoch(now + api_settings.ACCESS_TOKEN_LIFETIME)
+ )
+
def test_it_should_raise_token_error_if_token_invalid(self):
token = RefreshToken()
del token["exp"]
@@ -252,7 +414,7 @@ def test_it_should_raise_token_error_if_token_invalid(self):
with self.assertRaises(TokenError) as e:
s.is_valid()
- self.assertIn("invalid or expired", e.exception.args[0])
+ self.assertIn("expired", e.exception.args[0])
def test_it_should_raise_token_error_if_token_has_wrong_type(self):
token = RefreshToken()
@@ -285,6 +447,10 @@ def test_it_should_return_access_token_if_everything_ok(self):
access["exp"], datetime_to_epoch(now + api_settings.ACCESS_TOKEN_LIFETIME)
)
+ @override_api_settings(
+ ROTATE_REFRESH_TOKENS=True,
+ BLACKLIST_AFTER_ROTATION=False,
+ )
def test_it_should_return_refresh_token_if_tokens_should_be_rotated(self):
refresh = RefreshToken()
@@ -298,14 +464,9 @@ def test_it_should_return_refresh_token_if_tokens_should_be_rotated(self):
now = aware_utcnow() - api_settings.ACCESS_TOKEN_LIFETIME / 2
- with override_api_settings(
- ROTATE_REFRESH_TOKENS=True, BLACKLIST_AFTER_ROTATION=False
- ):
- with patch(
- "rest_framework_simplejwt.tokens.aware_utcnow"
- ) as fake_aware_utcnow:
- fake_aware_utcnow.return_value = now
- self.assertTrue(ser.is_valid())
+ with patch("rest_framework_simplejwt.tokens.aware_utcnow") as fake_aware_utcnow:
+ fake_aware_utcnow.return_value = now
+ self.assertTrue(ser.is_valid())
access = AccessToken(ser.validated_data["access"])
new_refresh = RefreshToken(ser.validated_data["refresh"])
@@ -324,6 +485,10 @@ def test_it_should_return_refresh_token_if_tokens_should_be_rotated(self):
datetime_to_epoch(now + api_settings.REFRESH_TOKEN_LIFETIME),
)
+ @override_api_settings(
+ ROTATE_REFRESH_TOKENS=True,
+ BLACKLIST_AFTER_ROTATION=True,
+ )
def test_it_should_blacklist_refresh_token_if_tokens_should_be_rotated_and_blacklisted(
self,
):
@@ -342,14 +507,9 @@ def test_it_should_blacklist_refresh_token_if_tokens_should_be_rotated_and_black
now = aware_utcnow() - api_settings.ACCESS_TOKEN_LIFETIME / 2
- with override_api_settings(
- ROTATE_REFRESH_TOKENS=True, BLACKLIST_AFTER_ROTATION=True
- ):
- with patch(
- "rest_framework_simplejwt.tokens.aware_utcnow"
- ) as fake_aware_utcnow:
- fake_aware_utcnow.return_value = now
- self.assertTrue(ser.is_valid())
+ with patch("rest_framework_simplejwt.tokens.aware_utcnow") as fake_aware_utcnow:
+ fake_aware_utcnow.return_value = now
+ self.assertTrue(ser.is_valid())
access = AccessToken(ser.validated_data["access"])
new_refresh = RefreshToken(ser.validated_data["refresh"])
@@ -368,12 +528,82 @@ def test_it_should_blacklist_refresh_token_if_tokens_should_be_rotated_and_black
datetime_to_epoch(now + api_settings.REFRESH_TOKEN_LIFETIME),
)
- self.assertEqual(OutstandingToken.objects.count(), 1)
+ self.assertEqual(OutstandingToken.objects.count(), 2)
self.assertEqual(BlacklistedToken.objects.count(), 1)
# Assert old refresh token is blacklisted
self.assertEqual(BlacklistedToken.objects.first().token.jti, old_jti)
+ @override_api_settings(
+ ROTATE_REFRESH_TOKENS=True,
+ BLACKLIST_AFTER_ROTATION=True,
+ )
+ def test_blacklist_app_not_installed_should_pass(self):
+ from rest_framework_simplejwt import serializers, tokens
+
+ # Remove blacklist app
+ new_apps = list(settings.INSTALLED_APPS)
+ new_apps.remove("rest_framework_simplejwt.token_blacklist")
+
+ with self.settings(INSTALLED_APPS=tuple(new_apps)):
+ # Reload module that blacklist app not installed
+ reload(tokens)
+ reload(serializers)
+
+ refresh = tokens.RefreshToken()
+
+ # Serializer validates
+ ser = serializers.TokenRefreshSerializer(data={"refresh": str(refresh)})
+ ser.validate({"refresh": str(refresh)})
+
+ # Restore origin module without mock
+ reload(tokens)
+ reload(serializers)
+
+ @override_api_settings(
+ CHECK_REVOKE_TOKEN=True,
+ REVOKE_TOKEN_CLAIM="hash_password",
+ BLACKLIST_AFTER_ROTATION=False,
+ )
+ def test_refresh_token_should_fail_after_password_change(self):
+ """
+ Tests that token refresh fails if CHECK_REVOKE_TOKEN is True and the
+ user's password has changed.
+ """
+ refresh = RefreshToken.for_user(self.user)
+ self.user.set_password("new_password")
+ self.user.save()
+
+ s = TokenRefreshSerializer(data={"refresh": str(refresh)})
+
+ with self.assertRaises(drf_exceptions.AuthenticationFailed) as e:
+ s.is_valid(raise_exception=True)
+
+ self.assertEqual(e.exception.get_codes(), "password_changed")
+
+ @override_api_settings(
+ CHECK_REVOKE_TOKEN=True,
+ REVOKE_TOKEN_CLAIM="hash_password",
+ BLACKLIST_AFTER_ROTATION=True,
+ )
+ def test_refresh_token_should_blacklist_after_password_change(self):
+ """
+ Tests that if token refresh fails due to a password change, the
+ offending refresh token is blacklisted.
+ """
+ refresh = RefreshToken.for_user(self.user)
+ self.user.set_password("new_password")
+ self.user.save()
+
+ s = TokenRefreshSerializer(data={"refresh": str(refresh)})
+ with self.assertRaises(drf_exceptions.AuthenticationFailed):
+ s.is_valid(raise_exception=True)
+
+ # Check that the token is now in the blacklist
+ jti = refresh[api_settings.JTI_CLAIM]
+ self.assertTrue(OutstandingToken.objects.filter(jti=jti).exists())
+ self.assertTrue(BlacklistedToken.objects.filter(token__jti=jti).exists())
+
class TestTokenVerifySerializer(TestCase):
def test_it_should_raise_token_error_if_token_invalid(self):
@@ -394,7 +624,7 @@ def test_it_should_raise_token_error_if_token_invalid(self):
with self.assertRaises(TokenError) as e:
s.is_valid()
- self.assertIn("invalid or expired", e.exception.args[0])
+ self.assertIn("expired", e.exception.args[0])
def test_it_should_not_raise_token_error_if_token_has_wrong_type(self):
token = RefreshToken()
@@ -439,7 +669,7 @@ def test_it_should_raise_token_error_if_token_invalid(self):
with self.assertRaises(TokenError) as e:
s.is_valid()
- self.assertIn("invalid or expired", e.exception.args[0])
+ self.assertIn("expired", e.exception.args[0])
def test_it_should_raise_token_error_if_token_has_wrong_type(self):
token = RefreshToken()
@@ -491,3 +721,25 @@ def test_it_should_blacklist_refresh_token_if_everything_ok(self):
# Assert old refresh token is blacklisted
self.assertEqual(BlacklistedToken.objects.first().token.jti, old_jti)
+
+ def test_blacklist_app_not_installed_should_pass(self):
+ from rest_framework_simplejwt import serializers, tokens
+
+ # Remove blacklist app
+ new_apps = list(settings.INSTALLED_APPS)
+ new_apps.remove("rest_framework_simplejwt.token_blacklist")
+
+ with self.settings(INSTALLED_APPS=tuple(new_apps)):
+ # Reload module that blacklist app not installed
+ reload(tokens)
+ reload(serializers)
+
+ refresh = tokens.RefreshToken()
+
+ # Serializer validates
+ ser = serializers.TokenBlacklistSerializer(data={"refresh": str(refresh)})
+ ser.validate({"refresh": str(refresh)})
+
+ # Restore origin module without mock
+ reload(tokens)
+ reload(serializers)
diff --git a/tests/test_token_blacklist.py b/tests/test_token_blacklist.py
index 8286c936b..03bada52c 100644
--- a/tests/test_token_blacklist.py
+++ b/tests/test_token_blacklist.py
@@ -1,9 +1,11 @@
+from importlib import reload
from unittest.mock import patch
from django.contrib.auth.models import User
from django.core.management import call_command
from django.db.models import BigAutoField
from django.test import TestCase
+from django.utils import timezone
from rest_framework_simplejwt.exceptions import TokenError
from rest_framework_simplejwt.serializers import TokenVerifySerializer
@@ -25,6 +27,19 @@ def setUp(self):
password="test_password",
)
+ def test_token_blacklist_old_django(self):
+ with patch("django.VERSION", (3, 1)):
+ # Import package mock blacklist old django
+ import rest_framework_simplejwt.token_blacklist.__init__ as blacklist
+
+ self.assertEqual(
+ blacklist.default_app_config,
+ ("rest_framework_simplejwt.token_blacklist.apps.TokenBlacklistConfig"),
+ )
+
+ # Restore origin module without mock
+ reload(blacklist)
+
def test_sliding_tokens_are_added_to_outstanding_list(self):
token = SlidingToken.for_user(self.user)
@@ -114,6 +129,65 @@ def test_tokens_can_be_manually_blacklisted(self):
self.assertEqual(OutstandingToken.objects.count(), 2)
+ def test_outstanding_token_and_blacklisted_token_expected_str(self):
+ outstanding = OutstandingToken.objects.create(
+ user=self.user,
+ jti="abc",
+ token="xyz",
+ expires_at=timezone.now(),
+ )
+ blacklisted = BlacklistedToken.objects.create(token=outstanding)
+
+ expected_outstanding_str = "Token for {} ({})".format(
+ outstanding.user, outstanding.jti
+ )
+ expected_blacklisted_str = f"Blacklisted token for {blacklisted.token.user}"
+
+ self.assertEqual(str(outstanding), expected_outstanding_str)
+ self.assertEqual(str(blacklisted), expected_blacklisted_str)
+
+ def test_outstanding_token_and_blacklisted_token_created_at(self):
+ token = RefreshToken.for_user(self.user)
+
+ token.blacklist()
+ outstanding_token = OutstandingToken.objects.get(token=token)
+ self.assertEqual(outstanding_token.created_at, token.current_time)
+
+ def test_outstanding_token_and_blacklisted_token_user(self):
+ token = RefreshToken.for_user(self.user)
+
+ token.blacklist()
+ outstanding_token = OutstandingToken.objects.get(token=token)
+ self.assertEqual(outstanding_token.user, self.user)
+
+ @override_api_settings(USER_ID_FIELD="email", USER_ID_CLAIM="email")
+ def test_outstanding_token_and_blacklisted_token_created_at_with_modified_user_id_field(
+ self,
+ ):
+ token = RefreshToken.for_user(self.user)
+
+ token.blacklist()
+ outstanding_token = OutstandingToken.objects.get(token=token)
+ self.assertEqual(outstanding_token.created_at, token.current_time)
+
+ @override_api_settings(USER_ID_FIELD="email", USER_ID_CLAIM="email")
+ def test_outstanding_token_and_blacklisted_token_user_with_modifed_user_id_field(
+ self,
+ ):
+ token = RefreshToken.for_user(self.user)
+
+ token.blacklist()
+ outstanding_token = OutstandingToken.objects.get(token=token)
+ self.assertEqual(outstanding_token.user, self.user)
+
+ @override_api_settings(USER_ID_FIELD="email", USER_ID_CLAIM="email")
+ def test_outstanding_token_with_deleted_user_and_modifed_user_id_field(self):
+ self.assertFalse(BlacklistedToken.objects.exists())
+ token = RefreshToken.for_user(self.user)
+ self.user.delete()
+ token.blacklist()
+ self.assertTrue(BlacklistedToken.objects.count(), 1)
+
class TestTokenBlacklistFlushExpiredTokens(TestCase):
def setUp(self):
@@ -172,6 +246,29 @@ def test_it_should_delete_any_expired_tokens(self):
[not_expired_2["jti"], not_expired_3["jti"]],
)
+ def test_token_blacklist_will_not_be_removed_on_User_delete(self):
+ token = RefreshToken.for_user(self.user)
+ outstanding_token = OutstandingToken.objects.first()
+
+ # Should raise no exception
+ RefreshToken(str(token))
+
+ # Add token to blacklist
+ BlacklistedToken.objects.create(token=outstanding_token)
+
+ with self.assertRaises(TokenError) as e:
+ # Should raise exception
+ RefreshToken(str(token))
+ self.assertIn("blacklisted", e.exception.args[0])
+
+ # Delete the User and try again
+ self.user.delete()
+
+ with self.assertRaises(TokenError) as e:
+ # Should raise exception
+ RefreshToken(str(token))
+ self.assertIn("blacklisted", e.exception.args[0])
+
class TestPopulateJtiHexMigration(MigrationTestCase):
migrate_from = ("token_blacklist", "0002_outstandingtoken_jti_hex")
@@ -214,25 +311,25 @@ def setUp(self):
super().setUp()
+ @override_api_settings(BLACKLIST_AFTER_ROTATION=True)
def test_token_verify_serializer_should_honour_blacklist_if_blacklisting_enabled(
self,
):
- with override_api_settings(BLACKLIST_AFTER_ROTATION=True):
- refresh_token = RefreshToken.for_user(self.user)
- refresh_token.blacklist()
+ refresh_token = RefreshToken.for_user(self.user)
+ refresh_token.blacklist()
- serializer = TokenVerifySerializer(data={"token": str(refresh_token)})
- self.assertFalse(serializer.is_valid())
+ serializer = TokenVerifySerializer(data={"token": str(refresh_token)})
+ self.assertFalse(serializer.is_valid())
+ @override_api_settings(BLACKLIST_AFTER_ROTATION=False)
def test_token_verify_serializer_should_not_honour_blacklist_if_blacklisting_not_enabled(
self,
):
- with override_api_settings(BLACKLIST_AFTER_ROTATION=False):
- refresh_token = RefreshToken.for_user(self.user)
- refresh_token.blacklist()
+ refresh_token = RefreshToken.for_user(self.user)
+ refresh_token.blacklist()
- serializer = TokenVerifySerializer(data={"token": str(refresh_token)})
- self.assertTrue(serializer.is_valid())
+ serializer = TokenVerifySerializer(data={"token": str(refresh_token)})
+ self.assertTrue(serializer.is_valid())
class TestBigAutoFieldIDMigration(MigrationTestCase):
diff --git a/tests/test_tokens.py b/tests/test_tokens.py
index 8e5670030..30aeed53f 100644
--- a/tests/test_tokens.py
+++ b/tests/test_tokens.py
@@ -1,11 +1,17 @@
from datetime import datetime, timedelta
+from importlib import reload
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.test import TestCase
+from freezegun import freeze_time
from jose import jwt
-from rest_framework_simplejwt.exceptions import TokenError
+from rest_framework_simplejwt.exceptions import (
+ ExpiredTokenError,
+ TokenBackendError,
+ TokenError,
+)
from rest_framework_simplejwt.settings import api_settings
from rest_framework_simplejwt.state import token_backend
from rest_framework_simplejwt.tokens import (
@@ -31,6 +37,26 @@ class TestToken(TestCase):
def setUp(self):
self.token = MyToken()
+ @classmethod
+ def setUpTestData(cls):
+ cls.username = "test_user"
+ cls.user = User.objects.create_user(
+ username=cls.username,
+ password="test_password",
+ )
+
+ def test_type_checking(self):
+ from rest_framework_simplejwt import tokens
+
+ with patch("typing.TYPE_CHECKING", True):
+ # Reload tokens, mock type checking
+ reload(tokens)
+
+ self.assertEqual(tokens.TYPE_CHECKING, True)
+
+ # Restore origin module without mock
+ reload(tokens)
+
def test_init_no_token_type_or_lifetime(self):
class MyTestToken(Token):
pass
@@ -136,7 +162,7 @@ def test_init_expired_token_given(self):
t = MyToken()
t.set_exp(lifetime=-timedelta(seconds=1))
- with self.assertRaises(TokenError):
+ with self.assertRaises(ExpiredTokenError):
MyToken(str(t))
def test_init_no_type_token_given(self):
@@ -225,6 +251,16 @@ def test_set_jti(self):
self.assertIn("jti", token)
self.assertNotEqual(old_jti, token["jti"])
+ @override_api_settings(JTI_CLAIM=None)
+ def test_optional_jti(self):
+ token = MyToken()
+ self.assertNotIn("jti", token)
+
+ @override_api_settings(TOKEN_TYPE_CLAIM=None)
+ def test_optional_type_token(self):
+ token = MyToken()
+ self.assertNotIn("type", token)
+
def test_set_exp(self):
now = make_utc(datetime(year=2000, month=1, day=1))
@@ -309,32 +345,69 @@ def test_check_exp(self):
"refresh_exp", current_time=current_time + timedelta(days=2)
)
- def test_for_user(self):
- username = "test_user"
- user = User.objects.create_user(
- username=username,
- password="test_password",
- )
+ def test_check_token_not_expired_if_in_leeway(self):
+ token = MyToken()
+ token.set_exp("refresh_exp", lifetime=timedelta(days=1))
+
+ datetime_in_leeway = token.current_time + timedelta(days=1)
+
+ with self.assertRaises(TokenError):
+ token.check_exp("refresh_exp", current_time=datetime_in_leeway)
+
+ # a token 1 day expired is valid if leeway is 2 days
+ # float (seconds)
+ token.token_backend.leeway = timedelta(days=2).total_seconds()
+ token.check_exp("refresh_exp", current_time=datetime_in_leeway)
+
+ # timedelta
+ token.token_backend.leeway = timedelta(days=2)
+ token.check_exp("refresh_exp", current_time=datetime_in_leeway)
+
+ # integer (seconds)
+ token.token_backend.leeway = 10
+ token.check_exp("refresh_exp", current_time=datetime_in_leeway)
- token = MyToken.for_user(user)
+ token.token_backend.leeway = 0
- user_id = getattr(user, api_settings.USER_ID_FIELD)
- if not isinstance(user_id, int):
- user_id = str(user_id)
+ def test_check_token_if_wrong_type_leeway(self):
+ token = MyToken()
+ token.set_exp("refresh_exp", lifetime=timedelta(days=1))
+
+ datetime_in_leeway = token.current_time + timedelta(days=1)
+
+ token.token_backend.leeway = "1"
+ with self.assertRaises(TokenBackendError):
+ token.check_exp("refresh_exp", current_time=datetime_in_leeway)
+ token.token_backend.leeway = 0
+
+ def test_for_user(self):
+ token = MyToken.for_user(self.user)
+
+ user_id = getattr(self.user, api_settings.USER_ID_FIELD)
+ user_id = str(user_id)
self.assertEqual(token[api_settings.USER_ID_CLAIM], user_id)
+ @override_api_settings(USER_ID_FIELD="username")
+ def test_for_user_with_username(self):
# Test with non-int user id
- with override_api_settings(USER_ID_FIELD="username"):
- token = MyToken.for_user(user)
+ token = MyToken.for_user(self.user)
+ self.assertEqual(token[api_settings.USER_ID_CLAIM], self.username)
- self.assertEqual(token[api_settings.USER_ID_CLAIM], username)
+ @override_api_settings(CHECK_REVOKE_TOKEN=True)
+ def test_revoke_token_claim_included_in_authorization_token(self):
+ token = MyToken.for_user(self.user)
+ self.assertIn(api_settings.REVOKE_TOKEN_CLAIM, token)
def test_get_token_backend(self):
token = MyToken()
self.assertEqual(token.get_token_backend(), token_backend)
+ def test_token_user_id_claim_should_always_be_string(self):
+ token = MyToken.for_user(self.user)
+ self.assertIsInstance(token[api_settings.USER_ID_CLAIM], str)
+
class TestSlidingToken(TestCase):
def test_init(self):
@@ -365,10 +438,13 @@ def test_init(self):
def test_access_token(self):
# Should create an access token from a refresh token
- refresh = RefreshToken()
- refresh["test_claim"] = "arst"
+ with freeze_time("2025-01-01"):
+ refresh = RefreshToken()
+ refresh["test_claim"] = "arst"
- access = refresh.access_token
+ with freeze_time("2025-01-02"):
+ # Ensure iat is different
+ access = refresh.access_token
self.assertIsInstance(access, AccessToken)
self.assertEqual(access[api_settings.TOKEN_TYPE_CLAIM], "access")
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 79afdbbf7..c97f7c224 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,8 +1,7 @@
-from datetime import datetime, timedelta
-from unittest.mock import patch
+from datetime import datetime, timedelta, timezone
from django.test import TestCase
-from django.utils import timezone
+from freezegun import freeze_time
from rest_framework_simplejwt.utils import (
aware_utcnow,
@@ -24,26 +23,22 @@ def test_it_should_return_the_correct_values(self):
with self.settings(USE_TZ=False):
dt = make_utc(dt)
- self.assertTrue(timezone.is_naive(dt))
+ self.assertTrue(dt.tzinfo is None)
with self.settings(USE_TZ=True):
dt = make_utc(dt)
- self.assertTrue(timezone.is_aware(dt))
+ self.assertTrue(dt.tzinfo is not None)
self.assertEqual(dt.utcoffset(), timedelta(seconds=0))
class TestAwareUtcnow(TestCase):
def test_it_should_return_the_correct_value(self):
- now = datetime.utcnow()
-
- with patch("rest_framework_simplejwt.utils.datetime") as fake_datetime:
- fake_datetime.utcnow.return_value = now
+ now = datetime.now(tz=timezone.utc).replace(tzinfo=None)
+ with freeze_time(now):
# Should return aware utcnow if USE_TZ == True
with self.settings(USE_TZ=True):
- self.assertEqual(
- timezone.make_aware(now, timezone=timezone.utc), aware_utcnow()
- )
+ self.assertEqual(now.replace(tzinfo=timezone.utc), aware_utcnow())
# Should return naive utcnow if USE_TZ == False
with self.settings(USE_TZ=False):
diff --git a/tests/test_views.py b/tests/test_views.py
index 34bae1d3a..0f67f81b0 100644
--- a/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,9 +1,10 @@
from datetime import timedelta
-from importlib import reload
+from unittest import mock
from unittest.mock import patch
from django.contrib.auth import get_user_model
from django.utils import timezone
+from rest_framework.test import APIRequestFactory
from rest_framework_simplejwt import serializers
from rest_framework_simplejwt.settings import api_settings
@@ -13,6 +14,7 @@
datetime_from_epoch,
datetime_to_epoch,
)
+from rest_framework_simplejwt.views import TokenViewBase
from .utils import APIViewTestCase, override_api_settings
@@ -91,20 +93,37 @@ def test_update_last_login(self):
user = User.objects.get(username=self.username)
self.assertEqual(user.last_login, None)
+ @override_api_settings(UPDATE_LAST_LOGIN=True)
+ def test_update_last_login_updated(self):
# verify last_login is updated
- with override_api_settings(UPDATE_LAST_LOGIN=True):
- reload(serializers)
- self.view_post(
- data={
+ self.view_post(
+ data={
+ User.USERNAME_FIELD: self.username,
+ "password": self.password,
+ }
+ )
+ user = User.objects.get(username=self.username)
+ self.assertIsNotNone(user.last_login)
+ self.assertGreaterEqual(timezone.now(), user.last_login)
+
+ def test_on_login_failed_is_called(self):
+ # Patch the ON_LOGIN_FAILED setting
+ with mock.patch(
+ "rest_framework_simplejwt.settings.api_settings.ON_LOGIN_FAILED"
+ ) as mocked_hook:
+ self.test_credentials_wrong()
+ mocked_hook.assert_called_once()
+
+ # Optional: check exact arguments
+ args, kwargs = mocked_hook.call_args
+ credentials, request = args
+ self.assertEqual(
+ credentials,
+ {
User.USERNAME_FIELD: self.username,
- "password": self.password,
- }
+ "password": "********************",
+ },
)
- user = User.objects.get(username=self.username)
- self.assertIsNotNone(user.last_login)
- self.assertGreaterEqual(timezone.now(), user.last_login)
-
- reload(serializers)
class TestTokenRefreshView(APIViewTestCase):
@@ -231,20 +250,18 @@ def test_update_last_login(self):
user = User.objects.get(username=self.username)
self.assertEqual(user.last_login, None)
+ @override_api_settings(UPDATE_LAST_LOGIN=True)
+ def test_update_last_login_updated(self):
# verify last_login is updated
- with override_api_settings(UPDATE_LAST_LOGIN=True):
- reload(serializers)
- self.view_post(
- data={
- User.USERNAME_FIELD: self.username,
- "password": self.password,
- }
- )
- user = User.objects.get(username=self.username)
- self.assertIsNotNone(user.last_login)
- self.assertGreaterEqual(timezone.now(), user.last_login)
-
- reload(serializers)
+ self.view_post(
+ data={
+ User.USERNAME_FIELD: self.username,
+ "password": self.password,
+ }
+ )
+ user = User.objects.get(username=self.username)
+ self.assertIsNotNone(user.last_login)
+ self.assertGreaterEqual(timezone.now(), user.last_login)
class TestTokenRefreshSlidingView(APIViewTestCase):
@@ -431,3 +448,28 @@ def test_it_should_return_401_if_token_is_blacklisted(self):
del self.view_name
self.assertEqual(res.status_code, 401)
+
+
+class TestCustomTokenView(APIViewTestCase):
+ def test_custom_view_class(self):
+ class CustomTokenView(TokenViewBase):
+ serializer_class = serializers.TokenObtainPairSerializer
+
+ factory = APIRequestFactory()
+ view = CustomTokenView.as_view()
+ request = factory.post("/", {}, format="json")
+ res = view(request)
+ self.assertEqual(res.status_code, 400)
+
+
+class TestTokenViewBase(APIViewTestCase):
+ def test_serializer_class_not_set_in_settings_and_class_attribute_or_wrong_path(
+ self,
+ ):
+ view = TokenViewBase()
+ msg = "Could not import serializer '%s'" % view._serializer_class
+
+ with self.assertRaises(ImportError) as e:
+ view.get_serializer_class()
+
+ self.assertEqual(e.exception.msg, msg)
diff --git a/tests/utils.py b/tests/utils.py
index e0ffa58a1..9e4c2c4cb 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -3,9 +3,9 @@
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from django.test import TestCase, TransactionTestCase
+from django.urls import reverse
from rest_framework.test import APIClient
-from rest_framework_simplejwt.compat import reverse
from rest_framework_simplejwt.settings import api_settings
@@ -62,23 +62,25 @@ def override_api_settings(**settings):
except AttributeError:
pass
- yield
-
- for k in settings.keys():
- # Delete temporary settings
- api_settings.user_settings.pop(k)
-
- # Restore saved settings
- try:
- api_settings.user_settings[k] = old_settings[k]
- except KeyError:
- pass
-
- # Delete any cached settings
- try:
- delattr(api_settings, k)
- except AttributeError:
- pass
+ try:
+ yield
+
+ finally:
+ for k in settings.keys():
+ # Delete temporary settings
+ api_settings.user_settings.pop(k)
+
+ # Restore saved settings
+ try:
+ api_settings.user_settings[k] = old_settings[k]
+ except KeyError:
+ pass
+
+ # Delete any cached settings
+ try:
+ delattr(api_settings, k)
+ except AttributeError:
+ pass
class MigrationTestCase(TransactionTestCase):
diff --git a/tox.ini b/tox.ini
index 8839a7ef4..2392f697d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,32 +1,30 @@
[tox]
envlist=
- py{37,38,39}-dj22-drf310-tests
- py{37,38,39}-dj{22,31}-drf{311,312}-tests
- py{37,38,39,310}-dj32-drf{311,312}-tests
- py{38,39,310}-djmain-drf312-tests
+ py{39,310,311,312}-dj42-drf{314,315}-pyjwt{171,2}-tests
+ py{310,311,312}-dj50-drf315-pyjwt{171,2}-tests
+ py{310,311,312,313}-dj51-drf315-pyjwt{171,2}-tests
+ py{311,312,313}-dj52-drf315-pyjwt{171,2}-tests
docs
[gh-actions]
python=
- 3.7: py37
- 3.8: py38, docs, lint
3.9: py39
3.10: py310
+ 3.11: py311
+ 3.12: py312, docs
+ 3.13: py313
[gh-actions:env]
DJANGO=
- 2.2: dj22
- 3.0: dj30
- 3.1: dj31
- 3.2: dj32
- main: djmain
+ 4.2: dj42
+ 5.0: dj50
+ 5.1: dj51
+ 5.2: dj52
DRF=
- 3.10: drf310
- 3.11: drf311
- 3.12: drf312
+ 3.14: drf314
+ 3.15: drf315
[testenv]
-usedevelop=True
commands = pytest {posargs:tests} --cov-append --cov-report=xml --cov=rest_framework_simplejwt
extras=
test
@@ -34,14 +32,14 @@ extras=
setenv=
PYTHONDONTWRITEBYTECODE=1
deps=
- dj22: Django>=2.2,<2.3
- dj31: Django>=3.1,<3.2
- dj32: Django>=3.2,<3.3
- drf310: djangorestframework>=3.10,<3.11
- drf311: djangorestframework>=3.11,<3.12
- drf312: djangorestframework>=3.12,<3.13
- djmain: https://github.com/django/django/archive/main.tar.gz
- djmain: pytz
+ dj42: Django>=4.2,<4.3
+ dj50: Django>=5.0,<5.1
+ dj51: Django>=5.1,<5.2
+ dj52: Django>=5.2,<5.3
+ drf314: djangorestframework>=3.14,<3.15
+ drf315: djangorestframework>=3.15,<3.16
+ pyjwt171: pyjwt>=1.7.1,<1.8
+ pyjwt2: pyjwt>=2,<3
allowlist_externals=make
[testenv:docs]