dj-jwt-auth 1.7.1__tar.gz → 1.9.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.1 → dj_jwt_auth-1.9.0}/PKG-INFO +1 -1
  2. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/dj_jwt_auth.egg-info/PKG-INFO +1 -1
  3. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/user.py +29 -13
  4. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/views.py +52 -36
  5. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/setup.cfg +1 -1
  6. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/tests/models.py +1 -0
  7. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/tests/test.py +28 -6
  8. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/MANIFEST.in +0 -0
  9. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/README.md +0 -0
  10. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/dj_jwt_auth.egg-info/SOURCES.txt +0 -0
  11. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/dj_jwt_auth.egg-info/dependency_links.txt +0 -0
  12. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/dj_jwt_auth.egg-info/requires.txt +0 -0
  13. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/dj_jwt_auth.egg-info/top_level.txt +0 -0
  14. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/__init__.py +0 -0
  15. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/config.py +0 -0
  16. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/exceptions.py +0 -0
  17. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/middleware.py +0 -0
  18. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/pkce.py +0 -0
  19. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/roles.py +0 -0
  20. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/settings.py +0 -0
  21. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/templates/admin/login.html +0 -0
  22. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/templates/django-jwt-index.html +0 -0
  23. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/urls.py +0 -0
  24. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/django_jwt/utils.py +0 -0
  25. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/pyproject.toml +0 -0
  26. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/setup.py +0 -0
  27. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.0}/tests/__init__.py +0 -0
  28. {dj_jwt_auth-1.7.1 → dj_jwt_auth-1.9.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.1
3
+ Version: 1.9.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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dj-jwt-auth
3
- Version: 1.7.1
3
+ Version: 1.9.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
@@ -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
 
@@ -39,68 +39,84 @@ def index_response(request, msg, status=400):
39
39
  )
40
40
 
41
41
 
42
- class AbsView(View):
43
- def dispatch(self, request, *args, **kwargs):
44
- try:
45
- return super().dispatch(request, *args, **kwargs)
46
- except HTTPError as exc:
47
- log.warning(f"OIDC Admin HTTPError: {exc}")
48
- return index_response(request=request, msg=exc.response.text, status=exc.response.status_code)
49
- except ConfigException as exc:
50
- return HttpResponse(content=str(exc), status=500)
51
- except BadRequestException as exc:
52
- return index_response(request=request, msg=str(exc))
53
- except Exception as exc:
54
- return index_response(request=request, msg=str(exc))
42
+ class InitiateView(View):
43
+ callback_view_name = "receive_redirect_view"
44
+ client_id = None
45
+ scope = "openid"
55
46
 
56
-
57
- class StartOIDCAuthView(AbsView):
58
47
  def get(self, request):
59
48
  pkce_secret = PKCESecret()
60
- redirect_uri = jwt_settings.OIDC_ADMIN_REDIRECT_URI
61
- if not redirect_uri:
62
- redirect_uri = request.build_absolute_uri(reverse("receive_redirect_view"))
49
+ redirect_uri = request.build_absolute_uri(reverse(self.callback_view_name))
63
50
  authorization_endpoint = config.admin().get("authorization_endpoint")
64
51
  state = base64.urlsafe_b64encode(get_random_string().encode()).decode()
65
52
  params = {
66
- "client_id": jwt_settings.OIDC_ADMIN_CLIENT_ID,
53
+ "client_id": self.client_id,
67
54
  "redirect_uri": redirect_uri,
68
55
  "response_type": "code",
69
56
  "state": state,
70
- "scope": jwt_settings.OIDC_ADMIN_SCOPE,
57
+ "scope": self.scope,
71
58
  "code_challenge": pkce_secret.challenge,
72
59
  "code_challenge_method": pkce_secret.challenge_method,
73
60
  "ui_locales": "en",
74
61
  "nonce": get_random_string(),
75
62
  }
76
63
  cache.set(state, str(pkce_secret), timeout=600)
77
- log.info(f"OIDC Admin login: {authorization_endpoint}?{urlencode(params)}")
64
+ log.info(f"OIDC Initiate: {authorization_endpoint}?{urlencode(params)}")
78
65
  return redirect(f"{authorization_endpoint}?{urlencode(params)}")
79
66
 
80
67
 
81
- class ReceiveRedirectView(AbsView):
82
- def get(self, request):
68
+ class CallbackView(View):
69
+ callback_view_name = "receive_redirect_view"
70
+ user = None
71
+ payload = None
72
+
73
+ def fail(self, request, msg):
74
+ raise BadRequestException(msg)
75
+
76
+ def dispatch(self, request, *args, **kwargs):
83
77
  code = request.GET.get("code")
84
78
  state = request.GET.get("state")
85
79
  if not code or not state:
