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.
Files changed (28) hide show
  1. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/PKG-INFO +2 -1
  2. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/README.md +2 -1
  3. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/PKG-INFO +2 -1
  4. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/config.py +4 -1
  5. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/exceptions.py +6 -0
  6. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/middleware.py +3 -0
  7. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/user.py +29 -13
  8. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/setup.cfg +1 -1
  9. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/tests/models.py +1 -0
  10. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/tests/test.py +49 -6
  11. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/MANIFEST.in +0 -0
  12. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/SOURCES.txt +0 -0
  13. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/dependency_links.txt +0 -0
  14. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/requires.txt +0 -0
  15. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/dj_jwt_auth.egg-info/top_level.txt +0 -0
  16. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/__init__.py +0 -0
  17. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/pkce.py +0 -0
  18. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/roles.py +0 -0
  19. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/settings.py +0 -0
  20. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/templates/admin/login.html +0 -0
  21. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/templates/django-jwt-index.html +0 -0
  22. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/urls.py +0 -0
  23. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/utils.py +0 -0
  24. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/django_jwt/views.py +0 -0
  25. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/pyproject.toml +0 -0
  26. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/setup.py +0 -0
  27. {dj_jwt_auth-1.7.0 → dj_jwt_auth-1.8.0}/tests/__init__.py +0 -0
  28. {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.7.0
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.7.0
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()
@@ -8,3 +8,9 @@ class BadRequestException(Exception):
8
8
  """Base class for exceptions in this module."""
9
9
 
10
10
  pass
11
+
12
+
13
+ class AlgorithmNotSupportedException(Exception):
14
+ """Base class for exceptions in this module."""
15
+
16
+ pass
@@ -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
- user_data = oidc_handler.get_user_info(self.access_token)
45
- log.info(f"[OIDC] User data: {user_data}")
46
- self.kwargs["email"] = user_data["email"].lower()
47
- self.kwargs.update(mapper(user_data))
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
- user = model.objects.get(**{settings.OIDC_USER_UID: self.kwargs[settings.OIDC_USER_UID]})
107
- self._update_existing_user(user)
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.error(f"[OIDC] Multiple users found by {settings.OIDC_USER_UID}: {self.kwargs[settings.OIDC_USER_UID]}")
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
 
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = dj-jwt-auth
3
- version = 1.7.0
3
+ version = 1.8.0
4
4
  description = A Django package for JSON Web Token validation and verification. Using PyJWT.
5
5
  long_description = file: README.md
6
6
  url = https://www.example.com/
@@ -6,3 +6,4 @@ from django.utils import timezone
6
6
  class User(AbstractUser):
7
7
  kc_id = models.CharField(max_length=255, null=True, blank=True)
8
8
  modified_timestamp = models.DateTimeField(auto_now=False, default=timezone.now)
9
+ email = models.EmailField(unique=True)
@@ -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
- """User exists in database by email"""
116
- user = User.objects.create(email="example@bk.com", kc_id="123")
117
- User.objects.create(email="example@gmail.com", kc_id="123", username="other")
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
- # fields are updated if they are changed in KeyCloak
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, access_token, user_info):
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
- # self.assertEqual(user_info.call_count, 1)
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