This commit is contained in:
nvrl committed 2026-04-27 03:36:35 +02:00
1 parent 2b2b4cf48f
commit 22fe0674dd
22 files changed
+620 -286

No files matched your search

View File
Whitespace-only changes.
+26
View File
@@ -0,0 +1,26 @@
"""Auth helpers for narrowing request.user to the concrete User type.
DRF's `request.user` is typed as `User | AnonymousUser`. After a view enforces
`IsAuthenticated`, the union is misleading: every code path past the permission
check has the concrete user. These helpers express that narrowing for mypy and
keep view bodies readable.
"""
from __future__ import annotations
from rest_framework.exceptions import NotAuthenticated
from rest_framework.request import Request
from api.models import User
def current_user(request: Request) -> User:
"""Return the authenticated User for `request` or raise 401.
Use after `IsAuthenticated` permission. The check is defensive — the
permission class already rejected anonymous calls.
"""
user = request.user
if not isinstance(user, User):
raise NotAuthenticated
return user
+74
View File
@@ -0,0 +1,74 @@
"""httpOnly-cookie JWT auth.
Reads `access_token` from a cookie set by the login/refresh endpoints. Falls
back to the Authorization header for backwards compatibility during rollout —
strip the fallback once all clients use cookies.
"""
from __future__ import annotations
from django.conf import settings
from drf_spectacular.extensions import OpenApiAuthenticationExtension
from rest_framework_simplejwt.authentication import JWTAuthentication
class CookieJWTAuthentication(JWTAuthentication):
def authenticate(self, request):
raw_token = request.COOKIES.get(settings.JWT_AUTH_COOKIE_ACCESS)
if raw_token is None:
return super().authenticate(request)
validated_token = self.get_validated_token(raw_token)
return self.get_user(validated_token), validated_token
class CookieJWTScheme(OpenApiAuthenticationExtension):
"""Make drf-spectacular treat CookieJWTAuthentication like the SimpleJWT scheme."""
target_class = "api.utils.cookie_auth.CookieJWTAuthentication"
name = "cookieAuth"
def get_security_definition(self, auto_schema): # type: ignore[no-untyped-def]
return {
"type": "apiKey",
"in": "cookie",
"name": settings.JWT_AUTH_COOKIE_ACCESS,
}
def set_jwt_cookies(response, access: str | None, refresh: str | None) -> None:
"""Attach access/refresh tokens to the response as httpOnly cookies."""
from datetime import timedelta
secure = settings.JWT_AUTH_COOKIE_SECURE
samesite = settings.JWT_AUTH_COOKIE_SAMESITE
path = settings.JWT_AUTH_COOKIE_PATH
if access:
access_lifetime: timedelta = settings.SIMPLE_JWT["ACCESS_TOKEN_LIFETIME"]
response.set_cookie(
settings.JWT_AUTH_COOKIE_ACCESS,
access,
max_age=int(access_lifetime.total_seconds()),
httponly=True,
secure=secure,
samesite=samesite,
path=path,
)
if refresh:
refresh_lifetime: timedelta = settings.SIMPLE_JWT["REFRESH_TOKEN_LIFETIME"]
response.set_cookie(
settings.JWT_AUTH_COOKIE_REFRESH,
refresh,
max_age=int(refresh_lifetime.total_seconds()),
httponly=True,
secure=secure,
samesite=samesite,
path=path,
)
def clear_jwt_cookies(response) -> None:
response.delete_cookie(settings.JWT_AUTH_COOKIE_ACCESS, path=settings.JWT_AUTH_COOKIE_PATH)
response.delete_cookie(settings.JWT_AUTH_COOKIE_REFRESH, path=settings.JWT_AUTH_COOKIE_PATH)