86
- log.warning(f"No code or state in the request {request.GET}")
87
- raise BadRequestException("No code or state in the request")
80
+ log.warning(f"OIDC No code or state in the request {request.GET}")
81
+ return self.fail(request, "No code or state in the request")
88
82
 
89
- redirect_uri = request.build_absolute_uri(reverse("receive_redirect_view"))
83
+ redirect_uri = request.build_absolute_uri(self.callback_view_name)
90
84
  if state := cache.get(state):
91
85
  token = get_access_token(code, redirect_uri, state)
92
- data = oidc_handler.decode_token(token)
93
- user = UserHandler(data, request, token).get_user()
94
- log.info(f"OIDC Admin login: {user}", extra={"data": data})
95
- roles = role_handler.apply(user, data)
96
- if not user.is_staff:
97
- raise BadRequestException(f"User {user.email} is not staff\nRoles: {roles}")
98
- login(request, user, backend=jwt_settings.OIDC_AUTHORIZATION_BACKEND)
99
- return redirect("admin:index")
86
+ self.payload = oidc_handler.decode_token(token)
87
+ self.user = UserHandler(self.payload, request, token).get_user()
88
+ return super().dispatch(request, *args, **kwargs)
89
+ return self.fail(request, "No PKCE secret found in cache")
100
90
 
101
- raise BadRequestException("No PKCE secret found in cache")
91
+
92
+ class StartOIDCAuthView(InitiateView):
93
+ client_id = jwt_settings.OIDC_ADMIN_CLIENT_ID
94
+ scope = jwt_settings.OIDC_ADMIN_SCOPE
95
+
96
+
97
+ class ReceiveRedirectView(CallbackView):
98
+ def dispatch(self, request, *args, **kwargs):
99
+ try:
100
+ return super().dispatch(request, *args, **kwargs)
101
+ except HTTPError as exc:
102
+ log.warning(f"OIDC Admin HTTPError: {exc}")
103
+ return index_response(request=request, msg=exc.response.text, status=exc.response.status_code)
104
+ except ConfigException as exc:
105
+ return HttpResponse(content=str(exc), status=500)
106
+ except BadRequestException as exc:
107
+ return index_response(request=request, msg=str(exc))
108
+ except Exception as exc:
109
+ return index_response(request=request, msg=str(exc))
110
+
111
+ def get(self, request):
112
+ log.info(f"OIDC Admin login: {self.user}", extra={"data": self.payload})
113
+ roles = role_handler.apply(self.user, self.payload)
114
+ if not self.user.is_staff:
115
+ raise BadRequestException(f"User {self.user.email} is not staff\nRoles: {roles}")
116
+ login(request, self.user, backend=jwt_settings.OIDC_AUTHORIZATION_BACKEND)
117
+ return redirect("admin:index")
102
118
 
103
119
 
104
- class LogoutView(AbsView):
120
+ class LogoutView(View):
105
121
  def get(self, request):
106
122
  return index_response(request, "Logged out", status=401)
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = dj-jwt-auth
3
- version = 1.7.1
3
+ version = 1.9.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)
@@ -114,14 +114,36 @@ class OIDCHandlerTest(TestCase):
114
114
  self.assertUserWithPayload()
115
115
 
116
116
  def test_exists_multiple_kc_id(self, *_):
117
- """User exists in database by email"""
118
- user = User.objects.create(email="example@bk.com", kc_id="123")
119
- 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
+
120
124
  self.middleware.process_request(self.request)
125
+
121
126
  self.assertEqual(self.request.user, user)
127
+ self.assertUserWithPayload()
122
128
 
123
- # 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)
124
144
  self.assertUserWithPayload()
145
+ self.assertEqual(user_a.kc_id, "1234")
146
+ self.assertEqual(user_b.kc_id, None)
125
147
 
126
148
  def test_exists_email_differeent_kc_id_user(self, *_):
127
149
  """User exists in database by email but different kc_id"""
@@ -180,7 +202,7 @@ class OIDCHandlerTest(TestCase):
180
202
  self.middleware.process_request(self.request)
181
203
  self.assertEqual(self.request.user.username, "override")
182
204
 
183
- def test_updated_at(self, access_token, user_info):
205
+ def test_updated_at(self, user_info, access_token):
184
206
  """Check that
185
207
  - the updated_at field saved correct
186
208
  - don't call userdata if updated_at is not changed
@@ -199,7 +221,7 @@ class OIDCHandlerTest(TestCase):
199
221
  self.middleware.process_request(self.request)
200
222
  user.refresh_from_db()
201
223
  self.assertEqual(user.modified_timestamp, updated_at)
202
- # self.assertEqual(user_info.call_count, 1)
224
+ self.assertEqual(user_info.call_count, 1)
203
225
 
204
226
 
205
227
  @patch("django_jwt.utils.get_alg", return_value="HS256")
File without changes
File without changes
File without changes
File without changes
File without changes