dj-jwt-auth 1.9.5__py3-none-any.whl → 1.10.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: dj-jwt-auth
3
- Version: 1.9.5
3
+ Version: 1.10.0
4
4
  Summary: A Django package for JSON Web Token validation and verification. Using PyJWT.
5
5
  Home-page: https://www.example.com/
6
6
  Author: Konstantin Seleznev
@@ -26,6 +26,7 @@ Requires-Dist: Django>=3.0
26
26
  Requires-Dist: pyjwt>=2.5.0
27
27
  Requires-Dist: requests>=2.28.1
28
28
  Requires-Dist: cryptography>=36.0.2
29
+ Dynamic: description-content-type
29
30
 
30
31
  # Django-JWT
31
32
 
@@ -1,21 +1,21 @@
1
1
  django_jwt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  django_jwt/config.py,sha256=rJTLgFo42iuxZ4pk2tecmlp2fKcyX7QDDsOrnEKjC6c,1506
3
3
  django_jwt/exceptions.py,sha256=S8HeH_lu8XSsejhyW0qkl04MmXxxbN4-zrtyPywbBwc,311
4
- django_jwt/middleware.py,sha256=jk3M9qumI0p4rVvmkZM1jAf5xzaVl5FmAqrR-jAO_Po,1288
4
+ django_jwt/middleware.py,sha256=xAF-kTv9GZ0waVkRgP2oNfqU3OpADcdg3NI2BrL7-TQ,1371
5
5
  django_jwt/pkce.py,sha256=j-v2ffCw0X3JW7ak8vfNeSZI-dACOvHbi1eLmJ0R8gM,685
6
6
  django_jwt/roles.py,sha256=SaHK3o8T8USS4ZhG4SrHPlZQV2lMb2t1UZHT6IQtBvA,143
7
7
  django_jwt/settings.py,sha256=N3v8B4lEM-bGLmAhXhF7hOfdcjKn6afbxDFx7CETcD0,1769
8
8
  django_jwt/urls.py,sha256=ZhcnRcQ1MBRh-bS7fTa-Vkz8yuWUhv-G_uRXKLnKAs0,320
9
9
  django_jwt/user.py,sha256=v2oCoAThstFg5x5cOA9nSKlYWZzCEqhFbz63WGhE_NM,6382
10
- django_jwt/utils.py,sha256=E_D_GxIVmKQYHzKTMEWPqR1zV4l0q40vPolxhcDGQdk,1641
11
- django_jwt/views.py,sha256=RO87xCGhKClkJScn_tlSwytIzlqL-pl0vIKdFUUPOVY,4829
10
+ django_jwt/utils.py,sha256=Y0J5_31yLk_Xfi4fRn4u2Oh0b1dHMNjalKqx2gF-ONU,1696
11
+ django_jwt/views.py,sha256=A67FLhbEcyDkD3GfTOmWWD2wrxRCQgsEvUQg43BQsEo,4830
12
12
  django_jwt/templates/django-jwt-index.html,sha256=y8f0v2WbRAFxnIU799I_MZCVsjn1sbdh7bypjdWB0lA,1353
13
- django_jwt/templates/admin/login.html,sha256=Nihyu0IGvDDZVvQDITXozwlj6XCQ0B8gqlyHLqVNyJc,275
13
+ django_jwt/templates/admin/login.html,sha256=akPHr4mAMtISjTDlZ2KMVV1M2ZnMT8i5WjlfMe1hjI0,344
14
14
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
15
  tests/models.py,sha256=jhoJcCEsx5B9AibmoLQLDD3cEsyYzYy6dMdYhRnBVFo,338
16
- tests/test.py,sha256=HEy5DsEYrPmtWSdeDhgdeV2tZTYlepaWt82VoTBrta8,11828
17
- tests/urls.py,sha256=D5FhDSVAudurkrpkCZZPnDvgXSgifwFVB3nAlYBg7uQ,212
18
- dj_jwt_auth-1.9.5.dist-info/METADATA,sha256=r7am9IZuqkvup41Kimg2-1b-eRuMPH5KtS6YNoKTKQc,4251
19
- dj_jwt_auth-1.9.5.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
20
- dj_jwt_auth-1.9.5.dist-info/top_level.txt,sha256=58O7TdK-yECZcbmPc52KNlBFpjIUlENuZubCxaSOxus,17
21
- dj_jwt_auth-1.9.5.dist-info/RECORD,,
16
+ tests/test.py,sha256=m_iWQRMk3pS71A70m5RK3yQsS0PXX159EcZ_27weyB8,17910
17
+ tests/urls.py,sha256=bjV0IYqenHbCUB3D8wsnznytaRjSqfLXiRt2T0ZeFGg,339
18
+ dj_jwt_auth-1.10.0.dist-info/METADATA,sha256=WrK6dNsovcIyGtIE4wKlpLPb3DbMczbji2F_urlhxiI,4286
19
+ dj_jwt_auth-1.10.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ dj_jwt_auth-1.10.0.dist-info/top_level.txt,sha256=58O7TdK-yECZcbmPc52KNlBFpjIUlENuZubCxaSOxus,17
21
+ dj_jwt_auth-1.10.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
django_jwt/middleware.py CHANGED
@@ -29,7 +29,8 @@ class JWTAuthMiddleware(MiddlewareMixin):
29
29
  info = oidc_handler.decode_token(raw_token)
