How to Customize Access Denied Responses

Symfony 5.4 is backed by Private Packagist.

How to Customize Access Denied Responses

In Symfony, you can throw an 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:

  • 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 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\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;

class AuthenticationEntryPoint implements AuthenticationEntryPointInterface
{
    private $urlGenerator;

    public function __construct(UrlGeneratorInterface $urlGenerator)
    {
        $this->urlGenerator = $urlGenerator;
    }

    public function start(Request $request, AuthenticationException $authException = null): RedirectResponse
    {
        // add a custom flash message and redirect to the login page
        $request->getSession()->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 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:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/packages/security.yaml
    firewalls:
        # ...
    
        main:
            # ...
            entry_point: App\Security\AuthenticationEntryPoint
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall name="main"
                entry-point="App\Security\AuthenticationEntryPoint"
            >
                <!-- ... -->
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // config/packages/security.php
    use App\Security\AuthenticationEntryPoint;
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $security->firewall('main')
            // ....
            ->entryPoint(AuthenticationEntryPoint::class)
        ;
    };
    

Customize the Forbidden Response

Create a class that implements Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface. 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):

// src/Security/AccessDeniedHandler.php
namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;

class AccessDeniedHandler implements AccessDeniedHandlerInterface
{
    public function handle(Request $request, AccessDeniedException $accessDeniedException): ?Response
    {
        // ...

        return new Response($content, 403);
    }
}

If you’re using the default services.yaml configuration, you’re done! Symfony will automatically know about your new service. You can then configure it under your firewall:

  • YAML
    1
    2
    3
    4
    5
    6
    7
    # config/packages/security.yaml
    firewalls:
        # ...
    
        main:
            # ...
            access_denied_handler: App\Security\AccessDeniedHandler
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    <!-- config/packages/security.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services
            https://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <config>
            <firewall name="main"
                access-denied-handler="App\Security\AccessDeniedHandler"
            >
                <!-- ... -->
            </firewall>
        </config>
    </srv:container>
    
  • PHP
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    // config/packages/security.php
    use App\Security\AccessDeniedHandler;
    use Symfony\Config\SecurityConfig;
    
    return static function (SecurityConfig $security) {
        $security->firewall('main')
            // ....
            ->accessDeniedHandler(AccessDeniedHandler::class)
        ;
    };
    

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 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->getThrowable();
        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();
    }
}

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.