karrio-server 2026.1.3__py3-none-any.whl → 2026.1.4__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.
- karrio/server/VERSION +1 -1
- karrio/server/settings/base.py +9 -0
- karrio/server/urls/jwt.py +177 -13
- {karrio_server-2026.1.3.dist-info → karrio_server-2026.1.4.dist-info}/METADATA +1 -1
- {karrio_server-2026.1.3.dist-info → karrio_server-2026.1.4.dist-info}/RECORD +8 -8
- {karrio_server-2026.1.3.dist-info → karrio_server-2026.1.4.dist-info}/WHEEL +0 -0
- {karrio_server-2026.1.3.dist-info → karrio_server-2026.1.4.dist-info}/entry_points.txt +0 -0
- {karrio_server-2026.1.3.dist-info → karrio_server-2026.1.4.dist-info}/top_level.txt +0 -0
karrio/server/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2026.1.
|
|
1
|
+
2026.1.4
|
karrio/server/settings/base.py
CHANGED
|
@@ -513,6 +513,15 @@ SIMPLE_JWT = {
|
|
|
513
513
|
"SLIDING_TOKEN_REFRESH_LIFETIME": timedelta(days=1),
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
+
# JWT Cookie settings for HTTP-only cookie authentication
|
|
517
|
+
JWT_AUTH_COOKIE = config("JWT_AUTH_COOKIE", default="karrio_access_token")
|
|
518
|
+
JWT_REFRESH_COOKIE = config("JWT_REFRESH_COOKIE", default="karrio_refresh_token")
|
|
519
|
+
JWT_AUTH_COOKIE_SECURE = config(
|
|
520
|
+
"JWT_AUTH_COOKIE_SECURE", default=USE_HTTPS, cast=bool
|
|
521
|
+
)
|
|
522
|
+
JWT_AUTH_COOKIE_SAMESITE = config("JWT_AUTH_COOKIE_SAMESITE", default="Lax")
|
|
523
|
+
JWT_AUTH_COOKIE_PATH = config("JWT_AUTH_COOKIE_PATH", default="/")
|
|
524
|
+
|
|
516
525
|
# OAuth2 config
|
|
517
526
|
OIDC_RSA_PRIVATE_KEY = config("OIDC_RSA_PRIVATE_KEY", default="").replace("\\n", "\n")
|
|
518
527
|
OAUTH2_PROVIDER_APPLICATION_MODEL = "oauth2_provider.Application"
|
karrio/server/urls/jwt.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
from django.urls import path
|
|
2
2
|
from django.contrib.auth import get_user_model
|
|
3
3
|
from django.utils.translation import gettext_lazy as _
|
|
4
|
-
from
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from rest_framework import serializers, exceptions, status
|
|
6
|
+
from rest_framework.response import Response
|
|
7
|
+
from rest_framework.permissions import AllowAny
|
|
5
8
|
from rest_framework_simplejwt import views as jwt_views, serializers as jwt
|
|
6
9
|
from two_factor.utils import default_device
|
|
7
10
|
|
|
@@ -11,6 +14,99 @@ ENDPOINT_ID = "&&" # This endpoint id is used to make operation ids unique make
|
|
|
11
14
|
User = get_user_model()
|
|
12
15
|
|
|
13
16
|
|
|
17
|
+
# --- Cookie helpers (shared by all JWT views) ---
|
|
18
|
+
|
|
19
|
+
def get_cookie_config(include_max_age=True):
|
|
20
|
+
"""Build cookie configuration from Django settings."""
|
|
21
|
+
config = dict(
|
|
22
|
+
access_cookie_name=getattr(settings, "JWT_AUTH_COOKIE", "karrio_access_token"),
|
|
23
|
+
refresh_cookie_name=getattr(settings, "JWT_REFRESH_COOKIE", "karrio_refresh_token"),
|
|
24
|
+
secure=getattr(settings, "JWT_AUTH_COOKIE_SECURE", getattr(settings, "USE_HTTPS", False)),
|
|
25
|
+
samesite=getattr(settings, "JWT_AUTH_COOKIE_SAMESITE", "Lax"),
|
|
26
|
+
path=getattr(settings, "JWT_AUTH_COOKIE_PATH", "/"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
if include_max_age:
|
|
30
|
+
jwt_config = getattr(settings, "SIMPLE_JWT", {})
|
|
31
|
+
access_lifetime = jwt_config.get("ACCESS_TOKEN_LIFETIME")
|
|
32
|
+
refresh_lifetime = jwt_config.get("REFRESH_TOKEN_LIFETIME")
|
|
33
|
+
config.update(
|
|
34
|
+
access_max_age=int(access_lifetime.total_seconds()) if access_lifetime else 1800,
|
|
35
|
+
refresh_max_age=int(refresh_lifetime.total_seconds()) if refresh_lifetime else 259200,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return config
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def set_auth_cookies(response, access_token, refresh_token):
|
|
42
|
+
"""Set HTTP-only cookies for access and refresh tokens on the response."""
|
|
43
|
+
config = get_cookie_config()
|
|
44
|
+
|
|
45
|
+
response.set_cookie(
|
|
46
|
+
config["access_cookie_name"],
|
|
47
|
+
access_token,
|
|
48
|
+
max_age=config["access_max_age"],
|
|
49
|
+
httponly=True,
|
|
50
|
+
secure=config["secure"],
|
|
51
|
+
samesite=config["samesite"],
|
|
52
|
+
path=config["path"],
|
|
53
|
+
)
|
|
54
|
+
response.set_cookie(
|
|
55
|
+
config["refresh_cookie_name"],
|
|
56
|
+
refresh_token,
|
|
57
|
+
max_age=config["refresh_max_age"],
|
|
58
|
+
httponly=True,
|
|
59
|
+
secure=config["secure"],
|
|
60
|
+
samesite=config["samesite"],
|
|
61
|
+
path=config["path"],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def clear_auth_cookies(response):
|
|
66
|
+
"""Clear HTTP-only auth cookies by expiring them immediately."""
|
|
67
|
+
config = get_cookie_config(include_max_age=False)
|
|
68
|
+
|
|
69
|
+
for cookie_name in [config["access_cookie_name"], config["refresh_cookie_name"]]:
|
|
70
|
+
response.set_cookie(
|
|
71
|
+
cookie_name,
|
|
72
|
+
"",
|
|
73
|
+
max_age=0,
|
|
74
|
+
httponly=True,
|
|
75
|
+
secure=config["secure"],
|
|
76
|
+
samesite=config["samesite"],
|
|
77
|
+
path=config["path"],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_refresh_token(request):
|
|
82
|
+
"""Get refresh token from cookie, falling back to request body."""
|
|
83
|
+
cookie_name = getattr(settings, "JWT_REFRESH_COOKIE", "karrio_refresh_token")
|
|
84
|
+
refresh_token = (
|
|
85
|
+
request.COOKIES.get(cookie_name)
|
|
86
|
+
or request.data.get("refresh")
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if not refresh_token:
|
|
90
|
+
raise exceptions.ValidationError(
|
|
91
|
+
{"refresh": _("Refresh token is required.")}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return refresh_token
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _build_token_response(access_token, refresh_token):
|
|
98
|
+
"""Build a 201 token pair response with no-cache headers."""
|
|
99
|
+
response = Response(
|
|
100
|
+
{"access": access_token, "refresh": refresh_token},
|
|
101
|
+
status=status.HTTP_201_CREATED,
|
|
102
|
+
)
|
|
103
|
+
response["Cache-Control"] = "no-store"
|
|
104
|
+
response["CDN-Cache-Control"] = "no-store"
|
|
105
|
+
return response
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# --- Serializers ---
|
|
109
|
+
|
|
14
110
|
class AccessToken(serializers.Serializer):
|
|
15
111
|
access = serializers.CharField()
|
|
16
112
|
|
|
@@ -49,7 +145,13 @@ class TokenObtainPairSerializer(jwt.TokenObtainPairSerializer):
|
|
|
49
145
|
|
|
50
146
|
class TokenRefreshSerializer(jwt.TokenRefreshSerializer):
|
|
51
147
|
def validate(self, attrs: dict):
|
|
52
|
-
|
|
148
|
+
refresh_token = attrs.get("refresh")
|
|
149
|
+
if not refresh_token:
|
|
150
|
+
raise exceptions.ValidationError(
|
|
151
|
+
{"refresh": _("Refresh token is required.")}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
refresh = jwt.RefreshToken(refresh_token)
|
|
53
155
|
|
|
54
156
|
if not refresh["is_verified"]:
|
|
55
157
|
raise exceptions.AuthenticationFailed(
|
|
@@ -86,7 +188,13 @@ class VerifiedTokenObtainPairSerializer(jwt.TokenRefreshSerializer):
|
|
|
86
188
|
)
|
|
87
189
|
|
|
88
190
|
def validate(self, attrs):
|
|
89
|
-
|
|
191
|
+
refresh_token = attrs.get("refresh")
|
|
192
|
+
if not refresh_token:
|
|
193
|
+
raise exceptions.ValidationError(
|
|
194
|
+
{"refresh": _("Refresh token is required.")}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
refresh = self.token_class(refresh_token)
|
|
90
198
|
user = User.objects.get(id=refresh["user_id"])
|
|
91
199
|
refresh["is_verified"] = self._validate_otp(attrs["otp_token"], user)
|
|
92
200
|
|
|
@@ -126,6 +234,8 @@ class VerifiedTokenObtainPairSerializer(jwt.TokenRefreshSerializer):
|
|
|
126
234
|
)
|
|
127
235
|
|
|
128
236
|
|
|
237
|
+
# --- Views ---
|
|
238
|
+
|
|
129
239
|
class TokenObtainPair(jwt_views.TokenObtainPairView):
|
|
130
240
|
serializer_class = TokenObtainPairSerializer
|
|
131
241
|
|
|
@@ -134,13 +244,17 @@ class TokenObtainPair(jwt_views.TokenObtainPairView):
|
|
|
134
244
|
tags=["Auth"],
|
|
135
245
|
operation_id=f"{ENDPOINT_ID}authenticate",
|
|
136
246
|
summary="Obtain auth token pair",
|
|
137
|
-
description="Authenticate the user and return a token pair",
|
|
247
|
+
description="Authenticate the user and return a token pair. Tokens are stored in HTTP-only cookies.",
|
|
138
248
|
responses={201: TokenPair()},
|
|
139
249
|
)
|
|
140
250
|
def post(self, *args, **kwargs):
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
251
|
+
serializer = self.get_serializer(data=self.request.data)
|
|
252
|
+
serializer.is_valid(raise_exception=True)
|
|
253
|
+
|
|
254
|
+
data = serializer.validated_data
|
|
255
|
+
response = _build_token_response(data["access"], data["refresh"])
|
|
256
|
+
set_auth_cookies(response, data["access"], data["refresh"])
|
|
257
|
+
|
|
144
258
|
return response
|
|
145
259
|
|
|
146
260
|
|
|
@@ -152,13 +266,23 @@ class TokenRefresh(jwt_views.TokenRefreshView):
|
|
|
152
266
|
tags=["Auth"],
|
|
153
267
|
operation_id=f"{ENDPOINT_ID}refresh_token",
|
|
154
268
|
summary="Refresh auth token",
|
|
155
|
-
description="
|
|
269
|
+
description="Refresh the authentication token. Tokens are stored in HTTP-only cookies.",
|
|
156
270
|
responses={201: TokenPair()},
|
|
157
271
|
)
|
|
158
272
|
def post(self, *args, **kwargs):
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
273
|
+
refresh_token = get_refresh_token(self.request)
|
|
274
|
+
|
|
275
|
+
serializer = self.get_serializer(data={"refresh": refresh_token})
|
|
276
|
+
serializer.is_valid(raise_exception=True)
|
|
277
|
+
|
|
278
|
+
data = serializer.validated_data
|
|
279
|
+
access = data["access"]
|
|
280
|
+
refresh = data.get("refresh")
|
|
281
|
+
|
|
282
|
+
response = _build_token_response(access, refresh or refresh_token)
|
|
283
|
+
if refresh is not None:
|
|
284
|
+
set_auth_cookies(response, access, refresh)
|
|
285
|
+
|
|
162
286
|
return response
|
|
163
287
|
|
|
164
288
|
|
|
@@ -187,13 +311,52 @@ class VerifiedTokenPair(jwt_views.TokenVerifyView):
|
|
|
187
311
|
tags=["Auth"],
|
|
188
312
|
operation_id=f"{ENDPOINT_ID}get_verified_token",
|
|
189
313
|
summary="Get verified JWT token",
|
|
190
|
-
description="Get a verified JWT token pair by submitting a Two-Factor authentication code.",
|
|
314
|
+
description="Get a verified JWT token pair by submitting a Two-Factor authentication code. Tokens are stored in HTTP-only cookies.",
|
|
191
315
|
responses={201: TokenPair()},
|
|
192
316
|
)
|
|
193
317
|
def post(self, *args, **kwargs):
|
|
194
|
-
|
|
318
|
+
refresh_token = get_refresh_token(self.request)
|
|
319
|
+
|
|
320
|
+
serializer = self.get_serializer(data={
|
|
321
|
+
"refresh": refresh_token,
|
|
322
|
+
"otp_token": self.request.data.get("otp_token"),
|
|
323
|
+
})
|
|
324
|
+
serializer.is_valid(raise_exception=True)
|
|
325
|
+
|
|
326
|
+
data = serializer.validated_data
|
|
327
|
+
access = data["access"]
|
|
328
|
+
refresh = data.get("refresh")
|
|
329
|
+
|
|
330
|
+
response = _build_token_response(access, refresh or refresh_token)
|
|
331
|
+
if refresh is not None:
|
|
332
|
+
set_auth_cookies(response, access, refresh)
|
|
333
|
+
|
|
334
|
+
return response
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class LogoutView(jwt_views.TokenVerifyView):
|
|
338
|
+
"""Logout view that clears HTTP-only auth cookies."""
|
|
339
|
+
permission_classes = [AllowAny]
|
|
340
|
+
|
|
341
|
+
@openapi.extend_schema(
|
|
342
|
+
auth=[],
|
|
343
|
+
tags=["Auth"],
|
|
344
|
+
operation_id=f"{ENDPOINT_ID}logout",
|
|
345
|
+
summary="Logout",
|
|
346
|
+
description="Clear authentication cookies and logout the user. Accessible without authentication.",
|
|
347
|
+
responses={200: openapi.OpenApiTypes.OBJECT},
|
|
348
|
+
)
|
|
349
|
+
def post(self, *args, **kwargs):
|
|
350
|
+
response = Response(
|
|
351
|
+
{"detail": "Successfully logged out."},
|
|
352
|
+
status=status.HTTP_200_OK,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
clear_auth_cookies(response)
|
|
356
|
+
|
|
195
357
|
response["Cache-Control"] = "no-store"
|
|
196
358
|
response["CDN-Cache-Control"] = "no-store"
|
|
359
|
+
|
|
197
360
|
return response
|
|
198
361
|
|
|
199
362
|
|
|
@@ -202,4 +365,5 @@ urlpatterns = [
|
|
|
202
365
|
path("api/token/refresh", TokenRefresh.as_view(), name="jwt-refresh"),
|
|
203
366
|
path("api/token/verify", TokenVerify.as_view(), name="jwt-verify"),
|
|
204
367
|
path("api/token/verified", VerifiedTokenPair.as_view(), name="verified-jwt-pair"),
|
|
368
|
+
path("api/logout", LogoutView.as_view(), name="jwt-logout"),
|
|
205
369
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
karrio/server/VERSION,sha256=
|
|
1
|
+
karrio/server/VERSION,sha256=QphdNAnHQVjVDgkk5LVXj3qpqRHRvOVtCt2bs_goMQ4,8
|
|
2
2
|
karrio/server/__init__.py,sha256=iOEMwnlORWezdO8-2vxBIPSR37D7JGjluZ8f55vzxls,81
|
|
3
3
|
karrio/server/__main__.py,sha256=hy2-Zb2wSVe_Pu6zWZ-BhiM4rBaGZ3D16HSuhudygqg,632
|
|
4
4
|
karrio/server/asgi.py,sha256=LsZYMWo8U9zURVPdHnvUsziOhMjdCdQoD2-gMJbS2U0,462
|
|
@@ -7,7 +7,7 @@ karrio/server/wsgi.py,sha256=SpWqkEYlMsj89_znZ8p8IjH3EgTVRWRq_9eS8t64dMw,403
|
|
|
7
7
|
karrio/server/lib/otel_huey.py,sha256=6MP6vX6b6x6RPF2K1m8B8L8S9GK1Q3vANmLidsxh65k,5428
|
|
8
8
|
karrio/server/settings/__init__.py,sha256=iw-NBcReOnDYpnvSEBdYDfV7jC0040jYdupnmSdElec,866
|
|
9
9
|
karrio/server/settings/apm.py,sha256=Zt8dP2y8DjPa_iNyv3ZywXTbHlULWc06SLPJhlTTA-A,19983
|
|
10
|
-
karrio/server/settings/base.py,sha256=
|
|
10
|
+
karrio/server/settings/base.py,sha256=8macll9KmzuTd2jfqa5Pch_sTnDNh-MGezrNOXsmQbE,23598
|
|
11
11
|
karrio/server/settings/cache.py,sha256=UYBd6pG-5pWSUlu4d3PuWGWYCiiDBL2oc1hYzq131zY,4045
|
|
12
12
|
karrio/server/settings/constance.py,sha256=84Ldmwr_o_5WBy5awf-k5AWpYmpMt2dzO3xh6MoqEXU,8013
|
|
13
13
|
karrio/server/settings/debug.py,sha256=fFyK2XX47UGeK0eRNSV-9ZLaComay5QvJW0692vaH98,527
|
|
@@ -73,10 +73,10 @@ karrio/server/static/karrio/js/karrio.js.map,sha256=9p642er0UNa_Knyf2OdHCEDbeV4G
|
|
|
73
73
|
karrio/server/templates/admin/base_site.html,sha256=kbcdvehXZ1EHaw07JL7fSZmjrnVMKsnydjbTKa7Ondg,466
|
|
74
74
|
karrio/server/templates/openapi/openapi.html,sha256=3ApCZ5pE6Wjv7CJllVbqD2WiDQuLy-BFS-IIHurXhBY,1133
|
|
75
75
|
karrio/server/urls/__init__.py,sha256=SHmmXmVn9Hbf7eZ2wcL10hG84yQPBrcVaCsvi5cdrrA,2062
|
|
76
|
-
karrio/server/urls/jwt.py,sha256=
|
|
76
|
+
karrio/server/urls/jwt.py,sha256=xZXgbdZAYPObeexgprokWKOeCXr8SqhTRlR5-P1Es5E,12198
|
|
77
77
|
karrio/server/urls/tokens.py,sha256=5slk9F0HPgV3dRnR9UipAl-R8837eiGqKJ31QnCdLnU,8215
|
|
78
|
-
karrio_server-2026.1.
|
|
79
|
-
karrio_server-2026.1.
|
|
80
|
-
karrio_server-2026.1.
|
|
81
|
-
karrio_server-2026.1.
|
|
82
|
-
karrio_server-2026.1.
|
|
78
|
+
karrio_server-2026.1.4.dist-info/METADATA,sha256=z6PS_ZXTUjxqx2nMUzqVnvM6a34hrU27mU1ks7Hu7vY,4303
|
|
79
|
+
karrio_server-2026.1.4.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
80
|
+
karrio_server-2026.1.4.dist-info/entry_points.txt,sha256=c2eftt6MpJjyp0OFv1OmO9nUYSDemt9fGq_RDdvpGLw,55
|
|
81
|
+
karrio_server-2026.1.4.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
|
|
82
|
+
karrio_server-2026.1.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|