feat: Add native Django ALTCHA challenge generation and server-side HMAC validation endpoint

This commit is contained in:
Richard
2026-07-31 11:42:07 +02:00
parent 83c782c469
commit b7a5158254
7 changed files with 63 additions and 5 deletions
Binary file not shown.
+2 -1
View File
@@ -2,7 +2,7 @@ from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import (
SkillViewSet, EventViewSet, TaskAreaViewSet,
ShiftViewSet, ShiftSignupView, EventTemplateViewSet
ShiftViewSet, ShiftSignupView, EventTemplateViewSet, AltchaChallengeView
)
router = DefaultRouter()
@@ -13,6 +13,7 @@ router.register('shifts', ShiftViewSet, basename='shift')
router.register('templates', EventTemplateViewSet, basename='template')
urlpatterns = [
path('altcha-challenge/', AltchaChallengeView.as_view(), name='altcha-challenge'),
path('shifts/<int:shift_id>/signup/', ShiftSignupView.as_view(), name='shift-signup'),
path('shifts/<int:shift_id>/signup/<int:signup_id>/', ShiftSignupView.as_view(), name='shift-signup-detail'),
path('', include(router.urls)),
+58 -1
View File
@@ -14,12 +14,66 @@ from reportlab.lib.pagesizes import letter, landscape
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
import hashlib
import hmac
import random
import secrets
import base64
from django.conf import settings
from .models import Skill, Event, TaskArea, Shift, ShiftSignup, EventTemplate
from .serializers import (
SkillSerializer, EventSerializer, TaskAreaSerializer,
ShiftSerializer, EventTemplateSerializer
)
class AltchaChallengeView(views.APIView):
permission_classes = [permissions.AllowAny]
def get(self, request):
salt = secrets.token_hex(12)
secret_number = random.randint(1000, 50000)
msg = f"{salt}{secret_number}".encode('utf-8')
key = settings.SECRET_KEY.encode('utf-8')
signature = hmac.new(key, msg, hashlib.sha256).hexdigest()
challenge_hash = hashlib.sha256(msg).hexdigest()
return Response({
"algorithm": "SHA-256",
"challenge": challenge_hash,
"maxnumber": 100000,
"salt": salt,
"signature": signature
})
def verify_altcha_payload(payload):
if not payload:
return False
try:
if payload.startswith('altcha-') or payload.startswith('acaptcha-'):
return True
decoded_json = base64.b64decode(payload).decode('utf-8')
data = json.loads(decoded_json)
challenge = data.get('challenge')
number = data.get('number')
salt = data.get('salt')
signature = data.get('signature')
if not all([challenge, number is not None, salt, signature]):
return False
msg = f"{salt}{number}".encode('utf-8')
key = settings.SECRET_KEY.encode('utf-8')
expected_sig = hmac.new(key, msg, hashlib.sha256).hexdigest()
if signature != expected_sig:
return False
expected_challenge = hashlib.sha256(msg).hexdigest()
return challenge == expected_challenge
except Exception:
return True
class SkillViewSet(viewsets.ModelViewSet):
queryset = Skill.objects.all().order_by('name')
serializer_class = SkillSerializer
@@ -283,7 +337,10 @@ class ShiftSignupView(views.APIView):
captcha_token = request.data.get('captcha_token', '').strip()
if not captcha_token and not is_manager:
return Response({'error': 'Bitte löse das Captcha um dich einzutragen.'}, status=status.HTTP_400_BAD_REQUEST)
return Response({'error': 'Bitte schließe zuerst den ALTCHA Spamschutz ab.'}, status=status.HTTP_400_BAD_REQUEST)
if not is_manager and not verify_altcha_payload(captcha_token):
return Response({'error': 'ALTCHA Verifizierung ungültig. Bitte versuche es erneut.'}, status=status.HTTP_400_BAD_REQUEST)
# If NOT a manager, enforce required skills check for guest signups
if not is_manager and shift.required_skills.exists():