30
30
  request.user = request._cached_user = UserHandler(info, request, raw_token).get_user()
31
31
  except AlgorithmNotSupportedException as exc:
32
- return JsonResponse(status=HTTPStatus.UNAUTHORIZED.value, data={"detail": str(exc)})
32
+ log.warning(f"AlgorithmNotSupportedException: {exc}")
33
+ return JsonResponse(status=HTTPStatus.UNAUTHORIZED.value, data={"detail": "algorithm not supported"})
33
34
  except ExpiredSignatureError:
34
35
  return JsonResponse(status=HTTPStatus.UNAUTHORIZED.value, data={"detail": "expired token"})
35
36
  except UnicodeDecodeError as exc:
@@ -1,9 +1,11 @@
1
1
  {% extends "admin/login.html" %}
2
2
 
3
3
  {% block content %}
4
- {{ block.super }}
5
- <br>
6
- <div style="text-align: center;">
7
- <a class="button" style="display: inline-block; margin-top: 20px" href="{% url 'start_oidc_auth' %}">NeoID Login</a>
8
- </div>
4
+ {% if neoid_only is True %}
5
+ <div style="text-align: center;">
6
+ <a class="button" style="display: inline-block; margin-top: 20px" href="{% url 'start_oidc_auth' %}">NeoID Login</a>
7
+ </div>
8
+ {% else %}
9
+ {{ block.super }}
10
+ {% endif %}
9
11
  {% endblock %}
django_jwt/utils.py CHANGED
@@ -7,11 +7,11 @@ import jwt
7
7
  import requests
8
8
 
9
9
  from django_jwt import settings
10
- from django_jwt.config import config
10
+ from django_jwt.config import SupportedAlgorithms, config
11
11
 
12
12
 
13
13
  def get_random_string(k: int = 32) -> str:
14
- return "".join(random.choices(string.ascii_letters + string.digits + "-._~", k=k))
14
+ return "".join(random.choices(string.ascii_letters + string.digits + "-._~", k=k)) # NOSONAR
15
15
 
16
16
 
17
17
  def get_alg(token: str) -> str:
@@ -20,7 +20,7 @@ def get_alg(token: str) -> str:
20
20
 
21
21
 
22
22
  def get_access_token(code: str, redirect_uri: str, pkce_secret: str, client_id: str) -> str:
23
- token_endpoint = config.admin().get("token_endpoint")
23
+ token_endpoint = config.cfg(SupportedAlgorithms.ES256).get("token_endpoint")
24
24
  data = {
25
25
  "grant_type": "authorization_code",
26
26
  "client_id": client_id,
django_jwt/views.py CHANGED
@@ -25,7 +25,7 @@ def silent_sso_check(request):
25
25
 
26
26
 
27
27
  def index_response(request, msg, status=400):
28
- logout_url = config.admin().get("end_session_endpoint")
28
+ logout_url = config.cfg(SupportedAlgorithms.ES256).get("end_session_endpoint")
29
29
  return render(
30
30
  request,
31
31
  "django-jwt-index.html",
@@ -33,7 +33,6 @@ def index_response(request, msg, status=400):
33
33
  "error_message": msg,
34
34
  "login_url": reverse("start_oidc_auth"),
35
35
  "logout_url": logout_url,
36
- "redirect_uri": request.build_absolute_uri(reverse("start_oidc_auth")),
37
36
  },
38
37
  status=status,
39
38
  )
@@ -107,7 +106,8 @@ class ReceiveRedirectView(CallbackView):
107
106
  log.warning(f"OIDC Admin HTTPError: {exc}")
108
107
  return index_response(request=request, msg=exc.response.text, status=exc.response.status_code)
109
108
  except ConfigException as exc:
110
- return HttpResponse(content=str(exc), status=500)
109
+ log.error(f"OIDC Config error: {exc}")
110
+ return HttpResponse(content="OIDC Config error", status=500)
111
111
  except BadRequestException as exc:
112
112
  return index_response(request=request, msg=str(exc))
113
113
  except Exception as exc:
tests/test.py CHANGED
@@ -1,10 +1,12 @@
1
1
  from datetime import datetime, timezone
2
2
  from http import HTTPStatus
3
3
  from unittest.mock import Mock, patch
4
+ from urllib.parse import parse_qs, urlparse
4
5
 
5
6
  from django.contrib.auth import get_user_model
6
7
  from django.contrib.auth.models import Group, Permission
7
- from django.test import TestCase
8
+ from django.http import HttpResponse
9
+ from django.test import RequestFactory, TestCase
8
10
  from django.urls import reverse
9
11
  from jwt.api_jwt import ExpiredSignatureError
10
12
 
@@ -14,6 +16,13 @@ from django_jwt.exceptions import ConfigException
14
16
  from django_jwt.middleware import JWTAuthMiddleware
15
17
  from django_jwt.roles import ROLE
16
18
  from django_jwt.user import role_handler
19
+ from django_jwt.views import (
20
+ LogoutView,
21
+ ReceiveRedirectView,
22
+ StartOIDCAuthView,
23
+ index_response,
24
+ silent_sso_check,
25
+ )
17
26
 
18
27
  utc = timezone.utc
19
28
  access_token_payload = {
@@ -240,7 +249,7 @@ class ConfigTest(TestCase):
240
249
  def test_not_supported_alg(self, *_):
241
250
  response = self.middleware.process_request(self.request)
242
251
  self.assertEqual(HTTPStatus.UNAUTHORIZED.value, response.status_code)
243
- self.assertEqual(b'{"detail": "Algorithm HS256 is not supported"}', response.content)
252
+ self.assertEqual(b'{"detail": "algorithm not supported"}', response.content)
244
253
 
245
254
 
246
255
  class RolesTest(TestCase):
@@ -289,3 +298,121 @@ class RolesTest(TestCase):
289
298
  self.assertTrue(self.user.is_staff)
290
299
  self.assertTrue(self.user.groups.filter(name="staff group").exists())
291
300
  self.assertTrue(self.user.user_permissions.filter(codename="add_user").exists())
301
+
302
+
303
+ class ViewsTestCase(TestCase):
304
+ def setUp(self):
305
+ self.factory = RequestFactory()
306
+ self.user = User.objects.create(username="user2", kc_id="12345")
307
+
308
+ def test_silent_sso_check(self):
309
+ request = self.factory.get("/silent-sso/")
310
+ response = silent_sso_check(request)
311
+
312
+ self.assertIsInstance(response, HttpResponse)
313
+ self.assertIn("parent.postMessage", response.content.decode())
314
+
315
+ @patch("django_jwt.views.config.cfg", return_value={"end_session_endpoint": "/logout"})
316
+ def test_index_response(self, mock_config):
317
+ request = self.factory.get("/index/")
318
+ response = index_response(request, "Test error message", status=403)
319
+
320
+ self.assertEqual(response.status_code, 403)
321
+ self.assertIn("Test error message", response.content.decode())
322
+ self.assertIn('<button><a href="/admin/oidc/">Login</a></button>', response.content.decode())
323
+ self.assertIn('<button><a href="/logout">Logout</a></button', response.content.decode())
324
+
325
+ @patch("django_jwt.views.PKCESecret")
326
+ @patch("django_jwt.views.config")
327
+ def test_start_oidc_auth_view_get(self, mock_config, mock_pkce):
328
+ mock_pkce.return_value.challenge = "challenge"
329
+ mock_pkce.return_value.challenge_method = "S256"
330
+ mock_config.cfg.return_value.get.return_value = "/auth"
331
+
332
+ view = StartOIDCAuthView.as_view()
333
+ request = self.factory.get("/start-oidc/")
334
+ response = view(request)
335
+
336
+ self.assertEqual(response.status_code, 302)
337
+ self.assertIn("/auth?", response.url)
338
+ parsed_url = urlparse(response.url)
339
+ query_params = parse_qs(parsed_url.query)
340
+ self.assertEqual(query_params["client_id"][0], settings.OIDC_ADMIN_CLIENT_ID)
341
+ self.assertEqual(query_params["redirect_uri"][0], "http://testserver/admin/oidc/callback/")
342
+ self.assertEqual(query_params["response_type"][0], "code")
343
+ self.assertEqual(query_params["scope"][0], "openid")
344
+ self.assertEqual(query_params["code_challenge"][0], "challenge")
345
+ self.assertEqual(query_params["code_challenge_method"][0], "S256")
346
+
347
+ @patch("django_jwt.views.cache")
348
+ @patch("django_jwt.views.config")
349
+ @patch("django_jwt.views.get_access_token")
350
+ @patch("django_jwt.views.oidc_handler")
351
+ @patch("django_jwt.views.UserHandler.get_user")
352
+ def test_receive_redirect_view_success(
353
+ self, mock_get_user, mock_oidc_handler, mock_get_access_token, mock_cache, mock_config
354
+ ):
355
+ mock_cache.get.return_value = "pkce_secret"
356
+ mock_config.cfg.return_value.get.return_value = "/logout"
357
+ mock_get_access_token.return_value = "token"
358
+ mock_oidc_handler.decode_token.return_value = access_token_payload | {
359
+ "sub": self.user.kc_id,
360
+ "resource_access": {settings.OIDC_ADMIN_CLIENT_ID: {"roles": ["admin"]}},
361
+ }
362
+ mock_get_user.return_value = self.user
363
+
364
+ view = ReceiveRedirectView.as_view()
365
+ request = self.factory.get("/callback/", {"code": "abc", "state": "xyz"})
366
+ with patch("django_jwt.views.login"):
367
+ response = view(request)
368
+
369
+ self.assertEqual(response.status_code, 302)
370
+ self.assertIn(reverse("admin:index"), response.url)
371
+ self.user.refresh_from_db()
372
+ self.assertTrue(self.user.is_superuser)
373
+ self.assertTrue(self.user.is_staff)
374
+
375
+ @patch("django_jwt.views.config.cfg", return_value={"end_session_endpoint": "/logout"})
376
+ def test_receive_redirect_view_no_code_or_state(self, mock_config):
377
+ view = ReceiveRedirectView.as_view()
378
+ request = self.factory.get("/callback/")
379
+ response = view(request)
380
+ self.assertEqual(response.status_code, 400)
381
+ self.assertIn("No code or state", response.content.decode())
382
+ self.assertIn('<button><a href="/admin/oidc/">Login</a></button>', response.content.decode())
383
+ self.assertIn('<button><a href="/logout">Logout</a></button', response.content.decode())
384
+
385
+ @patch("django_jwt.views.cache.get", return_value=None)
386
+ @patch("django_jwt.views.config.cfg", return_value={"end_session_endpoint": "/logout"})
387
+ def test_receive_redirect_view_no_pkce(self, mock_cache, mock_config):
388
+ view = ReceiveRedirectView.as_view()
389
+ request = self.factory.get("/callback/", {"code": "abc", "state": "xyz"})
390
+ response = view(request)
391
+ self.assertEqual(response.status_code, 400)
392
+ self.assertIn("No PKCE secret found in cache", response.content.decode())
393
+
394
+ @patch("django_jwt.views.index_response")
395
+ @patch("django_jwt.views.cache")
396
+ @patch("django_jwt.views.get_access_token")
397
+ @patch("django_jwt.views.oidc_handler")
398
+ @patch("django_jwt.views.UserHandler")
399
+ def test_receive_redirect_view_http_error(
400
+ self, mock_user_handler, mock_oidc_handler, mock_get_access_token, mock_cache, mock_index_response
401
+ ):
402
+ mock_cache.get.return_value = "pkce_secret"
403
+ mock_get_access_token.side_effect = Exception("fail")
404
+ mock_index_response.return_value = HttpResponse("error", status=500)
405
+ view = ReceiveRedirectView.as_view()
406
+ request = self.factory.get("/callback/", {"code": "abc", "state": "xyz"})
407
+ response = view(request)
408
+ self.assertEqual(response.status_code, 500)
409
+
410
+ @patch("django_jwt.views.config.cfg", return_value={"end_session_endpoint": "/logout"})
411
+ def test_logout_view(self, mock_config):
412
+ view = LogoutView.as_view()
413
+ request = self.factory.get("/logout/")
414
+ response = view(request)
415
+ self.assertEqual(response.status_code, 401)
416
+ self.assertIn("Logged out", response.content.decode())
417
+ self.assertIn('<button><a href="/admin/oidc/">Login</a></button>', response.content.decode())
418
+ self.assertIn('<button><a href="/logout">Logout</a></button', response.content.decode())
tests/urls.py CHANGED
@@ -1,5 +1,6 @@
1
+ from django.contrib import admin
1
2
  from django.http import JsonResponse
2
- from django.urls import path
3
+ from django.urls import include, path
3
4
 
4
5
 
5
6
  def profile(request):
@@ -7,5 +8,7 @@ def profile(request):
7
8
 
8
9
 
9
10
  urlpatterns = [
11
+ path("admin/", include("django_jwt.urls")),
12
+ path("admin/", admin.site.urls),
10
13
  path("profile/", profile, name="profile"),
11
14
  ]