dj-jwt-auth 1.4.1__py3-none-any.whl → 1.5.1__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
1
  Metadata-Version: 2.1
2
2
  Name: dj-jwt-auth
3
- Version: 1.4.1
3
+ Version: 1.5.1
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
@@ -70,14 +70,22 @@ User retated variables:
70
70
  - OIDC_TOKEN_MODIFIED_FIELD - access token field to store last modified date, by default `updated_at`
71
71
  - OIDC_USER_UID - User model" unique identifier, by default `kc_id`
72
72
  - OIDC_TOKEN_USER_UID - access token field to store user UID, by default `sub`
73
- - OIDC_USER_MAPPING - mapping between JWT claims and user model fields, by default:
73
+ - OIDC_USER_MAPPING - mapping between JWT claims and user model fields. Can be dict or function. By default:
74
74
  ```
75
75
  OIDC_USER_MAPPING = {
76
- "first_name": "first_name",
77
- "last_name": "last_name",
78
- "username": "username",
76
+ "given_name": "first_name",
77
+ "family_name": "last_name",
78
+ "name": "username",
79
79
  }
80
80
  ```
81
+ OR
82
+ ```
83
+ def OIDC_USER_MAPPING(userinfo):
84
+ return {
85
+ "first_name": userinfo.get("given_name"),
86
+ "last_name": userinfo.get("family_name"),
87
+ "username": userinfo.get("name"),
88
+ }
81
89
  - OIDC_USER_DEFAULTS - default values for user model fields, by default:
82
90
  ```
