Keamanan Symfony 2026: Voters, Firewalls, dan Pertanyaan Wawancara Teknis

Panduan lengkap keamanan Symfony: konfigurasi firewall, Access Token Handler stateless, sistem voter untuk otorisasi granular, atribut IsGranted, strategi access decision, debugging Twig di 7.4, serta pertanyaan wawancara teknis yang sering muncul.

Symfony Security Voters Firewalls

Komponen Security Symfony dibangun di atas dua mekanisme inti: firewall menangani autentikasi (siapa pengguna ini?), dan voter menangani otorisasi (apa yang boleh dilakukan pengguna ini?). Pemisahan tanggung jawab ini menjadi fondasi arsitektur yang memungkinkan developer membangun sistem keamanan berlapis, mudah diuji, dan mampu menampung logika bisnis sekompleks apa pun. Bagi developer yang mempersiapkan diri untuk wawancara teknis Symfony, penguasaan kedua mekanisme ini menjadi pembeda antara kandidat level menengah dan senior.

Symfony 7.4 LTS Security Stack

Symfony 7.4 (LTS terkini, didukung hingga November 2029) memperkenalkan penjelasan keputusan voter, fungsi otorisasi Twig baru (access_decision() dan access_decision_for_user()), serta penandatanganan pesan untuk handler Messenger. Semua contoh dalam artikel ini menargetkan Symfony 7.4+.

Cara Firewall Symfony Mengontrol Autentikasi

Firewall dalam Symfony bukan merujuk pada perangkat jaringan, melainkan pada titik masuk sistem autentikasi yang didefinisikan di config/packages/security.yaml. Setiap firewall menentukan bagian mana dari aplikasi yang dilindungi dan mekanisme autentikasi apa yang digunakan. Symfony mengevaluasi firewall dari atas ke bawah, dan firewall pertama yang pattern-nya cocok dengan URL request akan digunakan.

Urutan evaluasi ini sangat krusial. Kesalahan penempatan firewall bisa menyebabkan endpoint API terproteksi oleh firewall form login, atau endpoint sensitif tidak terproteksi sama sekali. Berikut konfigurasi yang mendemonstrasikan tiga skenario umum:

yaml
# config/packages/security.yaml
security:
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        api:
            pattern: ^/api
            stateless: true
            access_token:
                token_handler: App\Security\ApiTokenHandler

        main:
            lazy: true
            form_login:
                login_path: app_login
                check_path: app_login
                enable_csrf: true
            logout:
                path: app_logout
            remember_me:
                secret: '%kernel.secret%'
            login_throttling:
                max_attempts: 5
                interval: '15 minutes'

Firewall dev menonaktifkan keamanan untuk rute profiler dan aset statis. Firewall api dikonfigurasi sebagai stateless dengan Access Token Handler karena klien API mengirimkan token pada setiap request tanpa mengandalkan session. Firewall main menangani autentikasi browser melalui form login dengan proteksi CSRF, remember-me, dan login throttling yang membatasi 5 percobaan dalam 15 menit.

Opsi lazy: true pada firewall main memastikan Symfony tidak memulai session atau autentikasi kecuali halaman yang diakses membutuhkan informasi pengguna, sehingga halaman publik dimuat lebih cepat.

Stateless vs Stateful: Dua Paradigma Autentikasi

Perbedaan antara firewall stateless dan stateful merupakan konsep fundamental dalam arsitektur Symfony.

Firewall stateful (perilaku default) menyimpan informasi autentikasi di session PHP. Setelah login, Symfony menyimpan security token di session dan merekonstruksi konteks keamanan pada request berikutnya tanpa autentikasi ulang. Pendekatan ini cocok untuk aplikasi web tradisional dengan browser yang mengirim cookie session secara otomatis.

Firewall stateless (stateless: true) tidak membaca maupun menulis session. Setiap request harus membawa kredensialnya sendiri, biasanya berupa token di header Authorization: Bearer <token>. Pendekatan ini standar untuk REST API, backend mobile, dan komunikasi antar-microservice karena memungkinkan skalabilitas horizontal tanpa berbagi session.

Menggabungkan kedua paradigma dalam satu aplikasi adalah praktik yang lazim: konsumen API menggunakan access token, pengguna browser menggunakan form login dengan session.

