diff --git a/security/access_denied_handler.rst b/security/access_denied_handler.rst index e9e780e75ef..eed7589bee2 100644 --- a/security/access_denied_handler.rst +++ b/security/access_denied_handler.rst @@ -1,17 +1,116 @@ .. index:: single: Security; Creating a Custom Access Denied Handler -How to Create a Custom Access Denied Handler -============================================ +How to Customize Access Denied Responses +======================================== -When your application throws an ``AccessDeniedException``, you can handle this exception -with a service to return a custom response. +In Symfony, you can throw an +:class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` +to disallow access to the user. Symfony will handle this exception and +generates a response based on the authentication state: -First, create a class that implements +* **If the user is not authenticated** (or authenticated anonymously), an + authentication entry point is used to generated a response (typically + a redirect to the login page or an *401 Unauthorized* response); +* **If the user is authenticated, but does not have the required + permissions**, a *403 Forbidden* response is generated. + +Customize the Unauthorized Response +----------------------------------- + +You need to create a class that implements +:class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`. +This interface has one method (``start()``) that is called whenever an +unauthenticated user tries to access a protected resource:: + + // src/Security/AuthenticationEntryPoint.php + namespace App\Security; + + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Session\SessionInterface; + use Symfony\Component\Security\Core\Exception\AuthenticationException; + use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface; + + class AuthenticationEntryPoint implements AuthenticationEntryPointInterface + { + private $urlGenerator; + private $session; + + public function __construct(UrlGeneratorInterface $urlGenerator, SessionInterface $session) + { + $this->urlGenerator = $urlGenerator; + $this->session = $session; + } + + public function start(Request $request, AuthenticationException $authException = null): RedirectResponse + { + // add a custom flash message and redirect to the login page + $this->session->getFlashBag()->add('note', 'You have to login in order to access this page.'); + + return new RedirectResponse($this->urlGenerator->generate('security_login')); + } + } + +That's it if you're using the :ref:`default services.yaml configuration `. +Otherwise, you have to register this service in the container. + +Now, configure this service ID as the entry point for the firewall: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + firewalls: + # ... + + main: + # ... + entry_point: App\Security\AuthenticationEntryPoint + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use App\Security\AuthenticationEntryPoint; + + $container->loadFromExtension('security', [ + 'firewalls' => [ + 'main' => [ + // ... + 'entry_point' => AuthenticationEntryPoint::class, + ], + ], + ]); + +Customize the Forbidden Response +-------------------------------- + +Create a class that implements :class:`Symfony\\Component\\Security\\Http\\Authorization\\AccessDeniedHandlerInterface`. -This interface defines one method called ``handle()`` where you can implement whatever -logic that should run when access is denied for the current user (e.g. send a -mail, log a message, or generally return a custom response):: +This interface defines one method called ``handle()`` where you can +implement whatever logic that should execute when access is denied for the +current user (e.g. send a mail, log a message, or generally return a custom +response):: namespace App\Security; @@ -49,11 +148,21 @@ configure it under your firewall: .. code-block:: xml - - - App\Security\AccessDeniedHandler - - + + + + + + + + + .. code-block:: php @@ -69,5 +178,47 @@ configure it under your firewall: ], ]); -That's it! Any ``AccessDeniedException`` thrown by code under the ``main`` firewall -will now be handled by your service. +Customizing All Access Denied Responses +--------------------------------------- + +In some cases, you might want to customize both responses or do a specific +action (e.g. logging) for each ``AccessDeniedException``. In this case, +configure a :ref:`kernel.exception listener `:: + + // src/EventListener/AccessDeniedListener.php + namespace App\EventListener; + + use Symfony\Component\EventDispatcher\EventSubscriberInterface; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\HttpKernel\Event\ExceptionEvent; + use Symfony\Component\HttpKernel\KernelEvents; + use Symfony\Component\Security\Core\Exception\AccessDeniedException; + + class AccessDeniedListener implements EventSubscriberInterface + { + public static function getSubscribedEvents(): array + { + return [ + // the priority must be greater than the Security HTTP + // ExceptionListener, to make sure it's called before + // the default exception listener + KernelEvents::EXCEPTION => ['onKernelException', 2], + ]; + } + + public function onKernelException(ExceptionEvent $event): void + { + $exception = $event->getException(); + if (!$exception instanceof AccessDeniedException) { + return; + } + + // ... perform some action (e.g. logging) + + // optionally set the custom response + $event->setResponse(new Response(null, 403)); + + // or stop propagation (prevents the next exception listeners from being called) + //$event->stopPropagation(); + } + }