dj-jwt-auth 1.2.1__py3-none-any.whl → 1.3.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.
- {dj_jwt_auth-1.2.1.dist-info → dj_jwt_auth-1.3.1.dist-info}/METADATA +50 -14
- {dj_jwt_auth-1.2.1.dist-info → dj_jwt_auth-1.3.1.dist-info}/RECORD +9 -8
- django_jwt/roles.py +3 -0
- django_jwt/settings.py +5 -0
- django_jwt/user.py +44 -0
- django_jwt/views.py +2 -1
- tests/test.py +47 -7
- {dj_jwt_auth-1.2.1.dist-info → dj_jwt_auth-1.3.1.dist-info}/WHEEL +0 -0
- {dj_jwt_auth-1.2.1.dist-info → dj_jwt_auth-1.3.1.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: dj-jwt-auth
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -31,13 +31,16 @@ Requires-Dist: cryptography >=36.0.2
|
|
|
31
31
|
This is a package to verify and validate JSON Web Tokens (JWT) in Django.
|
|
32
32
|
|
|
33
33
|
### Installation
|
|
34
|
-
1. Install the package using pip
|
|
34
|
+
1. Install the package using pip:
|
|
35
|
+
```bash
|
|
36
|
+
pip install dj-jwt-auth
|
|
37
|
+
```
|
|
35
38
|
|
|
36
39
|
2. Add "django_jwt" to your INSTALLED_APPS setting like this::
|
|
37
40
|
```
|
|
38
41
|
INSTALLED_APPS = [
|
|
39
42
|
...
|
|
40
|
-
|
|
43
|
+
"django_jwt",
|
|
41
44
|
]
|
|
42
45
|
```
|
|
43
46
|
|
|
@@ -45,39 +48,39 @@ This is a package to verify and validate JSON Web Tokens (JWT) in Django.
|
|
|
45
48
|
```
|
|
46
49
|
MIDDLEWARE = [
|
|
47
50
|
...
|
|
48
|
-
|
|
51
|
+
"django_jwt.middleware.JWTAuthMiddleware",
|
|
49
52
|
]
|
|
50
53
|
```
|
|
51
54
|
|
|
52
55
|
### Configuration:
|
|
53
56
|
Required variables:
|
|
54
|
-
- OIDC_CONFIG_ROUTES - dict of
|
|
57
|
+
- OIDC_CONFIG_ROUTES - dict of "algorithm": "config_url". Required for using JWTAuthMiddleware. Example:
|
|
55
58
|
```
|
|
56
59
|
OIDC_CONFIG_ROUTES = {
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
"RS256": "https://keyCloak/realms/h/.well-known/openid-configuration",
|
|
61
|
+
"HS256": "https://keyCloak/realms/h/.well-known/openid-configuration",
|
|
59
62
|
}
|
|
60
63
|
```
|
|
61
|
-
|
|
62
64
|
Optional variables:
|
|
63
65
|
- OIDC_AUDIENCE - by default ["account", "broker"]
|
|
66
|
+
|
|
64
67
|
User retated variables:
|
|
65
68
|
- OIDC_USER_UPDATE - if True, user model will be updated from userinfo endpoint if MODIFIED date has changed, by default True
|
|
66
69
|
- OIDC_USER_MODIFIED_FIELD - user model field to store last modified date, by default `modified_timestamp`
|
|
67
70
|
- OIDC_TOKEN_MODIFIED_FIELD - access token field to store last modified date, by default `updated_at`
|
|
68
|
-
- OIDC_USER_UID - User model
|
|
71
|
+
- OIDC_USER_UID - User model" unique identifier, by default `kc_id`
|
|
69
72
|
- OIDC_USER_MAPPING - mapping between JWT claims and user model fields, by default:
|
|
70
73
|
```
|
|
71
74
|
OIDC_USER_MAPPING = {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
"first_name": "first_name",
|
|
76
|
+
"last_name": "last_name",
|
|
77
|
+
"username": "username",
|
|
75
78
|
}
|
|
76
79
|
```
|
|
77
80
|
- OIDC_USER_DEFAULTS - default values for user model fields, by default:
|
|
78
81
|
```
|
|
79
82
|
OIDC_USER_DEFAULTS = {
|
|
80
|
-
|
|
83
|
+
"is_active": True,
|
|
81
84
|
}
|
|
82
85
|
```
|
|
83
86
|
|
|
@@ -86,8 +89,41 @@ User retated variables:
|
|
|
86
89
|
OIDC_USER_ON_CREATE = None
|
|
87
90
|
OIDC_USER_ON_UPDATE = None
|
|
88
91
|
```
|
|
89
|
-
- OIDC_ADMIN_ISSUER - URL of the OIDC provider, by default None
|
|
90
92
|
These functions should accept two arguments: user and request.
|
|
91
93
|
|
|
94
|
+
### Admin panel integration:
|
|
95
|
+
To integrate admin panel with OIDC, add OIDC_ADMIN_ISSUER and OIDC_ADMIN_CLIENT_ID to settings.
|
|
96
|
+
- OIDC_ADMIN_ISSUER - required for admin-panel access through OIDC. Example:
|
|
97
|
+
```
|
|
98
|
+
OIDC_ADMIN_ISSUER = "https://keyCloak/realms/h/.well-known/openid-configuration"
|
|
99
|
+
```
|
|
100
|
+
- OIDC_ADMIN_CLIENT_ID - by default "complete-anatomy"
|
|
101
|
+
To mapping roles to admin panel permissions, use OIDC_ADMIN_ROLES. Example:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
|
|
105
|
+
from django_jwt.roles import ROLE
|
|
106
|
+
|
|
107
|
+
OIDC_ADMIN_ROLES = [
|
|
108
|
+
ROLE(
|
|
109
|
+
name="admin", # name from token
|
|
110
|
+
is_superuser=True,
|
|
111
|
+
),
|
|
112
|
+
ROLE(
|
|
113
|
+
name="staff",
|
|
114
|
+
groups=["LMS (Full)", "Organizations (Full)", "Customer Support (Full)"],
|
|
115
|
+
permissions=["Can add user"],
|
|
116
|
+
),
|
|
117
|
+
]
|
|
118
|
+
```
|
|
119
|
+
And add login view to urls.py:
|
|
120
|
+
```python
|
|
121
|
+
urlpatterns = [
|
|
122
|
+
path("admin/", include("django_jwt.urls")),
|
|
123
|
+
...
|
|
124
|
+
]
|
|
125
|
+
```
|
|
126
|
+
Login URL will be available at `/admin/oidc/`.
|
|
127
|
+
|
|
92
128
|
### Testing:
|
|
93
129
|
Run command `python runtests.py` to run tests.
|
|
@@ -3,16 +3,17 @@ django_jwt/config.py,sha256=BM_JeyJRB9qms9W5jcykkAJ5Cq3JqyAgvNE6dRrXSYw,1301
|
|
|
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
|
-
django_jwt/
|
|
6
|
+
django_jwt/roles.py,sha256=SaHK3o8T8USS4ZhG4SrHPlZQV2lMb2t1UZHT6IQtBvA,143
|
|
7
|
+
django_jwt/settings.py,sha256=HnuHNoM3H5sGfDRc9hvXZDa9iTyleMNrN9Ted5KmFRk,1474
|
|
7
8
|
django_jwt/urls.py,sha256=OoKbJ2kf41tuDBnVjK5TTW4aVt9bhRaz59HFlUOAins,251
|
|
8
|
-
django_jwt/user.py,sha256=
|
|
9
|
+
django_jwt/user.py,sha256=_ZZdfAFdQeScMmWwkrzo-5XjTKMVCbmfK4FPpiCbaQQ,5021
|
|
9
10
|
django_jwt/utils.py,sha256=Gz8cH0cD3y_cvW8FwRoCFgShBrYvcB7XBF0GWx0n2qQ,1485
|
|
10
|
-
django_jwt/views.py,sha256=
|
|
11
|
+
django_jwt/views.py,sha256=Mwcd70Qrp5aeZYgXWBMzkm8DD01Tf1nAVlfq6wIlhQY,3705
|
|
11
12
|
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
13
|
tests/models.py,sha256=K5e0QCgyZeLLHS6i3KRMQHooql47g7qqni7f9tKQrIY,251
|
|
13
|
-
tests/test.py,sha256=
|
|
14
|
+
tests/test.py,sha256=uZz9eG1nC1CljITD6K79Ruodtr8SDPEnBSt_5GOLVSc,7460
|
|
14
15
|
tests/urls.py,sha256=D5FhDSVAudurkrpkCZZPnDvgXSgifwFVB3nAlYBg7uQ,212
|
|
15
|
-
dj_jwt_auth-1.
|
|
16
|
-
dj_jwt_auth-1.
|
|
17
|
-
dj_jwt_auth-1.
|
|
18
|
-
dj_jwt_auth-1.
|
|
16
|
+
dj_jwt_auth-1.3.1.dist-info/METADATA,sha256=EurjeCr-WQKFOZcAmjOs2p7Q7afp0KTWXfGQUK_0U04,3994
|
|
17
|
+
dj_jwt_auth-1.3.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
18
|
+
dj_jwt_auth-1.3.1.dist-info/top_level.txt,sha256=58O7TdK-yECZcbmPc52KNlBFpjIUlENuZubCxaSOxus,17
|
|
19
|
+
dj_jwt_auth-1.3.1.dist-info/RECORD,,
|
django_jwt/roles.py
ADDED
django_jwt/settings.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from django.conf import settings
|
|
2
|
+
from django_jwt.roles import ROLE
|
|
2
3
|
|
|
3
4
|
OIDC_AUDIENCE = getattr(settings, "OIDC_AUDIENCE", ["account", "broker"])
|
|
4
5
|
OIDC_CONFIG_URL = getattr(settings, "OIDC_CONFIG_URL", None)
|
|
@@ -39,3 +40,7 @@ OIDC_CONFIG_ROUTES = getattr(settings, "OIDC_CONFIG_ROUTES", None)
|
|
|
39
40
|
OIDC_ADMIN_ISSUER = getattr(settings, "OIDC_ADMIN_ISSUER", None)
|
|
40
41
|
OIDC_ADMIN_CLIENT_ID = getattr(settings, "OIDC_ADMIN_CLIENT_ID", "complete-anatomy")
|
|
41
42
|
OIDC_ADMIN_SCOPE = getattr(settings, "OIDC_ADMIN_SCOPE", "openid")
|
|
43
|
+
OIDC_ADMIN_ROLES = getattr(settings, "OIDC_ADMIN_ROLES", [])
|
|
44
|
+
|
|
45
|
+
for role in OIDC_ADMIN_ROLES:
|
|
46
|
+
assert isinstance(role, ROLE), f"Role must be a namedtuple, got {type(role)}"
|
django_jwt/user.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
+
from functools import cache
|
|
2
3
|
from logging import getLogger
|
|
3
4
|
|
|
4
5
|
import pytz
|
|
5
6
|
from django.contrib.auth import get_user_model
|
|
7
|
+
from django.contrib.auth.models import Group, Permission
|
|
6
8
|
from django.http.request import HttpRequest
|
|
7
9
|
|
|
8
10
|
from django_jwt import settings
|
|
@@ -100,3 +102,45 @@ class UserHandler:
|
|
|
100
102
|
return self._get_by_email()
|
|
101
103
|
except model.DoesNotExist:
|
|
102
104
|
return self._create_new_user()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class RoleHandler:
|
|
108
|
+
"""
|
|
109
|
+
Process user roles and permissions from access token.
|
|
110
|
+
Token be like:
|
|
111
|
+
...
|
|
112
|
+
"resource_access": {
|
|
113
|
+
"complete_anatomy": {
|
|
114
|
+
"roles": [
|
|
115
|
+
"admin"
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def roles(self) -> dict:
|
|
123
|
+
return {role.name: role for role in settings.OIDC_ADMIN_ROLES}
|
|
124
|
+
|
|
125
|
+
@cache
|
|
126
|
+
def get_permissions(self, role_name: str) -> Permission:
|
|
127
|
+
return Permission.objects.filter(codename__in=self.roles[role_name].permissions)
|
|
128
|
+
|
|
129
|
+
@cache
|
|
130
|
+
def get_groups(self, role_name: str) -> Group:
|
|
131
|
+
return Group.objects.filter(name__in=self.roles[role_name].groups)
|
|
132
|
+
|
|
133
|
+
def apply(self, user: model, access_token: dict):
|
|
134
|
+
token_roles = access_token.get("resource_access", {}).get(settings.OIDC_ADMIN_CLIENT_ID, {}).get("roles", [])
|
|
135
|
+
for role_name in token_roles:
|
|
136
|
+
if role_name in self.roles:
|
|
137
|
+
role = self.roles[role_name]
|
|
138
|
+
user.groups.add(*self.get_groups(role_name))
|
|
139
|
+
user.user_permissions.add(*self.get_permissions(role_name))
|
|
140
|
+
if role.is_superuser != user.is_superuser:
|
|
141
|
+
user.is_superuser = role.is_superuser
|
|
142
|
+
user.save(update_fields=["is_superuser"])
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
role_handler = RoleHandler()
|
django_jwt/views.py
CHANGED
|
@@ -17,7 +17,7 @@ from django_jwt import settings as jwt_settings
|
|
|
17
17
|
from django_jwt.config import config
|
|
18
18
|
from django_jwt.exceptions import BadRequestException, ConfigException
|
|
19
19
|
from django_jwt.pkce import PKCESecret
|
|
20
|
-
from django_jwt.user import UserHandler
|
|
20
|
+
from django_jwt.user import UserHandler, role_handler
|
|
21
21
|
from django_jwt.utils import get_access_token, oidc_handler
|
|
22
22
|
|
|
23
23
|
log = getLogger(__name__)
|
|
@@ -81,6 +81,7 @@ class ReceiveRedirectView(AbsView):
|
|
|
81
81
|
data = oidc_handler.decode_token(token)
|
|
82
82
|
user = UserHandler(data, request, token).get_user()
|
|
83
83
|
log.info(f"OIDC Admin login: {user}", extra={"data": data})
|
|
84
|
+
role_handler.apply(user, data)
|
|
84
85
|
if not user.is_staff:
|
|
85
86
|
raise BadRequestException("User is not staff")
|
|
86
87
|
login(request, user, backend=settings.DEFAULT_AUTHENTICATION_BACKEND)
|
tests/test.py
CHANGED
|
@@ -2,12 +2,15 @@ from http import HTTPStatus
|
|
|
2
2
|
from unittest.mock import Mock, patch
|
|
3
3
|
|
|
4
4
|
from django.contrib.auth import get_user_model
|
|
5
|
+
from django.contrib.auth.models import Group, Permission
|
|
5
6
|
from django.test import TestCase
|
|
6
7
|
from django.urls import reverse
|
|
7
8
|
from jwt.api_jwt import ExpiredSignatureError
|
|
8
9
|
|
|
9
10
|
from django_jwt import settings
|
|
10
11
|
from django_jwt.middleware import JWTAuthMiddleware
|
|
12
|
+
from django_jwt.user import role_handler
|
|
13
|
+
from django_jwt.roles import ROLE
|
|
11
14
|
|
|
12
15
|
access_token_payload = {
|
|
13
16
|
"sub": "12345",
|
|
@@ -102,13 +105,6 @@ class OIDCHandlerTest(TestCase):
|
|
|
102
105
|
# fields are updated if they are changed in KeyCloak
|
|
103
106
|
self.assertUserWithPayload()
|
|
104
107
|
|
|
105
|
-
# def test_roles(self, decode_token):
|
|
106
|
-
# """User has admin and staff roles"""
|
|
107
|
-
# decode_token.return_value["realm_access"]["roles"] = ["admin", "staff"]
|
|
108
|
-
# self.middleware.process_request(self.request)
|
|
109
|
-
# self.assertTrue(self.request.user.is_staff)
|
|
110
|
-
# self.assertTrue(self.request.user.is_superuser)
|
|
111
|
-
|
|
112
108
|
def test_profile_info(self, *_):
|
|
113
109
|
"""User has profile info"""
|
|
114
110
|
|
|
@@ -140,3 +136,47 @@ class OIDCHandlerTest(TestCase):
|
|
|
140
136
|
User.objects.create(kc_id="1234", first_name="", last_name="", username="")
|
|
141
137
|
self.middleware.process_request(self.request)
|
|
142
138
|
self.assertEqual(self.request.user.username, "on_update")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class RolesTest(TestCase):
|
|
142
|
+
def setUp(self) -> None:
|
|
143
|
+
self.user = User.objects.create(username="user")
|
|
144
|
+
settings.OIDC_ADMIN_ROLES = [
|
|
145
|
+
ROLE(
|
|
146
|
+
name="admin",
|
|
147
|
+
is_superuser=True,
|
|
148
|
+
),
|
|
149
|
+
ROLE(
|
|
150
|
+
name="staff",
|
|
151
|
+
groups=["staff group"],
|
|
152
|
+
permissions=["add_user", "change_user", "delete_user"],
|
|
153
|
+
),
|
|
154
|
+
]
|
|
155
|
+
self.group = Group.objects.create(name="staff group")
|
|
156
|
+
self.permission = Permission.objects.get(name="Can add user")
|
|
157
|
+
self.access_token = {"resource_access": {settings.OIDC_ADMIN_CLIENT_ID: {"roles": ["staff"]}}}
|
|
158
|
+
|
|
159
|
+
def test_staff_role(self):
|
|
160
|
+
self.access_token["resource_access"][settings.OIDC_ADMIN_CLIENT_ID]["roles"] = ["staff"]
|
|
161
|
+
role_handler.apply(self.user, self.access_token)
|
|
162
|
+
self.assertTrue(self.user.groups.filter(name="staff group").exists())
|
|
163
|
+
self.assertTrue(self.user.user_permissions.filter(codename="add_user").exists())
|
|
164
|
+
self.assertFalse(self.user.is_superuser)
|
|
165
|
+
|
|
166
|
+
def test_admin_role(self):
|
|
167
|
+
self.access_token["resource_access"][settings.OIDC_ADMIN_CLIENT_ID]["roles"] = ["admin"]
|
|
168
|
+
role_handler.apply(self.user, self.access_token)
|
|
169
|
+
self.assertTrue(self.user.is_superuser)
|
|
170
|
+
|
|
171
|
+
def test_apply_staff_then_admin_role(self):
|
|
172
|
+
self.access_token["resource_access"][settings.OIDC_ADMIN_CLIENT_ID]["roles"] = ["staff"]
|
|
173
|
+
role_handler.apply(self.user, self.access_token)
|
|
174
|
+
self.assertFalse(self.user.is_superuser)
|
|
175
|
+
self.assertTrue(self.user.groups.filter(name="staff group").exists())
|
|
176
|
+
self.assertTrue(self.user.user_permissions.filter(codename="add_user").exists())
|
|
177
|
+
|
|
178
|
+
self.access_token["resource_access"][settings.OIDC_ADMIN_CLIENT_ID]["roles"] = ["admin"]
|
|
179
|
+
role_handler.apply(self.user, self.access_token)
|
|
180
|
+
self.assertTrue(self.user.is_superuser)
|
|
181
|
+
self.assertTrue(self.user.groups.filter(name="staff group").exists())
|
|
182
|
+
self.assertTrue(self.user.user_permissions.filter(codename="add_user").exists())
|
|
File without changes
|
|
File without changes
|