Membangun Custom Access Token Handler

Authenticator access_token, tersedia sejak Symfony 6.2, menyediakan abstraksi yang bersih untuk autentikasi berbasis token. Implementasinya hanya memerlukan satu class yang mengimplementasikan AccessTokenHandlerInterface:

src/Security/ApiTokenHandler.phpphp
namespace App\Security;

use App\Repository\ApiTokenRepository;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;

class ApiTokenHandler implements AccessTokenHandlerInterface
{
    public function __construct(
        private readonly ApiTokenRepository $repository,
    ) {}

    public function getUserBadgeFrom(#[\SensitiveParameter] string $accessToken): UserBadge
    {
        $token = $this->repository->findOneByValue($accessToken);

        if (!$token || !$token->isValid()) {
            throw new BadCredentialsException('Invalid or expired token.');
        }

        return new UserBadge($token->getUser()->getUserIdentifier());
    }
}

Symfony mengekstrak token dari header Authorization: Bearer <token> secara default. Handler memvalidasi token terhadap database dan mengembalikan UserBadge yang mengidentifikasi pengguna. Framework menangani sisanya: membuat security token, mengirim event autentikasi, dan menetapkan pengguna terautentikasi pada request.

Atribut #[\SensitiveParameter] mencegah nilai token muncul di stack trace atau log, sebuah keharusan keamanan untuk data kredensial.

Sistem Voter Symfony: Otorisasi Granular Berbasis Logika Bisnis

Voter merupakan mekanisme otorisasi paling fleksibel di Symfony. Berbeda dengan role statis seperti ROLE_ADMIN, voter membuat keputusan berdasarkan konteks: siapa penggunanya, apa objek yang diakses, dan bagaimana statusnya. Setiap voter meng-extend class Voter dan mengimplementasikan supports() serta voteOnAttribute().

Pada Symfony 7.4, voter mendapatkan parameter ?Vote $vote = null yang memungkinkan penambahan alasan pada setiap keputusan otorisasi, sehingga debugging menjadi lebih mudah.

src/Security/Voter/PostVoter.phpphp
namespace App\Security\Voter;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    public const EDIT = 'POST_EDIT';
    public const DELETE = 'POST_DELETE';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, [self::EDIT, self::DELETE])
            && $subject instanceof Post;
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token,
        ?Vote $vote = null,
    ): bool {
        $user = $token->getUser();

        if (!$user instanceof User) {
            $vote?->addReason('User is not authenticated.');
            return false;
        }

        /** @var Post $post */
        $post = $subject;

        return match ($attribute) {
            self::EDIT => $this->canEdit($post, $user, $vote),
            self::DELETE => $this->canDelete($post, $user, $vote),
            default => false,
        };
    }

    private function canEdit(Post $post, User $user, ?Vote $vote): bool
    {
        if ($post->getAuthor() === $user) {
            return true;
        }

        $vote?->addReason('Only the author can edit this post.');
        return false;
    }

    private function canDelete(Post $post, User $user, ?Vote $vote): bool
    {
        if (in_array('ROLE_ADMIN', $user->getRoles())) {
            return true;
        }

        if ($post->getAuthor() === $user && !$post->isPublished()) {
            return true;
        }

        $vote?->addReason('Only admins or authors of unpublished posts can delete.');
        return false;
    }
}

Method supports() menentukan apakah voter ini relevan untuk kombinasi atribut dan subjek tertentu. Voter hanya aktif untuk atribut POST_EDIT dan POST_DELETE yang diterapkan pada instance Post. Jika mengembalikan true, method voteOnAttribute() dipanggil untuk evaluasi sesungguhnya.

Logika canDelete() mendemonstrasikan aturan bisnis berlapis: administrator selalu dapat menghapus postingan apa pun, sedangkan penulis hanya dapat menghapus postingannya sendiri selama belum dipublikasikan. Aturan semacam ini tidak mungkin diekspresikan melalui hierarki role statis, sehingga voter menjadi satu-satunya solusi yang tepat.

Siap menguasai wawancara Symfony Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Menggunakan Atribut IsGranted pada Controller

Atribut #[IsGranted] menyediakan cara deklaratif untuk menerapkan aturan otorisasi langsung pada method controller. Pendekatan ini menggantikan pemanggilan manual $this->denyAccessUnlessGranted() dengan sintaks yang lebih bersih dan lebih mudah dibaca.

