dj-jwt-auth 1.9.5__tar.gz → 1.10.0__tar.gz
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.
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/PKG-INFO +3 -2
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/dj_jwt_auth.egg-info/PKG-INFO +3 -2
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/middleware.py +2 -1
- dj_jwt_auth-1.10.0/django_jwt/templates/admin/login.html +11 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/utils.py +3 -3
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/views.py +3 -3
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/setup.cfg +1 -1
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/tests/test.py +129 -2
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/tests/urls.py +4 -1
- dj_jwt_auth-1.9.5/django_jwt/templates/admin/login.html +0 -9
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/MANIFEST.in +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/README.md +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/dj_jwt_auth.egg-info/SOURCES.txt +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/dj_jwt_auth.egg-info/dependency_links.txt +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/dj_jwt_auth.egg-info/requires.txt +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/dj_jwt_auth.egg-info/top_level.txt +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/__init__.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/config.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/exceptions.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/pkce.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/roles.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/settings.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/templates/django-jwt-index.html +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/urls.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/django_jwt/user.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/pyproject.toml +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/setup.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/tests/__init__.py +0 -0
- {dj_jwt_auth-1.9.5 → dj_jwt_auth-1.10.0}/tests/models.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dj-jwt-auth
|
|
3
|
-
Version: 1.
|
|
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,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dj-jwt-auth
|
|
3
|
-
Version: 1.
|
|
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
|
|
|
@@ -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
|
-
|
|
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:
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{% extends "admin/login.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
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 %}
|
|
11
|
+
{% endblock %}
|
|
@@ -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.
|
|
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,
|
|
@@ -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.
|
|
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
|
-
|
|
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:
|
|
@@ -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.
|
|
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": "
|
|
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())
|
|
@@ -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
|
]
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
{% extends "admin/login.html" %}
|
|
2
|
-
|
|
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>
|
|
9
|
-
{% endblock %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|