mirror of
https://git.v0id.ovh/n3wt-innov/n3wt-school.git
synced 2026-04-04 04:01:27 +00:00
feat: Securisation du Backend
This commit is contained in:
@ -7,5 +7,22 @@ class ContentSecurityPolicyMiddleware:
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
response['Content-Security-Policy'] = f"frame-ancestors 'self' {settings.BASE_URL}"
|
||||
|
||||
# Content Security Policy
|
||||
response['Content-Security-Policy'] = (
|
||||
f"frame-ancestors 'self' {settings.BASE_URL}; "
|
||||
"default-src 'self'; "
|
||||
"script-src 'self'; "
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"img-src 'self' data: blob:; "
|
||||
"font-src 'self'; "
|
||||
"connect-src 'self'; "
|
||||
"object-src 'none'; "
|
||||
"base-uri 'self';"
|
||||
)
|
||||
# En-têtes de sécurité complémentaires
|
||||
response['X-Content-Type-Options'] = 'nosniff'
|
||||
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||
response['Permissions-Policy'] = 'geolocation=(), microphone=(), camera=()'
|
||||
|
||||
return response
|
||||
|
||||
@ -33,9 +33,9 @@ LOGIN_REDIRECT_URL = '/Subscriptions/registerForms'
|
||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = os.getenv('DJANGO_DEBUG', True)
|
||||
DEBUG = os.getenv('DJANGO_DEBUG', 'False').lower() in ('true', '1')
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
|
||||
|
||||
# Application definition
|
||||
|
||||
@ -62,6 +62,7 @@ INSTALLED_APPS = [
|
||||
'N3wtSchool',
|
||||
'drf_yasg',
|
||||
'rest_framework_simplejwt',
|
||||
'rest_framework_simplejwt.token_blacklist',
|
||||
'channels',
|
||||
]
|
||||
|
||||
@ -124,9 +125,15 @@ LOGGING = {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose", # Utilisation du formateur
|
||||
},
|
||||
"file": {
|
||||
"level": "WARNING",
|
||||
"class": "logging.FileHandler",
|
||||
"filename": os.path.join(BASE_DIR, "django.log"),
|
||||
"formatter": "verbose",
|
||||
},
|
||||
},
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"handlers": ["console", "file"],
|
||||
"level": os.getenv("ROOT_LOG_LEVEL", "INFO"),
|
||||
},
|
||||
"loggers": {
|
||||
@ -171,9 +178,31 @@ LOGGING = {
|
||||
"level": os.getenv("GESTION_ENSEIGNANTS_LOG_LEVEL", "INFO"),
|
||||
"propagate": False,
|
||||
},
|
||||
# Logs JWT : montre exactement pourquoi un token est rejeté (expiré,
|
||||
# signature invalide, claim manquant, etc.)
|
||||
"rest_framework_simplejwt": {
|
||||
"handlers": ["console"],
|
||||
"level": os.getenv("JWT_LOG_LEVEL", "DEBUG"),
|
||||
"propagate": False,
|
||||
},
|
||||
"rest_framework": {
|
||||
"handlers": ["console"],
|
||||
"level": os.getenv("DRF_LOG_LEVEL", "WARNING"),
|
||||
"propagate": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Hashage des mots de passe - configuration explicite pour garantir un stockage sécurisé
|
||||
# Les mots de passe ne sont JAMAIS stockés en clair
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
|
||||
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
|
||||
'django.contrib.auth.hashers.Argon2PasswordHasher',
|
||||
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
|
||||
'django.contrib.auth.hashers.ScryptPasswordHasher',
|
||||
]
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||
|
||||
@ -184,12 +213,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
'OPTIONS': {
|
||||
'min_length': 6,
|
||||
'min_length': 10,
|
||||
}
|
||||
},
|
||||
#{
|
||||
# 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
#},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
@ -276,6 +305,16 @@ CSRF_COOKIE_SECURE = os.getenv('CSRF_COOKIE_SECURE', 'false').lower() == 'true'
|
||||
CSRF_COOKIE_NAME = 'csrftoken'
|
||||
CSRF_COOKIE_DOMAIN = os.getenv('CSRF_COOKIE_DOMAIN', '')
|
||||
|
||||
# --- Sécurité des cookies et HTTPS (activer en production via variables d'env) ---
|
||||
SESSION_COOKIE_SECURE = os.getenv('SESSION_COOKIE_SECURE', 'false').lower() == 'true'
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
SECURE_HSTS_SECONDS = int(os.getenv('SECURE_HSTS_SECONDS', '0'))
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = os.getenv('SECURE_HSTS_INCLUDE_SUBDOMAINS', 'false').lower() == 'true'
|
||||
SECURE_HSTS_PRELOAD = os.getenv('SECURE_HSTS_PRELOAD', 'false').lower() == 'true'
|
||||
SECURE_SSL_REDIRECT = os.getenv('SECURE_SSL_REDIRECT', 'false').lower() == 'true'
|
||||
|
||||
USE_TZ = True
|
||||
TZ_APPLI = 'Europe/Paris'
|
||||
|
||||
@ -312,10 +351,22 @@ NB_MAX_PAGE = 100
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_PAGINATION_CLASS': 'Subscriptions.pagination.CustomSubscriptionPagination',
|
||||
'PAGE_SIZE': NB_RESULT_SUBSCRIPTIONS_PER_PAGE,
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
'Auth.backends.LoggingJWTAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
),
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
'rest_framework.throttling.AnonRateThrottle',
|
||||
'rest_framework.throttling.UserRateThrottle',
|
||||
],
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'anon': '100/min',
|
||||
'user': '1000/min',
|
||||
'login': '10/min',
|
||||
},
|
||||
}
|
||||
|
||||
CELERY_BROKER_URL = 'redis://redis:6379/0'
|
||||
@ -333,11 +384,28 @@ REDIS_PORT = 6379
|
||||
REDIS_DB = 0
|
||||
REDIS_PASSWORD = None
|
||||
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'QWQ8bYlCz1NpQ9G0vR5kxMnvWszfH2y3')
|
||||
_secret_key_default = '<SECRET_KEY>'
|
||||
_secret_key = os.getenv('SECRET_KEY', _secret_key_default)
|
||||
if _secret_key == _secret_key_default and not DEBUG:
|
||||
raise ValueError(
|
||||
"La variable d'environnement SECRET_KEY doit être définie en production. "
|
||||
"Utilisez une clé aléatoire forte (ex: python -c 'import secrets; print(secrets.token_hex(50))')."
|
||||
)
|
||||
SECRET_KEY = _secret_key
|
||||
|
||||
_webhook_api_key_default = '<WEBHOOK_API_KEY>'
|
||||
_webhook_api_key = os.getenv('WEBHOOK_API_KEY', _webhook_api_key_default)
|
||||
if _webhook_api_key == _webhook_api_key_default and not DEBUG:
|
||||
raise ValueError(
|
||||
"La variable d'environnement WEBHOOK_API_KEY doit être définie en production. "
|
||||
"Utilisez une clé aléatoire forte (ex: python -c 'import secrets; print(secrets.token_hex(50))')."
|
||||
)
|
||||
WEBHOOK_API_KEY = _webhook_api_key
|
||||
|
||||
SIMPLE_JWT = {
|
||||
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
|
||||
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
|
||||
'ROTATE_REFRESH_TOKENS': False,
|
||||
'ROTATE_REFRESH_TOKENS': True,
|
||||
'BLACKLIST_AFTER_ROTATION': True,
|
||||
'ALGORITHM': 'HS256',
|
||||
'SIGNING_KEY': SECRET_KEY,
|
||||
@ -346,7 +414,7 @@ SIMPLE_JWT = {
|
||||
'USER_ID_FIELD': 'id',
|
||||
'USER_ID_CLAIM': 'user_id',
|
||||
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
|
||||
'TOKEN_TYPE_CLAIM': 'token_type',
|
||||
'TOKEN_TYPE_CLAIM': 'type',
|
||||
}
|
||||
|
||||
# Django Channels Configuration
|
||||
|
||||
Reference in New Issue
Block a user