src/Controller/PostController.phpphp
namespace App\Controller;

use App\Entity\Post;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;

class PostController extends AbstractController
{
    #[Route('/posts/{id}/edit', name: 'post_edit')]
    #[IsGranted('POST_EDIT', subject: 'post', message: 'You cannot edit this post.')]
    public function edit(Post $post): Response
    {
        // The voter already validated access.
        // Only the post author reaches this point.
        return $this->render('post/edit.html.twig', ['post' => $post]);
    }

    #[Route('/admin/posts', name: 'admin_posts')]
    #[IsGranted('ROLE_ADMIN')]
    public function adminIndex(): Response
    {
        return $this->render('admin/posts.html.twig');
    }
}

Parameter subject: 'post' menginstruksikan Symfony untuk meneruskan argumen controller $post ke voter sebagai subjek evaluasi. Doctrine param converter mengonversi {id} dari URL menjadi instance Post. Jika voter menolak akses, Symfony melempar AccessDeniedException sebelum body method dieksekusi.

Method adminIndex() memperlihatkan contoh sederhana dengan pemeriksaan role ROLE_ADMIN tanpa logika kontekstual. Kedua pendekatan memisahkan logika otorisasi dari body controller secara deklaratif.

Strategi Access Decision dan Koordinasi Antar-Voter

Ketika beberapa voter memberikan suara pada atribut yang sama, Symfony memerlukan strategi untuk menentukan keputusan akhir. Access Decision Manager mengagregasi vote dari semua voter menggunakan salah satu dari empat strategi berikut:

| Strategi | Memberikan Akses Ketika | Paling Cocok Untuk | |---|---|---| | affirmative (default) | Minimal satu voter menyetujui | Penggunaan umum, pendekatan permisif | | consensus | Mayoritas voter menyetujui | Keputusan bergaya komite | | unanimous | Semua voter menyetujui | Operasi dengan keamanan tinggi | | priority | Voter non-abstain pertama yang memutuskan | Evaluasi berurutan berdasarkan prioritas |

Untuk sebagian besar aplikasi, strategi default affirmative sudah memadai. Namun, untuk operasi sensitif di mana setiap syarat keamanan harus terpenuhi, strategi unanimous lebih tepat:

yaml
# config/packages/security.yaml
security:
    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: false

Dengan strategi unanimous, satu suara penolakan dari voter mana pun sudah cukup untuk memblokir akses. Opsi allow_if_all_abstain: false memastikan bahwa jika semua voter abstain, akses ditolak secara default sesuai prinsip "deny by default".

Debugging Voter di Symfony 7.4 dengan Fungsi Twig

Symfony 7.4 memperkenalkan fungsi access_decision() di Twig yang mengembalikan objek AccessDecision lengkap dengan putusan, vote individual, dan alasan dari setiap voter. Fungsi ini memberikan transparansi penuh tentang mengapa suatu keputusan otorisasi dibuat, menghilangkan proses tebak-tebakan yang selama ini menjadi kendala dalam debugging otorisasi.

