dj-jwt-auth 1.7.0__tar.gz → 1.8.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.7.0 → dj_jwt_auth-1.8.0}/PKG-INFO +2 -1
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/README.md +2 -1
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/PKG-INFO +2 -1
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/config.py +4 -1
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/exceptions.py +6 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/middleware.py +3 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/user.py +29 -13
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/setup.cfg +1 -1
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/tests/models.py +1 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/tests/test.py +49 -6
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/MANIFEST.in +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/SOURCES.txt +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/dependency_links.txt +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/requires.txt +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/top_level.txt +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/__init__.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/pkce.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/roles.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/settings.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/templates/admin/login.html +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/templates/django-jwt-index.html +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/urls.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/utils.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/views.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/pyproject.toml +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/setup.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/tests/__init__.py +0 -0
- {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/tests/urls.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dj-jwt-auth
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.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
|
|
@@ -137,3 +137,4 @@ Login URL will be available at `/admin/oidc/`.
|
|
|
137
137
|
|
|
138
138
|
### Testing:
|
|
139
139
|
Run command `python runtests.py` to run tests.
|
|
140
|
+
To run specific test use `python runtests.py <test_name>`, like `python runtests.py "tests.test.OIDCHandlerTest.test_new_email_exists"`.
|
|
@@ -107,4 +107,5 @@ urlpatterns = [
|
|
|
107
107
|
Login URL will be available at `/admin/oidc/`.
|
|
108
108
|
|
|
109
109
|
### Testing:
|
|
110
|
-
Run command `python runtests.py` to run tests.
|
|
110
|
+
Run command `python runtests.py` to run tests.
|
|
111
|
+
To run specific test use `python runtests.py <test_name>`, like `python runtests.py "tests.test.OIDCHandlerTest.test_new_email_exists"`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dj-jwt-auth
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.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
|
|
@@ -137,3 +137,4 @@ Login URL will be available at `/admin/oidc/`.
|
|
|
137
137
|
|
|
138
138
|
### Testing:
|
|
139
139
|
Run command `python runtests.py` to run tests.
|
|
140
|
+
To run specific test use `python runtests.py <test_name>`, like `python runtests.py "tests.test.OIDCHandlerTest.test_new_email_exists"`.
|
|
@@ -6,7 +6,7 @@ import requests
|
|
|
6
6
|
from jwt.algorithms import ECAlgorithm, RSAAlgorithm
|
|
7
7
|
|
|
8
8
|
from django_jwt import settings
|
|
9
|
-
from django_jwt.exceptions import ConfigException
|
|
9
|
+
from django_jwt.exceptions import AlgorithmNotSupportedException, ConfigException
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def ensure_well_known(url: str) -> str:
|
|
@@ -24,6 +24,9 @@ class Config:
|
|
|
24
24
|
if not self.route:
|
|
25
25
|
raise ConfigException("OIDC_CONFIG_ROUTES is not set")
|
|
26
26
|
|
|
27
|
+
if alg not in self.route:
|
|
28
|
+
raise AlgorithmNotSupportedException(f"Algorithm {alg} is not supported")
|
|
29
|
+
|
|
27
30
|
response = requests.get(ensure_well_known(self.route[alg]))
|
|
28
31
|
response.raise_for_status()
|
|
29
32
|
return response.json()
|
|
@@ -5,6 +5,7 @@ from django.http import JsonResponse
|
|
|
5
5
|
from django.utils.deprecation import MiddlewareMixin
|
|
6
6
|
from jwt import ExpiredSignatureError
|
|
7
7
|
|
|
8
|
+
from django_jwt.exceptions import AlgorithmNotSupportedException
|
|
8
9
|
from django_jwt.user import UserHandler
|
|
9
10
|
from django_jwt.utils import oidc_handler
|
|
10
11
|
|
|
@@ -27,6 +28,8 @@ class JWTAuthMiddleware(MiddlewareMixin):
|
|
|
27
28
|
try:
|
|
28
29
|
info = oidc_handler.decode_token(raw_token)
|
|
29
30
|
request.user = request._cached_user = UserHandler(info, request, raw_token).get_user()
|
|
31
|
+
except AlgorithmNotSupportedException as exc:
|
|
32
|
+
return JsonResponse(status=HTTPStatus.UNAUTHORIZED.value, data={"detail": str(exc)})
|
|
30
33
|
except ExpiredSignatureError:
|
|
31
34
|
return JsonResponse(status=HTTPStatus.UNAUTHORIZED.value, data={"detail": "expired token"})
|
|
32
35
|
except UnicodeDecodeError as exc:
|
|
@@ -4,6 +4,8 @@ from logging import getLogger
|
|
|
4
4
|
|
|
5
5
|
from django.contrib.auth import get_user_model
|
|
6
6
|
from django.contrib.auth.models import Group, Permission
|
|
7
|
+
from django.db import transaction
|
|
8
|
+
from django.db.utils import IntegrityError
|
|
7
9
|
from django.http.request import HttpRequest
|
|
8
10
|
|
|
9
11
|
from django_jwt import settings
|
|
@@ -23,6 +25,7 @@ def mapper(user_data: dict) -> dict:
|
|
|
23
25
|
|
|
24
26
|
class UserHandler:
|
|
25
27
|
modified_at = None
|
|
28
|
+
userdata_collected = False
|
|
26
29
|
|
|
27
30
|
def __init__(self, payload: dict, request: HttpRequest, access_token: str):
|
|
28
31
|
self.payload = payload
|
|
@@ -41,10 +44,12 @@ class UserHandler:
|
|
|
41
44
|
def _collect_user_data(self):
|
|
42
45
|
"""Collect user data from KeyCloak"""
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
if not self.userdata_collected:
|
|
48
|
+
user_data = oidc_handler.get_user_info(self.access_token)
|
|
49
|
+
log.debug(f"[OIDC] User data: {user_data}")
|
|
50
|
+
self.kwargs["email"] = user_data["email"].lower()
|
|
51
|
+
self.kwargs.update(mapper(user_data))
|
|
52
|
+
self.userdata_collected = True
|
|
48
53
|
|
|
49
54
|
def _update_user(self, user):
|
|
50
55
|
"""Update user fields if they are changed"""
|
|
@@ -69,6 +74,7 @@ class UserHandler:
|
|
|
69
74
|
def _get_by_email(self) -> model:
|
|
70
75
|
"""Get user from database by email and update to resave kc_id."""
|
|
71
76
|
|
|
77
|
+
self._collect_user_data()
|
|
72
78
|
user = model.objects.get(email=self.kwargs["email"])
|
|
73
79
|
self._update_user(user)
|
|
74
80
|
if self.on_update:
|
|
@@ -84,17 +90,16 @@ class UserHandler:
|
|
|
84
90
|
user_modified_at = user_modified_at.replace(tzinfo=utc)
|
|
85
91
|
is_modified = user_modified_at < self.modified_at
|
|
86
92
|
|
|
87
|
-
log.info(
|
|
88
|
-
f"[OIDC] User modified at: {user_modified_at}, "
|
|
89
|
-
f"modified_at: {self.modified_at}, "
|
|
90
|
-
f"is_modified: {is_modified}, "
|
|
91
|
-
f"email: {user.email}",
|
|
92
|
-
)
|
|
93
93
|
if self.modified_at and is_modified:
|
|
94
94
|
self._update_user(user)
|
|
95
95
|
if self.on_update:
|
|
96
96
|
self.on_update(user, self.request, self.payload)
|
|
97
97
|
|
|
98
|
+
def _clean_kc_id(self):
|
|
99
|
+
model.objects.filter(**{settings.OIDC_USER_UID: self.kwargs[settings.OIDC_USER_UID]}).update(
|
|
100
|
+
**{settings.OIDC_USER_UID: None}
|
|
101
|
+
)
|
|
102
|
+
|
|
98
103
|
def get_user(self) -> model:
|
|
99
104
|
"""
|
|
100
105
|
Get user from database by kc_id or email.
|
|
@@ -103,18 +108,29 @@ class UserHandler:
|
|
|
103
108
|
"""
|
|
104
109
|
|
|
105
110
|
try:
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
with transaction.atomic():
|
|
112
|
+
user = model.objects.get(**{settings.OIDC_USER_UID: self.kwargs[settings.OIDC_USER_UID]})
|
|
113
|
+
self._update_existing_user(user)
|
|
108
114
|
return user
|
|
109
115
|
|
|
116
|
+
except IntegrityError:
|
|
117
|
+
# User with this email already exists
|
|
118
|
+
self._clean_kc_id()
|
|
119
|
+
return self._get_by_email() # Will update kc_id
|
|
120
|
+
|
|
110
121
|
except model.DoesNotExist:
|
|
111
122
|
self._collect_user_data()
|
|
112
123
|
try:
|
|
113
124
|
return self._get_by_email()
|
|
114
125
|
except model.DoesNotExist:
|
|
115
126
|
return self._create_new_user()
|
|
127
|
+
|
|
116
128
|
except model.MultipleObjectsReturned:
|
|
117
|
-
log.
|
|
129
|
+
log.warning(
|
|
130
|
+
f"[OIDC] Multiple users found by {settings.OIDC_USER_UID}: {self.kwargs[settings.OIDC_USER_UID]}"
|
|
131
|
+
)
|
|
132
|
+
# clear kc_id if multiple users found
|
|
133
|
+
self._clean_kc_id()
|
|
118
134
|
return self._get_by_email()
|
|
119
135
|
|
|
120
136
|
|
|
@@ -9,6 +9,8 @@ from django.urls import reverse
|
|
|
9
9
|
from jwt.api_jwt import ExpiredSignatureError
|
|
10
10
|
|
|
11
11
|
from django_jwt import settings
|
|
12
|
+
from django_jwt.config import config
|
|
13
|
+
from django_jwt.exceptions import ConfigException
|
|
12
14
|
from django_jwt.middleware import JWTAuthMiddleware
|
|
13
15
|
from django_jwt.roles import ROLE
|
|
14
16
|
from django_jwt.user import role_handler
|
|
@@ -112,14 +114,36 @@ class OIDCHandlerTest(TestCase):
|
|
|
112
114
|
self.assertUserWithPayload()
|
|
113
115
|
|
|
114
116
|
def test_exists_multiple_kc_id(self, *_):
|
|
115
|
-
"""
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
"""
|
|
118
|
+
KC ID exists in database by email
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
user = User.objects.create(email="example@bk.com", kc_id="1234")
|
|
122
|
+
User.objects.create(email="example@gmail.com", kc_id="1234", username="other")
|
|
123
|
+
|
|
118
124
|
self.middleware.process_request(self.request)
|
|
125
|
+
|
|
119
126
|
self.assertEqual(self.request.user, user)
|
|
127
|
+
self.assertUserWithPayload()
|
|
120
128
|
|
|
121
|
-
|
|
129
|
+
def test_new_email_exists(self, user_info, access_token):
|
|
130
|
+
"""Test case when:
|
|
131
|
+
- some email 'A' exists in DB with some KC ID
|
|
132
|
+
- user change email in KC from 'B' to 'A'
|
|
133
|
+
- KC ID will be attached to existing user with email 'A'
|
|
134
|
+
"""
|
|
135
|
+
user_info.return_value["email"] = "a@bk.com"
|
|
136
|
+
user_a = User.objects.create(email="a@bk.com", username="a")
|
|
137
|
+
user_b = User.objects.create(email="b@bk.com", kc_id="1234", username="b")
|
|
138
|
+
|
|
139
|
+
self.middleware.process_request(self.request)
|
|
140
|
+
user_a.refresh_from_db()
|
|
141
|
+
user_b.refresh_from_db()
|
|
142
|
+
|
|
143
|
+
self.assertEqual(self.request.user, user_a)
|
|
122
144
|
self.assertUserWithPayload()
|
|
145
|
+
self.assertEqual(user_a.kc_id, "1234")
|
|
146
|
+
self.assertEqual(user_b.kc_id, None)
|
|
123
147
|
|
|
124
148
|
def test_exists_email_differeent_kc_id_user(self, *_):
|
|
125
149
|
"""User exists in database by email but different kc_id"""
|
|
@@ -178,7 +202,7 @@ class OIDCHandlerTest(TestCase):
|
|
|
178
202
|
self.middleware.process_request(self.request)
|
|
179
203
|
self.assertEqual(self.request.user.username, "override")
|
|
180
204
|
|
|
181
|
-
def test_updated_at(self,
|
|
205
|
+
def test_updated_at(self, user_info, access_token):
|
|
182
206
|
"""Check that
|
|
183
207
|
- the updated_at field saved correct
|
|
184
208
|
- don't call userdata if updated_at is not changed
|
|
@@ -197,7 +221,26 @@ class OIDCHandlerTest(TestCase):
|
|
|
197
221
|
self.middleware.process_request(self.request)
|
|
198
222
|
user.refresh_from_db()
|
|
199
223
|
self.assertEqual(user.modified_timestamp, updated_at)
|
|
200
|
-
|
|
224
|
+
self.assertEqual(user_info.call_count, 1)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@patch("django_jwt.utils.get_alg", return_value="HS256")
|
|
228
|
+
class ConfigTest(TestCase):
|
|
229
|
+
def setUp(self):
|
|
230
|
+
self.middleware = JWTAuthMiddleware(get_response=lambda x: x)
|
|
231
|
+
self.request = Mock()
|
|
232
|
+
self.request.META = {"HTTP_AUTHORIZATION": "Bearer Token"}
|
|
233
|
+
|
|
234
|
+
@patch.object(config, "route", {})
|
|
235
|
+
def test_empty_routes(self, *_):
|
|
236
|
+
with self.assertRaises(ConfigException):
|
|
237
|
+
self.middleware.process_request(self.request)
|
|
238
|
+
|
|
239
|
+
@patch.object(config, "route", {"ES256": "http://localhost:8080"})
|
|
240
|
+
def test_not_supported_alg(self, *_):
|
|
241
|
+
response = self.middleware.process_request(self.request)
|
|
242
|
+
self.assertEqual(HTTPStatus.UNAUTHORIZED.value, response.status_code)
|
|
243
|
+
self.assertEqual(b'{"detail": "Algorithm HS256 is not supported"}', response.content)
|
|
201
244
|
|
|
202
245
|
|
|
203
246
|
class RolesTest(TestCase):
|
|
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
|