83
91
  OIDC_USER_DEFAULTS = {
@@ -94,7 +102,7 @@ These functions should accept two arguments: user and request.
94
102
 
95
103
  ### Admin panel integration:
96
104
  To integrate admin panel with OIDC, add OIDC_ADMIN_ISSUER and OIDC_ADMIN_CLIENT_ID to settings.
97
- - OIDC_ADMIN_ISSUER - required for admin-panel access through OIDC. Example:
105
+ - OIDC_ADMIN_ISSUER - for admin-panel access through OIDC. By default will be used 'ES256' from OIDC_CONFIG_ROUTES. Example:
98
106
  ```
99
107
  OIDC_ADMIN_ISSUER = "https://keyCloak/realms/h/.well-known/openid-configuration"
100
108
  ```
@@ -1,19 +1,19 @@
1
1
  django_jwt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- django_jwt/config.py,sha256=BM_JeyJRB9qms9W5jcykkAJ5Cq3JqyAgvNE6dRrXSYw,1301
2
+ django_jwt/config.py,sha256=-9JkGjMXRVNmQYPvrEwaoJacu068nySNfjMyo5DtXcw,1550
3
3
  django_jwt/exceptions.py,sha256=vFJcGOCSZvxJRbSeMgWgPUM9wcXu6CSHblpwMzhV-Ic,198
4
4
  django_jwt/middleware.py,sha256=4PiF0-v13aLjvTyeyumQqYFimb6gCDHBgm6KooPGZdM,1176
5
5
  django_jwt/pkce.py,sha256=HYIQI0vKSmQkYIqTj3cciIT01ldkjhqlYiXkYcnNSGc,711
6
6
  django_jwt/roles.py,sha256=SaHK3o8T8USS4ZhG4SrHPlZQV2lMb2t1UZHT6IQtBvA,143
7
- django_jwt/settings.py,sha256=NhA0froKOtkAD4QeO1TpEHdEPY-7z85wue9ceA8-sJ4,1552
8
- django_jwt/urls.py,sha256=OoKbJ2kf41tuDBnVjK5TTW4aVt9bhRaz59HFlUOAins,251
9
- django_jwt/user.py,sha256=DW87Vt22rt-DOFH6wmgHSbcoHPa19yuAkGijhhu0uxA,5011
7
+ django_jwt/settings.py,sha256=gJePa3ER0vY6k5sDk-L1VagjbF4_dYrP0zrRJkGNY6Y,1708
8
+ django_jwt/urls.py,sha256=PmNoMxcVg_1oCDHHQJFAcPxhPAOkiMhd4PFnS-Q3JLA,326
9
+ django_jwt/user.py,sha256=DrUpsaGEv7P9Qi7GxcabPKH76mv8rSWpsnvxUMlmjGQ,5241
10
10
  django_jwt/utils.py,sha256=Gz8cH0cD3y_cvW8FwRoCFgShBrYvcB7XBF0GWx0n2qQ,1485
11
- django_jwt/views.py,sha256=Mwcd70Qrp5aeZYgXWBMzkm8DD01Tf1nAVlfq6wIlhQY,3705
11
+ django_jwt/views.py,sha256=LweS9G_NBeiuVDLhtm_GtOi_Ok6Sz5KJVTU62k91Jcg,4352
12
12
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  tests/models.py,sha256=K5e0QCgyZeLLHS6i3KRMQHooql47g7qqni7f9tKQrIY,251
14
- tests/test.py,sha256=OzfDEIgbDZvCinV_terIYEYjq7vPvhQQIqa0qgQNtxo,7405
14
+ tests/test.py,sha256=g1Itea87V6hqnK3FGX_nSq0znLRFxPW6WDNTuxYPf3M,8785
15
15
  tests/urls.py,sha256=D5FhDSVAudurkrpkCZZPnDvgXSgifwFVB3nAlYBg7uQ,212
16
- dj_jwt_auth-1.4.1.dist-info/METADATA,sha256=-9vahYCqTZgczLROxpUTROC5citx4QCQzJkN5fg5CME,4073
17
- dj_jwt_auth-1.4.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
18
- dj_jwt_auth-1.4.1.dist-info/top_level.txt,sha256=58O7TdK-yECZcbmPc52KNlBFpjIUlENuZubCxaSOxus,17
19
- dj_jwt_auth-1.4.1.dist-info/RECORD,,
16
+ dj_jwt_auth-1.5.1.dist-info/METADATA,sha256=4SsXuxjNUEW3lKT0ekPnpSnEthQ7Qdh14Umb8MKVcLw,4369
17
+ dj_jwt_auth-1.5.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
18
+ dj_jwt_auth-1.5.1.dist-info/top_level.txt,sha256=58O7TdK-yECZcbmPc52KNlBFpjIUlENuZubCxaSOxus,17
19
+ dj_jwt_auth-1.5.1.dist-info/RECORD,,
django_jwt/config.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  from functools import cache
3
+ from urllib.parse import urljoin
3
4
 
4
5
  import requests
5
6
  from jwt.algorithms import ECAlgorithm, RSAAlgorithm
@@ -8,6 +9,12 @@ from django_jwt import settings
8
9
  from django_jwt.exceptions import ConfigException
9
10
 
10
11
 
12
+ def ensure_well_known(url: str) -> str:
13
+ if url.endswith(".well-known/openid-configuration"):
14
+ return url
15
+ return urljoin(url, ".well-known/openid-configuration")
16
+
17
+
11
18
  class Config:
12
19
  def __init__(self):
13
20
  self.route = settings.OIDC_CONFIG_ROUTES
@@ -17,7 +24,7 @@ class Config:
17
24
  if not self.route:
18
25
  raise ConfigException("OIDC_CONFIG_ROUTES is not set")
19
26
 
20
- response = requests.get(self.route[alg])
27
+ response = requests.get(ensure_well_known(self.route[alg]))
21
28
  response.raise_for_status()
22
29
  return response.json()
23
30
 
@@ -35,7 +42,7 @@ class Config:
35
42
  @cache
36
43
  def admin(self) -> dict:
37
44
  if settings.OIDC_ADMIN_ISSUER:
38
- response = requests.get(settings.OIDC_ADMIN_ISSUER)
45
+ response = requests.get(ensure_well_known(settings.OIDC_ADMIN_ISSUER))
39
46
  response.raise_for_status()
40
47
  return response.json()
41
48
  raise ConfigException("OIDC_ADMIN_ISSUER is not set")
django_jwt/settings.py CHANGED
@@ -1,8 +1,8 @@
1
1
  from django.conf import settings
2
+
2
3
  from django_jwt.roles import ROLE
3
4
 
4
5
  OIDC_AUDIENCE = getattr(settings, "OIDC_AUDIENCE", ["account", "broker"])
5
- OIDC_CONFIG_URL = getattr(settings, "OIDC_CONFIG_URL", None)
6
6
 
7
7
  # key from KeyCloak; value is user model
8
8
  OIDC_USER_UPDATE = getattr(settings, "OIDC_USER_UPDATE", True)
@@ -37,11 +37,18 @@ OIDC_USER_ON_UPDATE = getattr(
37
37
  None,
38
38
  )
39
39
 
40
- OIDC_CONFIG_ROUTES = getattr(settings, "OIDC_CONFIG_ROUTES", None)
40
+ OIDC_CONFIG_ROUTES = getattr(settings, "OIDC_CONFIG_ROUTES", {})
41
41
  OIDC_ADMIN_ISSUER = getattr(settings, "OIDC_ADMIN_ISSUER", None)
42
42
  OIDC_ADMIN_CLIENT_ID = getattr(settings, "OIDC_ADMIN_CLIENT_ID", "cs-completeanatomy-admin")
43
43
  OIDC_ADMIN_SCOPE = getattr(settings, "OIDC_ADMIN_SCOPE", "openid")
44
44
  OIDC_ADMIN_ROLES = getattr(settings, "OIDC_ADMIN_ROLES", [])
45
45
 
46
+ if not OIDC_ADMIN_ISSUER:
47
+ OIDC_ADMIN_ISSUER = OIDC_CONFIG_ROUTES.get("ES256", None)
48
+
46
49
  for role in OIDC_ADMIN_ROLES:
47
50
  assert isinstance(role, ROLE), f"Role must be a namedtuple, got {type(role)}"
51
+
52
+ assert isinstance(OIDC_USER_MAPPING, dict) or callable(
53
+ OIDC_USER_MAPPING
54
+ ), "OIDC_USER_MAPPING must be a dict or function"
django_jwt/urls.py CHANGED
@@ -4,5 +4,6 @@ from django_jwt import views
4
4
 
5
5
  urlpatterns = [
6
6
  path("oidc/callback/", views.ReceiveRedirectView.as_view(), name="receive_redirect_view"),
7
+ path("oidc/logout/", views.silent_sso_check, name="silent_sso_check"),
7
8
  path("oidc/", views.StartOIDCAuthView.as_view(), name="start_oidc_auth"),
8
9
  ]
django_jwt/user.py CHANGED
@@ -16,6 +16,12 @@ log = getLogger(__name__)
16
16
  model = get_user_model()
17
17
 
18
18
 
19
+ def mapper(user_data: dict) -> dict:
20
+ if callable(settings.OIDC_USER_MAPPING):
21
+ return settings.OIDC_USER_MAPPING(user_data)
22
+ return {ca_key: user_data[kc_key] for kc_key, ca_key in settings.OIDC_USER_MAPPING.items() if kc_key in user_data}
23
+
24
+
19
25
  class UserHandler:
20
26
  modified_at = None
21
27
 
@@ -37,11 +43,9 @@ class UserHandler:
37
43
  """Collect user data from KeyCloak"""
38
44
 
39
45
  user_data = oidc_handler.get_user_info(self.access_token)
40
- self.kwargs["email"] = user_data["email"].lower()
41
- self.kwargs.update(
42
- {ca_key: user_data[kc_key] for kc_key, ca_key in settings.OIDC_USER_MAPPING.items() if kc_key in user_data}
43
- )
44
46
  log.info(f"User data: {self.kwargs}, access_token: {self.access_token}")
47
+ self.kwargs["email"] = user_data["email"].lower()
48
+ self.kwargs.update(mapper(user_data))
45
49
 
46
50
  def _update_user(self, user):
47
51
  """Update user fields if they are changed"""
@@ -130,17 +134,20 @@ class RoleHandler:
130
134
  def get_groups(self, role_name: str) -> Group:
131
135
  return Group.objects.filter(name__in=self.roles[role_name].groups)
132
136
 
133
- def apply(self, user: model, access_token: dict):
137
+ def apply(self, user: model, access_token: dict) -> list[str]:
134
138
  token_roles = access_token.get("resource_access", {}).get(settings.OIDC_ADMIN_CLIENT_ID, {}).get("roles", [])
135
139
  for role_name in token_roles:
136
140
  if role_name in self.roles:
137
141
  role = self.roles[role_name]
138
142
  user.groups.add(*self.get_groups(role_name))
139
143
  user.user_permissions.add(*self.get_permissions(role_name))
144
+ user.is_staff = True
140
145
  if role.is_superuser != user.is_superuser:
141
146
  user.is_superuser = role.is_superuser
142
- user.save(update_fields=["is_superuser"])
147
+ user.save(update_fields=["is_superuser", "is_staff"])
143
148
  break
144
149
 
150
+ return token_roles
151
+
145
152
 
146
153
  role_handler = RoleHandler()
django_jwt/views.py CHANGED
@@ -8,7 +8,7 @@ from django.conf import settings
8
8
  from django.contrib.auth import login
9
9
  from django.core.cache import cache
10
10
  from django.http.response import HttpResponse
11
- from django.shortcuts import redirect
11
+ from django.shortcuts import redirect, render
12
12
  from django.urls import reverse
13
13
  from django.views import View
14
14
  from requests.exceptions import HTTPError
@@ -27,19 +27,34 @@ def silent_sso_check(request):
27
27
  return HttpResponse("<html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>")
28
28
 
29
29
 
30
+ def index_response(request, msg, status=400):
31
+ logout_url = config.admin().get("end_session_endpoint")
32
+ return render(
33
+ request,
34
+ "django-jwt-index.html",
35
+ {
36
+ "error_message": msg,
37
+ "login_url": reverse("start_oidc_auth"),
38
+ "logout_url": logout_url,
39
+ "redirect_uri": request.build_absolute_uri(reverse("start_oidc_auth")),
40
+ },
41
+ status=status,
42
+ )
43
+
44
+
30
45
  class AbsView(View):
31
46
  def dispatch(self, request, *args, **kwargs):
32
47
  try:
33
48
  return super().dispatch(request, *args, **kwargs)
34
49
  except HTTPError as exc:
35
50
  log.warning(f"OIDC Admin HTTPError: {exc}")
36
- return HttpResponse(status=exc.response.status_code, content=exc.response.text)
51
+ return index_response(request=request, msg=exc.response.text, status=exc.response.status_code)
37
52
  except ConfigException as exc:
38
53
  return HttpResponse(content=str(exc), status=500)
39
54
  except BadRequestException as exc:
40
- return HttpResponse(content=str(exc), status=400)
41
- except Exception:
42
- return redirect("start_oidc_auth")
55
+ return index_response(request=request, msg=str(exc))
56
+ except Exception as exc:
57
+ return index_response(request=request, msg=str(exc))
43
58
 
44
59
 
45
60
  class StartOIDCAuthView(AbsView):
@@ -81,10 +96,15 @@ class ReceiveRedirectView(AbsView):
81
96
  data = oidc_handler.decode_token(token)
82
97
  user = UserHandler(data, request, token).get_user()
83
98
  log.info(f"OIDC Admin login: {user}", extra={"data": data})
84
- role_handler.apply(user, data)
99
+ roles = role_handler.apply(user, data)
85
100
  if not user.is_staff:
86
- raise BadRequestException("User is not staff")
101
+ raise BadRequestException(f"User {user.email} is not staff\nRoles: {roles}")
87
102
  login(request, user, backend=settings.DEFAULT_AUTHENTICATION_BACKEND)
88
103
  return redirect("admin:index")
89
104
 
90
105
  raise BadRequestException("No PKCE secret found in cache")
106
+
107
+
108
+ class LogoutView(AbsView):
109
+ def get(self, request):
110
+ return index_response(request, "Logged out", status=401)
tests/test.py CHANGED
@@ -3,14 +3,14 @@ from unittest.mock import Mock, patch
3
3
 
4
4
  from django.contrib.auth import get_user_model
5
5
  from django.contrib.auth.models import Group, Permission
6
- from django.test import TestCase
6
+ from django.test import TestCase, override_settings
7
7
  from django.urls import reverse
8
8
  from jwt.api_jwt import ExpiredSignatureError
9
9
 
10
10
  from django_jwt import settings
11
11
  from django_jwt.middleware import JWTAuthMiddleware
12
- from django_jwt.user import role_handler
13
12
  from django_jwt.roles import ROLE
13
+ from django_jwt.user import role_handler
14
14
 
15
15
  access_token_payload = {
16
16
  "sub": "1234",
@@ -36,6 +36,16 @@ def _on_update(user, request):
36
36
  user.save()
37
37
 
38
38
 
39
+ def test_mapper(user_data: dict) -> dict:
40
+ """Override user data - set username to 'override'"""
41
+
42
+ return {
43
+ "username": "override",
44
+ "first_name": user_data["given_name"],
45
+ "last_name": user_data["family_name"],
46
+ }
47
+
48
+
39
49
  @patch("django_jwt.utils.OIDCHandler.decode_token", return_value=access_token_payload)
40
50
  @patch("django_jwt.utils.OIDCHandler.get_user_info", return_value=user_info_payload)
41
51
  class OIDCHandlerTest(TestCase):
@@ -43,6 +53,11 @@ class OIDCHandlerTest(TestCase):
43
53
  self.middleware = JWTAuthMiddleware(get_response=lambda x: x)
44
54
  self.request = Mock()
45
55
  self.request.META = {"HTTP_AUTHORIZATION": "Bearer 1234"}
56
+ settings.OIDC_USER_MAPPING = { # default mapping
57
+ "given_name": "first_name",
58
+ "family_name": "last_name",
59
+ "name": "username",
60
+ }
46
61
 
47
62
  def assertUserWithPayload(self):
48
63
  self.assertEqual(self.request.user.first_name, user_info_payload["given_name"])
@@ -135,6 +150,22 @@ class OIDCHandlerTest(TestCase):
135
150
  self.middleware.process_request(self.request)
136
151
  self.assertEqual(self.request.user.username, "on_update")
137
152
 
153
+ def test_user_data_mapping(self, *_):
154
+ """User data is mapped"""
155
+
156
+ settings.OIDC_USER_MAPPING = {"name": "username", "given_name": "last_name", "family_name": "first_name"}
157
+ self.middleware.process_request(self.request)
158
+ self.assertEqual(self.request.user.username, user_info_payload["name"])
159
+ self.assertEqual(self.request.user.first_name, user_info_payload["family_name"])
160
+ self.assertEqual(self.request.user.last_name, user_info_payload["given_name"])
161
+
162
+ def test_user_data_mapping_callable(self, *_):
163
+ """User data is mapped"""
164
+
165
+ settings.OIDC_USER_MAPPING = test_mapper
166
+ self.middleware.process_request(self.request)
167
+ self.assertEqual(self.request.user.username, "override")
168
+
138
169
 
139
170
  class RolesTest(TestCase):
140
171
  def setUp(self) -> None:
@@ -160,21 +191,25 @@ class RolesTest(TestCase):
160
191
  self.assertTrue(self.user.groups.filter(name="staff group").exists())
161
192
  self.assertTrue(self.user.user_permissions.filter(codename="add_user").exists())
162
193
  self.assertFalse(self.user.is_superuser)
194
+ self.assertTrue(self.user.is_staff)
163
195
 
164
196
  def test_admin_role(self):
165
197
  self.access_token["resource_access"][settings.OIDC_ADMIN_CLIENT_ID]["roles"] = ["admin"]
166
198
  role_handler.apply(self.user, self.access_token)
167
199
  self.assertTrue(self.user.is_superuser)
200
+ self.assertTrue(self.user.is_staff)
168
201
 
169
202
  def test_apply_staff_then_admin_role(self):
170
203
  self.access_token["resource_access"][settings.OIDC_ADMIN_CLIENT_ID]["roles"] = ["staff"]
171
204
  role_handler.apply(self.user, self.access_token)
172
205
  self.assertFalse(self.user.is_superuser)
206
+ self.assertTrue(self.user.is_staff)
173
207
  self.assertTrue(self.user.groups.filter(name="staff group").exists())
174
208
  self.assertTrue(self.user.user_permissions.filter(codename="add_user").exists())
175
209
 
176
210
  self.access_token["resource_access"][settings.OIDC_ADMIN_CLIENT_ID]["roles"] = ["admin"]
177
211
  role_handler.apply(self.user, self.access_token)
178
212
  self.assertTrue(self.user.is_superuser)
213
+ self.assertTrue(self.user.is_staff)
179
214
  self.assertTrue(self.user.groups.filter(name="staff group").exists())
180
215
  self.assertTrue(self.user.user_permissions.filter(codename="add_user").exists())