twig
{# templates/post/show.html.twig #}
{% set decision = access_decision('POST_EDIT', post) %}

{% if decision.isGranted %}
    <a href="{{ path('post_edit', {id: post.id}) }}">Edit</a>
{% endif %}

{# In dev: inspect why access was denied #}
{% if app.debug and not decision.isGranted %}
    {% for vote in decision.votes %}
        {# vote.reasons contains explanations from voters #}
    {% endfor %}
{% endif %}

Dibandingkan is_granted(), access_decision() memberikan informasi yang jauh lebih kaya. Informasi dari vote.reasons langsung menunjukkan voter mana yang menolak akses dan alasannya, menghemat waktu debugging secara signifikan. Blok debugging dikondisikan oleh app.debug agar hanya tampil di lingkungan development.

Pertanyaan Wawancara Teknis Symfony Security

Pertanyaan-pertanyaan berikut sering muncul dalam sesi wawancara teknis untuk posisi developer Symfony, mulai dari konsep dasar hingga keputusan arsitektur tingkat lanjut.

Apa perbedaan antara autentikasi dan otorisasi di Symfony?

Autentikasi memverifikasi identitas pengguna dan ditangani oleh firewall beserta authenticator-authenticatornya. Otorisasi memeriksa apakah pengguna yang sudah teridentifikasi memiliki izin untuk melakukan tindakan tertentu, dan ditangani oleh voter, role, serta aturan access control. Kedua proses ini bersifat independen namun bekerja secara berurutan: autentikasi selalu terjadi terlebih dahulu, baru kemudian otorisasi dievaluasi.

Kapan sebaiknya menggunakan voter dibandingkan pemeriksaan role?

Pemeriksaan role seperti ROLE_ADMIN atau ROLE_EDITOR sudah memadai untuk otorisasi statis yang melekat pada level pengguna. Voter diperlukan ketika keputusan otorisasi bersifat dinamis dan bergantung pada konteks: "Bisakah pengguna ini mengedit postingan tertentu ini?" Jawabannya bergantung pada siapa penulis postingan tersebut, status publikasinya, atau keanggotaan tim pengguna. Aturan praktisnya sederhana: jika keputusan otorisasi memerlukan pemeriksaan terhadap subjek atau objek tertentu, gunakan voter.

Apa fungsi parameter Vote pada voter di Symfony 7.3+?

Parameter ?Vote $vote = null memungkinkan voter menambahkan penjelasan (reason) pada setiap keputusan otorisasi melalui $vote->addReason(). Informasi ini ditampilkan di Security Profiler dan dapat diakses melalui fungsi access_decision() di Twig. Fitur ini sangat berguna untuk debugging dan audit, karena memberikan transparansi penuh tentang mengapa suatu akses diberikan atau ditolak. Parameter ini bersifat nullable untuk menjaga kompatibilitas mundur dengan voter yang sudah ada.

Bagaimana cara menguji voter secara terisolasi?

Voter merupakan class PHP murni yang dapat diuji tanpa memerlukan kernel Symfony, koneksi database, atau HTTP request. Pengujian unit hanya memerlukan pembuatan TokenInterface yang berisi pengguna dan pemanggilan method vote() secara langsung:

tests/Security/Voter/PostVoterTest.phpphp
namespace App\Tests\Security\Voter;

use App\Entity\Post;
use App\Entity\User;
use App\Security\Voter\PostVoter;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;

class PostVoterTest extends TestCase
{
    private PostVoter $voter;

    protected function setUp(): void
    {
        $this->voter = new PostVoter();
    }

    public function testAuthorCanEdit(): void
    {
        $user = new User();
        $post = (new Post())->setAuthor($user);
        $token = new UsernamePasswordToken($user, 'main', $user->getRoles());

        $result = $this->voter->vote($token, $post, [PostVoter::EDIT]);

        $this->assertSame(VoterInterface::ACCESS_GRANTED, $result);
    }

    public function testNonAuthorCannotEdit(): void
    {
        $author = new User();
        $otherUser = new User();
        $post = (new Post())->setAuthor($author);
        $token = new UsernamePasswordToken($otherUser, 'main', $otherUser->getRoles());

        $result = $this->voter->vote($token, $post, [PostVoter::EDIT]);

        $this->assertSame(VoterInterface::ACCESS_DENIED, $result);
    }
}

Setiap test case mengisolasi satu skenario bisnis: penulis postingan memiliki akses untuk mengedit, sedangkan pengguna lain tidak. Tidak diperlukan booting kernel, koneksi database, maupun HTTP request. Voter termasuk komponen yang paling mudah diuji dalam keseluruhan stack keamanan Symfony.

Apa yang terjadi ketika security: false diatur pada firewall?

Firewall dinonaktifkan sepenuhnya untuk rute yang cocok dengan pattern tersebut. Tidak ada autentikasi yang dilakukan, tidak ada pengguna yang dimuat dari session, dan tidak ada event keamanan yang dikirim. Pengaturan ini hanya boleh digunakan untuk aset statis dan alat development seperti profiler dan Web Debug Toolbar, bukan untuk rute yang menyajikan data pengguna atau menangani operasi bisnis.

Hindari security: false pada Rute API

Mengatur security: false pada firewall yang cocok dengan rute API akan menonaktifkan seluruh stack keamanan. Logging, auditing, dan rate limiting berdasarkan pengguna terautentikasi semuanya berhenti berfungsi. Untuk rute publik yang tetap memerlukan konteks keamanan, gunakan access_control dengan PUBLIC_ACCESS sebagai gantinya.

Memperkuat Keamanan Symfony Melampaui Konfigurasi Default

Selain firewall dan voter, Symfony menyediakan mekanisme UserChecker untuk memvalidasi status akun pengguna pada saat autentikasi. Implementasi ini memungkinkan pemblokiran pengguna yang telah dinonaktifkan, di-ban, atau belum memverifikasi email, sebelum mereka dapat mengakses bagian mana pun dari aplikasi.

src/Security/UserEnabledChecker.phpphp
namespace App\Security;

use App\Entity\User;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class UserEnabledChecker implements UserCheckerInterface
{
    public function checkPreAuth(UserInterface $user): void
    {
        if (!$user instanceof User) {
            return;
        }

        if ($user->isBanned()) {
            throw new CustomUserMessageAccountStatusException(
                'This account has been suspended.'
            );
        }
    }

    public function checkPostAuth(UserInterface $user): void
    {
        if (!$user instanceof User) {
            return;
        }

        if (!$user->isEmailVerified()) {
            throw new CustomUserMessageAccountStatusException(
                'Please verify your email address before logging in.'
            );
        }
    }
}

Method checkPreAuth() dieksekusi sebelum kredensial diverifikasi, sehingga akun yang di-ban langsung ditolak tanpa memproses password. Method checkPostAuth() dieksekusi setelah kredensial valid, tempat yang tepat untuk pengecekan verifikasi email. Praktik penguatan lainnya mencakup login throttling, proteksi CSRF pada semua form, dan konfigurasi header keamanan HTTP.

Security Events untuk Audit dan Monitoring

Symfony mengirim event pada setiap tahap proses keamanan: LoginSuccessEvent, LoginFailureEvent, LogoutEvent, SwitchUserEvent, dan TokenDeauthenticatedEvent. Mendaftarkan event listener pada event-event ini memungkinkan pencatatan audit trail yang komprehensif, pengiriman notifikasi pada aktivitas mencurigakan, serta integrasi dengan sistem monitoring eksternal.

Siap menguasai wawancara Symfony Anda?

Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.

Kesimpulan

Komponen Security Symfony pada versi 7.4 LTS menyediakan arsitektur keamanan yang lengkap, teruji, dan mampu menangani kebutuhan aplikasi PHP modern dari yang paling sederhana hingga yang paling kompleks. Berikut ringkasan konsep-konsep utama yang dibahas dalam artikel ini:

  • Firewall mendefinisikan batas autentikasi dan harus dikonfigurasi sesuai jenis klien: stateful untuk browser, stateless untuk API. Urutan pendefinisian sangat menentukan karena Symfony menggunakan pencocokan first-match
  • Stateless vs stateful bukan soal preferensi teknis, melainkan soal kebutuhan arsitektur. API menggunakan stateless, aplikasi web tradisional menggunakan stateful
  • Access Token Handler menyediakan abstraksi bersih untuk autentikasi API tanpa state, dengan dukungan atribut #[SensitiveParameter] untuk melindungi kredensial
  • Voter merupakan mekanisme otorisasi paling fleksibel untuk logika bisnis kompleks. Parameter Vote di Symfony 7.3+ menambahkan debugging yang sangat berharga
  • Atribut #[IsGranted] memisahkan logika otorisasi dari body controller secara deklaratif, menjaga kode tetap bersih dan mudah dibaca
  • Strategi access decision (affirmative, consensus, unanimous, priority) mengkoordinasikan interaksi antar-voter sesuai tingkat keamanan yang dibutuhkan
  • Fungsi Twig access_decision() di Symfony 7.4 memberikan transparansi penuh tentang alasan di balik keputusan otorisasi
  • Voter mudah diuji secara terisolasi dengan PHPUnit tanpa memerlukan kernel boot atau koneksi database
  • UserChecker menambahkan validasi pre-auth dan post-auth untuk menangani status akun seperti pemblokiran dan verifikasi email

Mulai berlatih!

Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.

Tag

#symfony
#security
#php
#voters
#firewalls
#authentication
#interview

Bagikan

Artikel terkait