mirror of
https://github.com/ajurna/cbwebreader.git
synced 2026-11-02 18:46:53 +00:00
- Introduced `CookieJWTAuthentication` for authentication via HttpOnly cookies, enabling browser-initiated requests without `Authorization` headers. - Added authentication views (`CookieTokenObtainPairView`, `CookieTokenRefreshView`, `CookieTokenBlacklistView`) to replace default SimpleJWT views. - Updated Vuex store and frontend logic to handle sessions using HttpOnly cookies instead of localStorage. - Enhanced backend and frontend configurations (Django settings, router, Vuex, and API service) to fully support cookie-based authentication. - Updated authentication tests to ensure correct cookie handling and authentication flows.
73 lines
3.0 KiB
Python
73 lines
3.0 KiB
Python
"""Token views that store JWTs in HttpOnly cookies (see comic/auth.py)."""
|
|
|
|
from rest_framework import status
|
|
from rest_framework.exceptions import AuthenticationFailed
|
|
from rest_framework.request import Request
|
|
from rest_framework.response import Response
|
|
from rest_framework_simplejwt.exceptions import TokenError
|
|
from rest_framework_simplejwt.serializers import TokenBlacklistSerializer, TokenRefreshSerializer
|
|
from rest_framework_simplejwt.views import TokenObtainPairView
|
|
|
|
from comic.auth import clear_auth_cookies, refresh_cookie_name, set_auth_cookies
|
|
|
|
|
|
def _refresh_value(request: Request) -> str | None:
|
|
"""The refresh token from the request body, or from the refresh cookie."""
|
|
value = request.data.get('refresh') if isinstance(request.data, dict) else None
|
|
return value or request.COOKIES.get(refresh_cookie_name())
|
|
|
|
|
|
class CookieTokenObtainPairView(TokenObtainPairView):
|
|
"""Log in: issue tokens and store them in HttpOnly cookies."""
|
|
|
|
def post(self, request: Request, *args: object, **kwargs: object) -> Response:
|
|
response = super().post(request, *args, **kwargs)
|
|
if response.status_code == status.HTTP_200_OK and isinstance(response.data, dict):
|
|
set_auth_cookies(response, response.data)
|
|
# The tokens live in HttpOnly cookies, so they are not returned to JavaScript.
|
|
response.data = {}
|
|
return response
|
|
|
|
|
|
class CookieTokenRefreshView(TokenObtainPairView):
|
|
"""Refresh using the refresh cookie and update both cookies."""
|
|
|
|
serializer_class = TokenRefreshSerializer
|
|
|
|
def post(self, request: Request, *args: object, **kwargs: object) -> Response:
|
|
refresh = _refresh_value(request)
|
|
if not refresh:
|
|
return Response(status=status.HTTP_401_UNAUTHORIZED, data={'errors': 'No refresh token.'})
|
|
|
|
serializer = self.get_serializer(data={'refresh': refresh})
|
|
try:
|
|
serializer.is_valid(raise_exception=True)
|
|
except TokenError as exc:
|
|
raise AuthenticationFailed(exc.args[0]) from exc
|
|
|
|
response = Response(status=status.HTTP_200_OK)
|
|
set_auth_cookies(response, serializer.validated_data)
|
|
# Keep the cookies out of the body so JavaScript never sees the tokens.
|
|
return response
|
|
|
|
|
|
class CookieTokenBlacklistView(TokenObtainPairView):
|
|
"""Log out: blacklist the refresh cookie and clear both cookies."""
|
|
|
|
serializer_class = TokenBlacklistSerializer
|
|
|
|
def post(self, request: Request, *args: object, **kwargs: object) -> Response:
|
|
refresh = _refresh_value(request)
|
|
if not refresh:
|
|
return Response(status=status.HTTP_401_UNAUTHORIZED, data={'errors': 'No refresh token.'})
|
|
|
|
serializer = self.get_serializer(data={'refresh': refresh})
|
|
try:
|
|
serializer.is_valid(raise_exception=True)
|
|
except TokenError as exc:
|
|
raise AuthenticationFailed(exc.args[0]) from exc
|
|
|
|
response = Response(status=status.HTTP_204_NO_CONTENT)
|
|
clear_auth_cookies(response)
|
|
return response
|