Refactor CSRF token validation in backoffice controllers

- Applied `#[IsCsrfTokenValid]` attribute for CSRF checks to simplify and standardize validation.
- Removed manual `isCsrfTokenValid` calls and associated exception throwing.
- Updated method signatures across affected endpoints to remove unnecessary `Request` dependency.
- Ensured consistency in route HTTP method restrictions where applicable.
This commit is contained in:
2026-05-24 16:28:05 +02:00
parent 0aeb943aa2
commit 3c878d126f
3 changed files with 24 additions and 47 deletions
@@ -10,6 +10,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
use Tvdt\Controller\AbstractController;
use Tvdt\Entity\Elimination;
use Tvdt\Entity\Quiz;
@@ -20,35 +21,30 @@ final class PrepareEliminationController extends AbstractController
{
public function __construct(private readonly EliminationFactory $eliminationFactory, private readonly EntityManagerInterface $em) {}
#[IsCsrfTokenValid('prepare_elimination')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/elimination/prepare',
name: 'tvdt_prepare_elimination',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID],
methods: ['POST'],
)]
public function index(Season $season, Quiz $quiz, Request $request): RedirectResponse
public function index(Season $season, Quiz $quiz): RedirectResponse
{
if (!$this->isCsrfTokenValid('prepare_elimination', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
$elimination = $this->eliminationFactory->createEliminationFromQuiz($quiz);
return $this->redirectToRoute('tvdt_prepare_elimination_view', ['elimination' => $elimination->id]);
}
#[IsCsrfTokenValid('prepare_elimination', methods: ['POST'])]
#[Route(
'/backoffice/elimination/{elimination}',
name: 'tvdt_prepare_elimination_view',
requirements: ['elimination' => Requirement::UUID],
methods: ['GET', 'POST'],
)]
public function viewElimination(Elimination $elimination, Request $request): Response
{
if ('POST' === $request->getMethod()) {
if (!$this->isCsrfTokenValid('prepare_elimination', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
if ($request->isMethod('POST')) {
$elimination->updateFromInputBag($request->request);
$this->em->flush();
@@ -57,6 +53,8 @@ final class PrepareEliminationController extends AbstractController
}
$this->addFlash('success', 'Elimination updated');
return $this->redirectToRoute('tvdt_prepare_elimination_view', ['elimination' => $elimination->id]);
}
return $this->render('backoffice/prepare_elimination/index.html.twig', [
+12 -32
View File
@@ -12,6 +12,7 @@ use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
use Tvdt\Controller\AbstractController;
@@ -183,6 +184,7 @@ class QuizController extends AbstractController
]);
}
#[IsCsrfTokenValid('candidate_answer')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/candidates/{question}',
@@ -192,10 +194,6 @@ class QuizController extends AbstractController
)]
public function saveCandidateAnswers(Season $season, Quiz $quiz, Question $question, Request $request): RedirectResponse
{
if (!$this->isCsrfTokenValid('candidate_answer', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
if (false === $season->quizzes->contains($quiz)
|| false === $quiz->questions->contains($question)) {
throw new BadRequestHttpException('Invalid quiz or question');
@@ -244,6 +242,7 @@ class QuizController extends AbstractController
]);
}
#[IsCsrfTokenValid('enable_quiz')]
#[IsGranted(SeasonVoter::EDIT, subject: 'season')]
#[Route(
'/backoffice/season/{seasonCode:season}/quiz/{quiz}/enable',
@@ -251,12 +250,8 @@ class QuizController extends AbstractController
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'quiz' => Requirement::UUID.'|null'],
methods: ['POST'],
)]
public function enableQuiz(Season $season, ?Quiz $quiz, Request $request): RedirectResponse
public function enableQuiz(Season $season, ?Quiz $quiz): RedirectResponse
{
if (!$this->isCsrfTokenValid('enable_quiz', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
$season->activeQuiz = $quiz;
$this->em->flush();
@@ -267,6 +262,7 @@ class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_backoffice_season', ['seasonCode' => $season->seasonCode]);
}
#[IsCsrfTokenValid('clear_quiz')]
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
#[Route(
'/backoffice/quiz/{quiz}/clear',
@@ -274,12 +270,8 @@ class QuizController extends AbstractController
requirements: ['quiz' => Requirement::UUID],
methods: ['POST'],
)]
public function clearQuiz(Quiz $quiz, Request $request): RedirectResponse
public function clearQuiz(Quiz $quiz): RedirectResponse
{
if (!$this->isCsrfTokenValid('clear_quiz', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
try {
$this->quizRepository->clearQuiz($quiz);
$this->addFlash('success', $this->translator->trans('Quiz cleared'));
@@ -290,6 +282,7 @@ class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_backoffice_quiz', ['seasonCode' => $quiz->season->seasonCode, 'quiz' => $quiz->id]);
}
#[IsCsrfTokenValid('delete_quiz')]
#[IsGranted(SeasonVoter::DELETE, subject: 'quiz')]
#[Route(
'/backoffice/quiz/{quiz}/delete',
@@ -297,12 +290,8 @@ class QuizController extends AbstractController
requirements: ['quiz' => Requirement::UUID],
methods: ['POST'],
)]
public function deleteQuiz(Quiz $quiz, Request $request): RedirectResponse
public function deleteQuiz(Quiz $quiz): RedirectResponse
{
if (!$this->isCsrfTokenValid('delete_quiz', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
$this->quizRepository->deleteQuiz($quiz);
$this->addFlash('success', $this->translator->trans('Quiz deleted'));
@@ -310,6 +299,7 @@ class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_backoffice_season', ['seasonCode' => $quiz->season->seasonCode]);
}
#[IsCsrfTokenValid('candidate_correction')]
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
#[Route(
'/backoffice/quiz/{quiz}/candidate/{candidate}/modify_correction',
@@ -319,10 +309,6 @@ class QuizController extends AbstractController
)]
public function modifyCorrection(Quiz $quiz, Candidate $candidate, Request $request): RedirectResponse
{
if (!$this->isCsrfTokenValid('candidate_correction', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
$corrections = (float) $request->request->get('corrections');
$this->quizCandidateRepository->setCorrectionsForCandidate($quiz, $candidate, $corrections);
@@ -330,6 +316,7 @@ class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_backoffice_quiz', ['seasonCode' => $quiz->season->seasonCode, 'quiz' => $quiz->id]);
}
#[IsCsrfTokenValid('candidate_penalty')]
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
#[Route(
'/backoffice/quiz/{quiz}/candidate/{candidate}/modify_penalty',
@@ -339,10 +326,6 @@ class QuizController extends AbstractController
)]
public function modifyPenalty(Quiz $quiz, Candidate $candidate, Request $request): RedirectResponse
{
if (!$this->isCsrfTokenValid('candidate_penalty', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
$penalty = (int) $request->request->get('penalty');
$this->quizCandidateRepository->setPenaltyForCandidate($quiz, $candidate, $penalty);
@@ -350,6 +333,7 @@ class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_backoffice_quiz', ['seasonCode' => $quiz->season->seasonCode, 'quiz' => $quiz->id]);
}
#[IsCsrfTokenValid('toggle_candidate')]
#[IsGranted(SeasonVoter::EDIT, subject: 'quiz')]
#[Route(
'/backoffice/quiz/{quiz}/candidate/{candidate}/toggle',
@@ -357,12 +341,8 @@ class QuizController extends AbstractController
requirements: ['quiz' => Requirement::UUID, 'candidate' => Requirement::UUID],
methods: ['POST'],
)]
public function toggleCandidate(Quiz $quiz, Candidate $candidate, Request $request): RedirectResponse
public function toggleCandidate(Quiz $quiz, Candidate $candidate): RedirectResponse
{
if (!$this->isCsrfTokenValid('toggle_candidate', $request->request->get('_token'))) {
throw $this->createAccessDeniedException();
}
$quizCandidate = $this->quizCandidateRepository->findOneBy([
'quiz' => $quiz,
'candidate' => $candidate,
+4 -5
View File
@@ -9,6 +9,7 @@ use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsCsrfTokenValid;
use Symfony\Contracts\Translation\TranslatorInterface;
use Tvdt\Entity\Answer;
use Tvdt\Entity\Candidate;
@@ -70,10 +71,12 @@ final class QuizController extends AbstractController
return $this->render('quiz/enter_name.twig', ['season' => $season, 'form' => $form]);
}
#[IsCsrfTokenValid('question', tokenKey: 'token', methods: ['POST'])]
#[Route(
path: '/{seasonCode:season}/{nameHash}',
name: 'tvdt_quiz_quiz_page',
requirements: ['seasonCode' => self::SEASON_CODE_REGEX, 'nameHash' => self::CANDIDATE_HASH_REGEX],
methods: ['GET', 'POST'],
)]
public function quizPage(
Season $season,
@@ -96,11 +99,7 @@ final class QuizController extends AbstractController
return $this->redirectToRoute('tvdt_quiz_enter_name', ['seasonCode' => $season->seasonCode]);
}
if ('POST' === $request->getMethod()) {
if (!$this->isCsrfTokenValid('question', $request->request->get('token'))) {
throw $this->createAccessDeniedException();
}
if ($request->isMethod('POST')) {
// TODO: Extract saving answer logic to a service
// Check if candidate is inactive for this quiz
$quizCandidate = $this->quizCandidateRepository->findOneBy(['quiz' => $quiz, 'candidate' => $candidate]);