From 9994ede03967b123c78a395917a2984f34f40ca4 Mon Sep 17 00:00:00 2001 From: Nikolay Kiryanov Date: Wed, 23 Apr 2025 18:17:28 +0300 Subject: [PATCH 1/3] Migrate to simplejwt with refresh token support --- {{ cookiecutter.name }}/mypy.ini | 2 +- .../src/a12n/api/serializers.py | 6 ++ {{ cookiecutter.name }}/src/a12n/api/urls.py | 4 +- {{ cookiecutter.name }}/src/a12n/api/views.py | 6 +- .../src/a12n/tests/jwt_views/conftest.py | 17 ++++ .../tests/jwt_views/test_obtain_jwt_view.py | 74 ---------------- .../tests/jwt_views/test_refresh_jwt_token.py | 71 --------------- .../tests/jwt_views/tests_obtain_jwt_view.py | 77 +++++++++++++++++ .../tests/jwt_views/tests_refresh_jwt_view.py | 86 +++++++++++++++++++ {{ cookiecutter.name }}/src/a12n/utils.py | 13 --- {{ cookiecutter.name }}/src/app/conf/api.py | 2 +- {{ cookiecutter.name }}/src/app/conf/auth.py | 11 ++- .../src/app/conf/installed_apps.py | 2 +- 13 files changed, 201 insertions(+), 170 deletions(-) create mode 100644 {{ cookiecutter.name }}/src/a12n/api/serializers.py create mode 100644 {{ cookiecutter.name }}/src/a12n/tests/jwt_views/conftest.py delete mode 100644 {{ cookiecutter.name }}/src/a12n/tests/jwt_views/test_obtain_jwt_view.py delete mode 100644 {{ cookiecutter.name }}/src/a12n/tests/jwt_views/test_refresh_jwt_token.py create mode 100644 {{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_obtain_jwt_view.py create mode 100644 {{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_refresh_jwt_view.py delete mode 100644 {{ cookiecutter.name }}/src/a12n/utils.py diff --git a/{{ cookiecutter.name }}/mypy.ini b/{{ cookiecutter.name }}/mypy.ini index bb764225..6678edaa 100644 --- a/{{ cookiecutter.name }}/mypy.ini +++ b/{{ cookiecutter.name }}/mypy.ini @@ -22,7 +22,7 @@ plugins = [mypy.plugins.django-stubs] django_settings_module = "app.settings" -[mypy-rest_framework_jwt.*] +[mypy-rest_framework_simplejwt.*] ignore_missing_imports = on [mypy-app.testing.api.*] diff --git a/{{ cookiecutter.name }}/src/a12n/api/serializers.py b/{{ cookiecutter.name }}/src/a12n/api/serializers.py new file mode 100644 index 00000000..c13dd74b --- /dev/null +++ b/{{ cookiecutter.name }}/src/a12n/api/serializers.py @@ -0,0 +1,6 @@ +from django.utils.translation import gettext_lazy as _ +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + + +class TokenObtainPairWithProperMessageSerializer(TokenObtainPairSerializer): + default_error_messages = {"no_active_account": _("Invalid username or password.")} diff --git a/{{ cookiecutter.name }}/src/a12n/api/urls.py b/{{ cookiecutter.name }}/src/a12n/api/urls.py index 7774e5b1..ecf1e6fa 100644 --- a/{{ cookiecutter.name }}/src/a12n/api/urls.py +++ b/{{ cookiecutter.name }}/src/a12n/api/urls.py @@ -6,6 +6,6 @@ app_name = "a12n" urlpatterns = [ - path("token/", views.ObtainJSONWebTokenView.as_view()), - path("token/refresh/", views.RefreshJSONWebTokenView.as_view()), + path("token/", views.TokenObtainPairView.as_view(), name="auth_obtain_pair"), + path("token/refresh/", views.TokenRefreshView.as_view(), name="auth_refresh"), ] diff --git a/{{ cookiecutter.name }}/src/a12n/api/views.py b/{{ cookiecutter.name }}/src/a12n/api/views.py index 6bf404b3..127b05e4 100644 --- a/{{ cookiecutter.name }}/src/a12n/api/views.py +++ b/{{ cookiecutter.name }}/src/a12n/api/views.py @@ -1,11 +1,11 @@ -from rest_framework_jwt import views as jwt +from rest_framework_simplejwt import views as jwt from a12n.api.throttling import AuthAnonRateThrottle -class ObtainJSONWebTokenView(jwt.ObtainJSONWebTokenView): +class TokenObtainPairView(jwt.TokenObtainPairView): throttle_classes = [AuthAnonRateThrottle] -class RefreshJSONWebTokenView(jwt.RefreshJSONWebTokenView): +class TokenRefreshView(jwt.TokenRefreshView): throttle_classes = [AuthAnonRateThrottle] diff --git a/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/conftest.py b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/conftest.py new file mode 100644 index 00000000..f6ad706b --- /dev/null +++ b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/conftest.py @@ -0,0 +1,17 @@ +import pytest +from rest_framework_simplejwt.tokens import RefreshToken + + +@pytest.fixture +def user(factory): + user = factory.user(username="jwt-tester-user") + user.set_password("sn00pd0g") + user.save() + + return user + + +@pytest.fixture +def initial_token_pair(user): + refresh = RefreshToken.for_user(user) + return {"refresh": str(refresh), "access": str(refresh.access_token)} diff --git a/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/test_obtain_jwt_view.py b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/test_obtain_jwt_view.py deleted file mode 100644 index 959c34ce..00000000 --- a/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/test_obtain_jwt_view.py +++ /dev/null @@ -1,74 +0,0 @@ -import json - -import pytest -from axes.models import AccessAttempt - - -pytestmark = pytest.mark.django_db - - -@pytest.fixture(autouse=True) -def _enable_django_axes(settings): - settings.AXES_ENABLED = True - - -@pytest.fixture -def get_token(as_user): - def _get_token(username, password, expected_status=201): - return as_user.post( - "/api/v1/auth/token/", - { - "username": username, - "password": password, - }, - format="json", - expected_status=expected_status, - ) - - return _get_token - - -def _decode(response): - return json.loads(response.content.decode("utf-8", errors="ignore")) - - -def test_getting_token_ok(as_user, get_token): - result = get_token(as_user.user.username, as_user.password) - - assert "token" in result - - -def test_getting_token_is_token(as_user, get_token): - result = get_token(as_user.user.username, as_user.password) - - assert len(result["token"]) > 32 # every stuff that is long enough, may be a JWT token - - -def test_getting_token_with_incorrect_password(as_user, get_token): - result = get_token(as_user.user.username, "z3r0c00l", expected_status=400) - - assert "nonFieldErrors" in result - - -def test_getting_token_with_incorrect_password_creates_access_attempt_log_entry(as_user, get_token): - get_token(as_user.user.username, "z3r0c00l", expected_status=400) # act - - assert AccessAttempt.objects.count() == 1 - - -@pytest.mark.parametrize( - ("extract_token", "status_code"), - [ - (lambda response: response["token"], 200), - ( - lambda *args: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6InRpbW90aHk5NSIsImlhdCI6MjQ5MzI0NDgwMCwiZXhwIjoyNDkzMjQ1MTAwLCJqdGkiOiI2MWQ2MTE3YS1iZWNlLTQ5YWEtYWViYi1mOGI4MzBhZDBlNzgiLCJ1c2VyX2lkIjoxLCJvcmlnX2lhdCI6MjQ5MzI0NDgwMH0.YQnk0vSshNQRTAuq1ilddc9g3CZ0s9B0PQEIk5pWa9I", - 401, - ), - (lambda *args: "sh1t", 401), - ], -) -def test_received_token_works(as_user, get_token, as_anon, extract_token, status_code): - token = extract_token(get_token(as_user.user.username, as_user.password)) - as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {token}") - - as_anon.get("/api/v1/users/me/", expected_status=status_code) # act diff --git a/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/test_refresh_jwt_token.py b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/test_refresh_jwt_token.py deleted file mode 100644 index 104236b1..00000000 --- a/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/test_refresh_jwt_token.py +++ /dev/null @@ -1,71 +0,0 @@ -import pytest -from freezegun import freeze_time - -from a12n.utils import get_jwt - - -pytestmark = [ - pytest.mark.django_db, - pytest.mark.freeze_time("2049-01-05"), -] - - -@pytest.fixture -def refresh_token(as_user): - def _refresh_token(token, expected_status=201): - return as_user.post( - "/api/v1/auth/token/refresh/", - { - "token": token, - }, - format="json", - expected_status=expected_status, - ) - - return _refresh_token - - -@pytest.fixture -def initial_token(as_user): - with freeze_time("2049-01-03"): - return get_jwt(as_user.user) - - -def test_refresh_token_ok(initial_token, refresh_token): - result = refresh_token(initial_token) - - assert "token" in result - - -def test_refreshed_token_is_a_token(initial_token, refresh_token): - result = refresh_token(initial_token) - - assert len(result["token"]) > 32 - - -def test_refreshed_token_is_new_one(initial_token, refresh_token): - result = refresh_token(initial_token) - - assert result["token"] != initial_token - - -def test_refresh_token_fails_with_incorrect_previous_token(refresh_token): - result = refresh_token("some-invalid-previous-token", expected_status=400) - - assert "nonFieldErrors" in result - - -def test_token_is_not_allowed_to_refresh_if_expired(initial_token, refresh_token): - with freeze_time("2049-02-05"): - result = refresh_token(initial_token, expected_status=400) - - assert "expired" in result["nonFieldErrors"][0] - - -def test_received_token_works(as_anon, refresh_token, initial_token): - token = refresh_token(initial_token)["token"] - as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {token}") - - result = as_anon.get("/api/v1/users/me/") - - assert result is not None diff --git a/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_obtain_jwt_view.py b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_obtain_jwt_view.py new file mode 100644 index 00000000..a499f18d --- /dev/null +++ b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_obtain_jwt_view.py @@ -0,0 +1,77 @@ +import pytest +from axes.models import AccessAttempt +from freezegun import freeze_time + + +pytestmark = [ + pytest.mark.django_db, + pytest.mark.usefixtures("user"), +] + + +@pytest.fixture(autouse=True) +def _enable_django_axes(settings): + settings.AXES_ENABLED = True + + +@pytest.fixture +def get_tokens(as_anon): + def _get_tokens(username, password, expected_status=200): + return as_anon.post( + "/api/v1/auth/token/", + { + "username": username, + "password": password, + }, + expected_status=expected_status, + ) + + return _get_tokens + + +def test_get_token_pair(get_tokens): + result = get_tokens("jwt-tester-user", "sn00pd0g") + + assert len(result["access"]) > 40 + assert len(result["refresh"]) > 40 + + +def test_error_if_incorrect_password(get_tokens): + result = get_tokens("jwt-tester-user", "50cent", expected_status=401) + + assert "Invalid username or password" in result["detail"] + + +def test_error_if_user_is_not_active(get_tokens, user): + user.is_active = False + user.save() + + result = get_tokens("jwt-tester-user", "sn00pd0g", expected_status=401) + + assert "Invalid username or password" in result["detail"] + + +def test_getting_token_with_incorrect_password_creates_access_attempt_log_entry(get_tokens): + get_tokens("jwt-tester-user", "50cent", expected_status=401) + + assert AccessAttempt.objects.count() == 1 + + +def test_access_token_gives_access_to_correct_user(get_tokens, as_anon, user): + access_token = get_tokens("jwt-tester-user", "sn00pd0g")["access"] + + as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {access_token}") + result = as_anon.get("/api/v1/users/me/") + + assert result["id"] == user.id + + +@pytest.mark.freeze_time("2049-01-05 10:00:00Z") +def test_token_is_not_allowed_to_access_if_expired(as_anon, get_tokens): + access_token = get_tokens("jwt-tester-user", "sn00pd0g")["access"] + + with freeze_time("2049-01-05 10:15:01Z"): + as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {access_token}") + result = as_anon.get("/api/v1/users/me/", expected_status=401) + + assert "not valid" in result["detail"] diff --git a/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_refresh_jwt_view.py b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_refresh_jwt_view.py new file mode 100644 index 00000000..fb6a8351 --- /dev/null +++ b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_refresh_jwt_view.py @@ -0,0 +1,86 @@ +import pytest +from freezegun import freeze_time + + +pytestmark = [ + pytest.mark.django_db, + pytest.mark.usefixtures("user"), +] + + +@pytest.fixture +def refresh_tokens(as_anon): + def _refresh_tokens(token, expected_status=200): + return as_anon.post( + "/api/v1/auth/token/refresh/", + { + "refresh": token, + }, + expected_status=expected_status, + ) + + return _refresh_tokens + + +def test_refresh_token_endpoint_token_pair(initial_token_pair, refresh_tokens): + refreshed_token_pair = refresh_tokens(initial_token_pair["refresh"]) + + assert len(refreshed_token_pair["access"]) > 40 + assert len(refreshed_token_pair["refresh"]) > 40 + + +def test_refresh_tokens_are_new(initial_token_pair, refresh_tokens): + refreshed_token_pair = refresh_tokens(initial_token_pair["refresh"]) + + assert initial_token_pair["access"] != refreshed_token_pair["access"] + assert initial_token_pair["refresh"] != refreshed_token_pair["refresh"] + + +def test_refreshed_access_token_works_as_expected(initial_token_pair, refresh_tokens, user, as_anon): + refreshed_access_token = refresh_tokens(initial_token_pair["refresh"])["access"] + + as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {refreshed_access_token}") + result = as_anon.get("/api/v1/users/me/") + + assert result["id"] == user.id + + +def test_refreshed_refresh_token_is_also_good(initial_token_pair, refresh_tokens, user, as_anon): + refreshed_refresh_token = refresh_tokens(initial_token_pair["refresh"])["refresh"] + last_refreshed_access_token = refresh_tokens(refreshed_refresh_token)["access"] + + as_anon.credentials(HTTP_AUTHORIZATION=f"Bearer {last_refreshed_access_token}") + result = as_anon.get("/api/v1/users/me/") + + assert result["id"] == user.id + + +def test_refresh_token_fails_if_user_is_not_active(refresh_tokens, initial_token_pair, user): + user.is_active = False + user.save() + + result = refresh_tokens(initial_token_pair["refresh"], expected_status=401) + + assert "No active account found" in result["detail"] + + +def test_refresh_token_fails_with_incorrect_previous_token(refresh_tokens): + result = refresh_tokens("some-invalid-previous-token", expected_status=401) + + assert "Token is invalid" in result["detail"] + + +@pytest.mark.freeze_time("2049-01-01 10:00:00Z") +def test_token_is_not_allowed_to_refresh_if_expired(initial_token_pair, refresh_tokens): + with freeze_time("2049-01-22 10:00:01Z"): # 21 days and 1 second later + result = refresh_tokens(initial_token_pair["refresh"], expected_status=401) + + assert "expired" in result["detail"] + + +def test_token_is_not_allowed_to_refresh_twice(initial_token_pair, refresh_tokens): + refresh_tokens(initial_token_pair["refresh"]) + + result = refresh_tokens(initial_token_pair["refresh"], expected_status=401) + + assert "blacklisted" in result["detail"] diff --git a/{{ cookiecutter.name }}/src/a12n/utils.py b/{{ cookiecutter.name }}/src/a12n/utils.py deleted file mode 100644 index 33e68e3a..00000000 --- a/{{ cookiecutter.name }}/src/a12n/utils.py +++ /dev/null @@ -1,13 +0,0 @@ -from rest_framework_jwt.settings import api_settings - -from users.models import User - - -def get_jwt(user: User) -> str: - """Make JWT for given user""" - jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER - jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER - - payload = jwt_payload_handler(user) - - return jwt_encode_handler(payload) diff --git a/{{ cookiecutter.name }}/src/app/conf/api.py b/{{ cookiecutter.name }}/src/app/conf/api.py index 066afd84..8730f1bf 100644 --- a/{{ cookiecutter.name }}/src/app/conf/api.py +++ b/{{ cookiecutter.name }}/src/app/conf/api.py @@ -12,7 +12,7 @@ "DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticatedOrReadOnly",), "DEFAULT_AUTHENTICATION_CLASSES": [ "rest_framework.authentication.TokenAuthentication", - "rest_framework_jwt.authentication.JSONWebTokenAuthentication", + "rest_framework_simplejwt.authentication.JWTAuthentication", ], "DEFAULT_RENDERER_CLASSES": [ "app.api.renderers.AppJSONRenderer", diff --git a/{{ cookiecutter.name }}/src/app/conf/auth.py b/{{ cookiecutter.name }}/src/app/conf/auth.py index cb4f0f7e..3976a6c7 100644 --- a/{{ cookiecutter.name }}/src/app/conf/auth.py +++ b/{{ cookiecutter.name }}/src/app/conf/auth.py @@ -11,10 +11,13 @@ "django.contrib.auth.backends.ModelBackend", ] -JWT_AUTH = { - "JWT_EXPIRATION_DELTA": timedelta(days=14), - "JWT_REFRESH_EXPIRATION_DELTA": timedelta(days=21), - "JWT_ALLOW_REFRESH": True, +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=15), + "REFRESH_TOKEN_LIFETIME": timedelta(days=21), + "AUTH_HEADER_TYPES": ("Bearer",), + "ROTATE_REFRESH_TOKENS": True, + "BLACKLIST_AFTER_ROTATION": True, + "TOKEN_OBTAIN_SERIALIZER": "a12n.api.serializers.TokenObtainPairWithProperMessageSerializer", } diff --git a/{{ cookiecutter.name }}/src/app/conf/installed_apps.py b/{{ cookiecutter.name }}/src/app/conf/installed_apps.py index 743b9adb..4785a4ee 100644 --- a/{{ cookiecutter.name }}/src/app/conf/installed_apps.py +++ b/{{ cookiecutter.name }}/src/app/conf/installed_apps.py @@ -11,7 +11,7 @@ "drf_spectacular_sidecar", "rest_framework", "rest_framework.authtoken", - "rest_framework_jwt.blacklist", + "rest_framework_simplejwt.token_blacklist", "django_filters", "axes", "django.contrib.admin", From 7a88f28efaec5bb4a956706474f2e527abe299ab Mon Sep 17 00:00:00 2001 From: Nikolay Kiryanov Date: Wed, 23 Apr 2025 18:43:17 +0300 Subject: [PATCH 2/3] Auth: logout view (blacklist existed refresh token) --- {{ cookiecutter.name }}/src/a12n/api/urls.py | 2 + .../tests/jwt_views/tests_logout_jwt_view.py | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 {{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_logout_jwt_view.py diff --git a/{{ cookiecutter.name }}/src/a12n/api/urls.py b/{{ cookiecutter.name }}/src/a12n/api/urls.py index ecf1e6fa..08c79657 100644 --- a/{{ cookiecutter.name }}/src/a12n/api/urls.py +++ b/{{ cookiecutter.name }}/src/a12n/api/urls.py @@ -1,4 +1,5 @@ from django.urls import path +from rest_framework_simplejwt import views as jwt from a12n.api import views @@ -8,4 +9,5 @@ urlpatterns = [ path("token/", views.TokenObtainPairView.as_view(), name="auth_obtain_pair"), path("token/refresh/", views.TokenRefreshView.as_view(), name="auth_refresh"), + path("logout/", jwt.TokenBlacklistView.as_view(), name="auth_logout"), ] diff --git a/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_logout_jwt_view.py b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_logout_jwt_view.py new file mode 100644 index 00000000..f0d869b4 --- /dev/null +++ b/{{ cookiecutter.name }}/src/a12n/tests/jwt_views/tests_logout_jwt_view.py @@ -0,0 +1,40 @@ +import pytest +from rest_framework_simplejwt.token_blacklist.models import BlacklistedToken + + +pytestmark = [ + pytest.mark.django_db, + pytest.mark.usefixtures("user"), +] + + +@pytest.fixture +def logout(as_anon): + def _logout(token, expected_status=200): + return as_anon.post( + "/api/v1/auth/logout/", + { + "refresh": token, + }, + expected_status=expected_status, + ) + + return _logout + + +def test_logout_token_saved_to_blacklist(logout, initial_token_pair): + logout(initial_token_pair["refresh"]) + + assert BlacklistedToken.objects.get(token__token=initial_token_pair["refresh"]) + + +def test_logout_refresh_token_impossible_to_reuse(initial_token_pair, logout, as_anon): + logout(initial_token_pair["refresh"]) + + result = as_anon.post( + path="/api/v1/auth/token/refresh/", + data={"refresh": initial_token_pair["refresh"]}, + expected_status=401, + ) + + assert "blacklisted" in result["detail"] From 020462c175e8bdf4f3fab7804294c9056e4faef0 Mon Sep 17 00:00:00 2001 From: Nikolay Kiryanov Date: Sat, 26 Apr 2025 17:13:06 +0300 Subject: [PATCH 3/3] Fix for master rebase: update poetry deps --- {{ cookiecutter.name }}/poetry.lock | 54 +++++++++++++------------- {{ cookiecutter.name }}/pyproject.toml | 1 + 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/{{ cookiecutter.name }}/poetry.lock b/{{ cookiecutter.name }}/poetry.lock index b39845a1..4564c91a 100644 --- a/{{ cookiecutter.name }}/poetry.lock +++ b/{{ cookiecutter.name }}/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. [[package]] name = "amqp" @@ -748,6 +748,31 @@ files = [ {file = "djangorestframework-camel-case-1.4.2.tar.gz", hash = "sha256:cdae75846648abb6585c7470639a1d2fb064dc45f8e8b62aaa50be7f1a7a61f4"}, ] +[[package]] +name = "djangorestframework-simplejwt" +version = "5.5.0" +description = "A minimal JSON Web Token authentication plugin for Django REST Framework" +optional = false +python-versions = ">=3.9" +files = [ + {file = "djangorestframework_simplejwt-5.5.0-py3-none-any.whl", hash = "sha256:4ef6b38af20cdde4a4a51d1fd8e063cbbabb7b45f149cc885d38d905c5a62edb"}, + {file = "djangorestframework_simplejwt-5.5.0.tar.gz", hash = "sha256:474a1b737067e6462b3609627a392d13a4da8a08b1f0574104ac6d7b1406f90e"}, +] + +[package.dependencies] +cryptography = {version = ">=3.3.1", optional = true, markers = "extra == \"crypto\""} +django = ">=4.2" +djangorestframework = ">=3.14" +pyjwt = ">=1.7.1,<2.10.0" + +[package.extras] +crypto = ["cryptography (>=3.3.1)"] +dev = ["Sphinx (>=1.6.5,<2)", "cryptography", "freezegun", "ipython", "pre-commit", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "pyupgrade", "ruff", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel", "yesqa"] +doc = ["Sphinx (>=1.6.5,<2)", "sphinx_rtd_theme (>=0.1.9)"] +lint = ["pre-commit", "pyupgrade", "ruff", "yesqa"] +python-jose = ["python-jose (==3.3.0)"] +test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] + [[package]] name = "djangorestframework-stubs" version = "3.15.0" @@ -789,28 +814,6 @@ click_default_group = ">=1.2,<2.0" ply = ">=3.11,<4.0" typing_extensions = ">=4.0,<5.0" -[[package]] -name = "drf-jwt" -version = "1.19.2" -description = "JSON Web Token based authentication for Django REST framework" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "drf-jwt-1.19.2.tar.gz", hash = "sha256:660bc66f992065cef59832adcbbdf871847e9738671c19e5121971e773768235"}, - {file = "drf_jwt-1.19.2-py2.py3-none-any.whl", hash = "sha256:63c3d4ed61a1013958cd63416e2d5c84467d8ae3e6e1be44b1fb58743dbd1582"}, -] - -[package.dependencies] -Django = ">=1.11" -djangorestframework = ">=3.7" -PyJWT = {version = ">=1.5.2,<3.0.0", extras = ["crypto"]} - -[package.extras] -dev = ["tox"] -docs = ["mkdocs (==0.13.2)"] -lint = ["black", "flake8", "isort"] -test = ["mock", "pytest (>=3.0)", "pytest-cov", "pytest-django", "pytest-runner", "six"] - [[package]] name = "drf-spectacular" version = "0.27.2" @@ -1457,9 +1460,6 @@ files = [ {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, ] -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} - [package.extras] crypto = ["cryptography (>=3.4.0)"] dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] @@ -2187,4 +2187,4 @@ brotli = ["brotli"] [metadata] lock-version = "2.0" python-versions = "~3.11" -content-hash = "b2e590e4543e3a3926f1e09e0ba8e96f7f2896adec246f7111969a78e01f0d9d" +content-hash = "7397b65d71dff3c165de4c8384685f5379225e0c51fdd4d13ea77cb354b11d75" diff --git a/{{ cookiecutter.name }}/pyproject.toml b/{{ cookiecutter.name }}/pyproject.toml index acc38a0f..ed7715aa 100644 --- a/{{ cookiecutter.name }}/pyproject.toml +++ b/{{ cookiecutter.name }}/pyproject.toml @@ -28,6 +28,7 @@ django-split-settings = "^1.3.2" django-storages = "^1.14.4" djangorestframework = "^3.15.2" djangorestframework-camel-case = "^1.4.2" +djangorestframework-simplejwt = {extras = ["crypto"], version = "^5.5.0"} drf-jwt = "^1.19.2" drf-spectacular = {extras = ["sidecar"], version = "^0.27.2"} pillow = "^10.1.0"