feat: Securisation du Backend

This commit is contained in:
Luc SORIGNET
2026-02-27 10:45:36 +01:00
parent 2fef6d61a4
commit fa843097ba
55 changed files with 2898 additions and 910 deletions

View File

@ -17,10 +17,12 @@ from datetime import datetime, timedelta
import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
import json
import uuid
from . import validator
from .models import Profile, ProfileRole
from rest_framework.decorators import action, api_view
from rest_framework.decorators import action, api_view, permission_classes
from rest_framework.permissions import IsAuthenticated, AllowAny
from django.db.models import Q
from Auth.serializers import ProfileSerializer, ProfileRoleSerializer
@ -28,13 +30,28 @@ from Subscriptions.models import RegistrationForm, Guardian
import N3wtSchool.mailManager as mailer
import Subscriptions.util as util
import logging
from N3wtSchool import bdd, error, settings
from N3wtSchool import bdd, error
from rest_framework.throttling import AnonRateThrottle
from rest_framework_simplejwt.authentication import JWTAuthentication
logger = logging.getLogger("AuthViews")
class LoginRateThrottle(AnonRateThrottle):
"""Limite les tentatives de connexion à 10/min par IP.
Configurable via DEFAULT_THROTTLE_RATES['login'] dans settings.
"""
scope = 'login'
def get_rate(self):
try:
return super().get_rate()
except Exception:
# Fallback si le scope 'login' n'est pas configuré dans les settings
return '10/min'
@swagger_auto_schema(
method='get',
operation_description="Obtenir un token CSRF",
@ -43,11 +60,15 @@ logger = logging.getLogger("AuthViews")
}))}
)
@api_view(['GET'])
@permission_classes([AllowAny])
def csrf(request):
token = get_token(request)
return JsonResponse({'csrfToken': token})
class SessionView(APIView):
permission_classes = [AllowAny]
authentication_classes = [] # SessionView gère sa propre validation JWT
@swagger_auto_schema(
operation_description="Vérifier une session utilisateur",
manual_parameters=[openapi.Parameter('Authorization', openapi.IN_HEADER, type=openapi.TYPE_STRING, description='Bearer token')],
@ -70,6 +91,11 @@ class SessionView(APIView):
token = request.META.get('HTTP_AUTHORIZATION', '').split('Bearer ')[-1]
try:
decoded_token = jwt.decode(token, settings.SECRET_KEY, algorithms=['HS256'])
# Refuser les refresh tokens : seul le type 'access' est autorisé
# Accepter 'type' (format custom) ET 'token_type' (format SimpleJWT)
token_type_claim = decoded_token.get('type') or decoded_token.get('token_type')
if token_type_claim != 'access':
return JsonResponse({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED)
userid = decoded_token.get('user_id')
user = Profile.objects.get(id=userid)
roles = ProfileRole.objects.filter(profile=user).values('role_type', 'establishment__name')
@ -88,6 +114,8 @@ class SessionView(APIView):
return JsonResponse({"error": "Invalid token"}, status=status.HTTP_401_UNAUTHORIZED)
class ProfileView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_description="Obtenir la liste des profils",
responses={200: ProfileSerializer(many=True)}
@ -118,6 +146,8 @@ class ProfileView(APIView):
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class ProfileSimpleView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_description="Obtenir un profil par son ID",
responses={200: ProfileSerializer}
@ -152,8 +182,12 @@ class ProfileSimpleView(APIView):
def delete(self, request, id):
return bdd.delete_object(Profile, id)
@method_decorator(csrf_exempt, name='dispatch')
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class LoginView(APIView):
permission_classes = [AllowAny]
throttle_classes = [LoginRateThrottle]
@swagger_auto_schema(
operation_description="Connexion utilisateur",
request_body=openapi.Schema(
@ -240,12 +274,14 @@ def makeToken(user):
})
# Générer le JWT avec la bonne syntaxe datetime
# jti (JWT ID) est obligatoire : SimpleJWT le vérifie via AccessToken.verify_token_id()
access_payload = {
'user_id': user.id,
'email': user.email,
'roleIndexLoginDefault': user.roleIndexLoginDefault,
'roles': roles,
'type': 'access',
'jti': str(uuid.uuid4()),
'exp': datetime.utcnow() + settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
@ -255,16 +291,23 @@ def makeToken(user):
refresh_payload = {
'user_id': user.id,
'type': 'refresh',
'jti': str(uuid.uuid4()),
'exp': datetime.utcnow() + settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'],
'iat': datetime.utcnow(),
}
refresh_token = jwt.encode(refresh_payload, settings.SIMPLE_JWT['SIGNING_KEY'], algorithm=settings.SIMPLE_JWT['ALGORITHM'])
return access_token, refresh_token
except Exception as e:
logger.error(f"Erreur lors de la création du token: {str(e)}")
return None
logger.error(f"Erreur lors de la création du token: {str(e)}", exc_info=True)
# On lève l'exception pour que l'appelant (LoginView / RefreshJWTView)
# retourne une erreur HTTP 500 explicite plutôt que de crasher silencieusement
# sur le unpack d'un None.
raise
class RefreshJWTView(APIView):
permission_classes = [AllowAny]
throttle_classes = [LoginRateThrottle]
@swagger_auto_schema(
operation_description="Rafraîchir le token d'accès",
request_body=openapi.Schema(
@ -290,7 +333,6 @@ class RefreshJWTView(APIView):
))
}
)
@method_decorator(csrf_exempt, name='dispatch')
def post(self, request):
data = JSONParser().parse(request)
refresh_token = data.get("refresh")
@ -335,14 +377,16 @@ class RefreshJWTView(APIView):
return JsonResponse({'errorMessage': 'Refresh token expiré'}, status=400)
except InvalidTokenError as e:
logger.error(f"Token invalide: {str(e)}")
return JsonResponse({'errorMessage': f'Token invalide: {str(e)}'}, status=400)
return JsonResponse({'errorMessage': 'Token invalide'}, status=400)
except Exception as e:
logger.error(f"Erreur inattendue: {str(e)}")
return JsonResponse({'errorMessage': f'Erreur inattendue: {str(e)}'}, status=400)
logger.error(f"Erreur inattendue: {str(e)}", exc_info=True)
return JsonResponse({'errorMessage': 'Erreur interne du serveur'}, status=500)
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class SubscribeView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
operation_description="Inscription utilisateur",
manual_parameters=[
@ -430,6 +474,8 @@ class SubscribeView(APIView):
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class NewPasswordView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
operation_description="Demande de nouveau mot de passe",
request_body=openapi.Schema(
@ -479,6 +525,8 @@ class NewPasswordView(APIView):
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class ResetPasswordView(APIView):
permission_classes = [AllowAny]
@swagger_auto_schema(
operation_description="Réinitialisation du mot de passe",
request_body=openapi.Schema(
@ -525,7 +573,9 @@ class ResetPasswordView(APIView):
return JsonResponse({'message': retour, "errorMessage": retourErreur, "errorFields": errorFields}, safe=False)
class ProfileRoleView(APIView):
permission_classes = [IsAuthenticated]
pagination_class = CustomProfilesPagination
@swagger_auto_schema(
operation_description="Obtenir la liste des profile_roles",
responses={200: ProfileRoleSerializer(many=True)}
@ -596,6 +646,8 @@ class ProfileRoleView(APIView):
@method_decorator(csrf_protect, name='dispatch')
@method_decorator(ensure_csrf_cookie, name='dispatch')
class ProfileRoleSimpleView(APIView):
permission_classes = [IsAuthenticated]
@swagger_auto_schema(
operation_description="Obtenir un profile_role par son ID",
responses={200: ProfileRoleSerializer}