Skip to content

[Security] Clarify the purpose of access denied handler (and entry point) #14045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 31, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 166 additions & 15 deletions security/access_denied_handler.rst
Original file line number Diff line number Diff line change
@@ -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 <service-container-services-load-example>`.
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

<!-- 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>

.. 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;

Expand Down Expand Up @@ -49,11 +148,21 @@ configure it under your firewall:
.. code-block:: xml

<!-- config/packages/security.xml -->
<config>
<firewall name="main">
<access-denied-handler>App\Security\AccessDeniedHandler</access-denied-handler>
</firewall>
</config>
<?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>

.. code-block:: php

Expand All @@ -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 <use-kernel-exception-event>`::

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