Security

Contao uses Symfony’s Security Component to handle front end and back end user authentication and authorization. The Contao Managed Edition provides its own firewall, user providers and access control via the contao/manager-bundle. If you do not use the Managed Edition and added Contao to your custom Symfony application, you will have to register Contao’s security settings yourself, as mentioned in the Getting Started article. Contao’s Symfony Security configuration in Contao 4.9 looks like this for example:

Contao's Symfony Security Configuration

If you want to learn more about Symfony’s Security Component use the provided links to read up on. This documentation will only cover implementation details that are unique to Contao.

Since within Contao you can put a login form on basically any page, Contao does not utilise Symfony’s built-in form_login Authentication Provider. Instead, Contao implements its own user checker and request matcher service, the latter of which checks the request scope. For example in the front end, all URLs to Contao pages will have the _scope request attribute set to frontend and the contao_frontend firewall will thus be applicable to all these URLs. Contao implements an authentication listener which will check for any POST request containing the parameters username and password and the parameter FORM_SUBMIT with the value tl_login (as these are the parameters used by Contao’s login module).

Voters

This feature is available in Contao 4.7 and later.

Starting with Contao 4.7 Contao implements Voters in order to easily check whether an authenticated user is authorized to access specific resources. These voters are automatically added to Symfony’s security system and then invoked when the respective permission is accessed via the Security Helper.

Contao automatically uses the priority access decision strategy for any request that is either in Contao’s frontend or backend scope. This means the first voter that does not abstain will decide on the vote. Thus if you want to expand voting on a certain back end privilege you need to make sure that your voter abstains from any query it is not concerned with and that the service has a priority higher than the default via the security.voter service tag.

The security helper can be used to check whether the currently authenticated user has access in the back end to specific forms, table fields (as defined via the DCA), folders and modules for example:

// User has access to form ID 5
$security->isGranted('contao_user.forms', 5);

// User is allowed to access field "published" of table "tl_page"
$security->isGranted('contao_user.alexf', 'tl_page::published');

// Check access to folder
$security->isGranted('contao_user.filemounts', '/files/foo/bar');

This feature is available in Contao 4.10 and later.

You can also use the security helper to check wether the user can edit a page or is allowed to edit fields in a specific DCA for example:

// whether the user can access any field in tl_page
$security->isGranted('contao_user.can_edit_fields', 'tl_page');

// whether user can edit the given page (array of row or page model)
$security->isGranted('contao_user.can_edit_page', [/* row of page data */]);
$security->isGranted('contao_user.can_edit_page', $pageModel);

This feature is available in Contao 4.12 and later.

The security helper can also be used to check whether a front end user belongs to any of the specified user groups:

$security->isGranted('contao_member.groups', $groupId);
$security->isGranted('contao_member.groups', [/* array of group IDs */]);

This feature is available in Contao 5.0 and later.

The contao_dc.<data-container> permission can be used to check whether a back end user has the right to perform certain DCA actions. The subject in this case will be a Contao\CoreBundle\Security\DataContainer\AbstractAction.

use Contao\CoreBundle\Security\DataContainer\CreateAction;
use Contao\CoreBundle\Security\DataContainer\DeleteAction;
use Contao\CoreBundle\Security\DataContainer\ReadAction;
use Contao\CoreBundle\Security\DataContainer\UpdateAction;

$security->isGranted('contao_dc.tl_foobar', new CreateAction('tl_foobar', $record));
$security->isGranted('contao_dc.tl_foobar', new DeleteAction('tl_foobar', $record));
$security->isGranted('contao_dc.tl_foobar', new ReadAction('tl_foobar', $record));
$security->isGranted('contao_dc.tl_foobar', new UpdateAction('tl_foobar', $record));

since 4.10 There are now class constants available for the various permission attributes, so that you do not have to remember them yourself and instead can use your IDE to find the correct attribute. For the Contao Core these constants are available in Contao\CoreBundle\Security\ContaoCorePermissions while the permissions of the additional bundles are available in Contao\NewsBundle\Security\ContaoNewsPermissions, Contao\CalendarBundle\Security\ContaoCalendarPermissions, Contao\NewsletterBundle\Security\ContaoNewsletterPermissions and Contao\FaqBundle\Security\ContaoFaqPermissions respectively. The PHP documentation of these constants also contain hints about what type the subject should be. Examples:

use Contao\CoreBundle\Security\ContaoCorePermissions;
use Contao\NewsBundle\Security\ContaoNewsPermissions;

// Whether user can access the news module in the back end
$security->isGranted(ContaoCorePermissions::USER_CAN_ACCESS_MODULE, 'news');

// Whether user can use the hidden input field in the form generator
$security->isGranted(ContaoCorePermissions::USER_CAN_ACCESS_FIELD, 'hidden');

// Whether user has access to any field of tl_content
$security->isGranted(ContaoCorePermissions::USER_CAN_EDIT_FIELDS_OF_TABLE, 'tl_content');

// Whether user can create news archives
$security->isGranted(ContaoNewsPermissions::USER_CAN_CREATE_ARCHIVES);

Examples

By default admins can access everything and you can restrict access to back end sections only for non-admins via back end user groups. The following example implements a custom voter for your application which grants access to the “Maintenance” back end section only for the admin with ID “1”.

// src/Security/Voter/AdminMaintenanceAccessVoter.php
namespace App\Security\Voter;

use Contao\BackendUser;
use Contao\CoreBundle\Security\ContaoCorePermissions;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;

class AdminMaintenanceAccessVoter extends Voter
{
    public function vote(TokenInterface $token, mixed $subject, array $attributes): int
    {
        // Abstain if not back end admin
        if (!($user = $token->getUser()) instanceof BackendUser || !$user->isAdmin) {
            return Voter::ACCESS_ABSTAIN;
        }

        return parent::vote($token, $subject, $attributes);
    }

    protected function supports(string $attribute, $subject): bool
    {
        // Abstain if we are not voting for maintenance back end module access
        if ('maintenance' !== $subject || $attribute !== ContaoCorePermissions::USER_CAN_ACCESS_MODULE) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        // Only allow admin with ID "1"
        return 1 === (int) $token->getUser()->id;
    }
}

since 5.0 Here is another example with which you can restrict editing of news records to their original authors. The voter checks for any update or delete actions of the data container and then checks whether the author of the news record is the currently logged in user. In this case we implement the necessary checks also for the child table tl_content accordingly - otherwise you would still be able to edit the news content.

// src/Security/Voter/NewsAccessVoter.php
namespace App\Security\Voter;

use Contao\BackendUser;
use Contao\CoreBundle\Security\ContaoCorePermissions;
use Contao\CoreBundle\Security\DataContainer\DeleteAction;
use Contao\CoreBundle\Security\DataContainer\UpdateAction;
use Contao\NewsModel;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class NewsAccessVoter extends Voter
{
    protected function supports(string $attribute, $subject): bool
    {
        // We only want to vote on edit actions (delete and update)
        if (!$subject instanceof DeleteAction && !$subject instanceof UpdateAction) {
            return false;
        }

        if (ContaoCorePermissions::DC_PREFIX.'tl_news' === $attribute) {
            return true;
        }

        // Also take content elements of news into account
        if (ContaoCorePermissions::DC_PREFIX.'tl_content' === $attribute) {
            return 'tl_news' === $subject->getCurrent()['ptable'];
        }

        return false;
    }

    /**
     * @param DeleteAction|UpdateAction $subject
     */
    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        /** @var BackendUser $user */
        $user = $token->getUser();

        if ($user->isAdmin) {
            return true;
        }

        // Determine the author ID
        $record = $subject->getCurrent();

        if ('tl_news' === $subject->getDataSource()) {
            $authorId = $record['author'];
        } else {
            $news = NewsModel::findByPk($record['pid']);
            $authorId = $news->author;
        }

        return (int) $user->id === (int) $authorId;
    }
}

Custom Back End Access Rights

To implement your own back end access rights (e.g. for custom modules in the back end) the following steps are necessary:

  1. Add your new permission to $GLOBALS['TL_PERMISSIONS']. This registers this permission to be used by Contao’s contao_user voter.
  2. Add your new permission to tl_user and tl_user_group.
// contao/config/config.php
$GLOBALS['TL_PERMISSIONS'][] = 'my_permissions';
// contao/dca/tl_user.php
use Contao\CoreBundle\DataContainer\PaletteManipulator;

$GLOBALS['TL_DCA']['tl_user']['fields']['my_permissions'] = [
    'exclude' => true,
    'inputType' => 'checkbox',
    'eval' => ['multiple' => true],
    'options' => [
        'first_permission' => 'First permission',
        'second_permission' => 'Second permission',
    ],
    'sql' => ['type' => 'blob', 'notnull' => false],
];

PaletteManipulator::create()
    ->addLegend('my_legend', null)
    ->addField('my_permissions', 'my_legend', PaletteManipulator::POSITION_APPEND)
    ->applyToPalette('extend', 'tl_user')
    ->applyToPalette('custom', 'tl_user')
;
// contao/dca/tl_user_group.php
use Contao\CoreBundle\DataContainer\PaletteManipulator;

$GLOBALS['TL_DCA']['tl_user_group']['fields']['my_permissions'] = [
    'exclude' => true,
    'inputType' => 'checkbox',
    'eval' => ['multiple' => true],
    'options' => [
        'first_permission' => 'First permission',
        'second_permission' => 'Second permission',
    ],
    'sql' => ['type' => 'blob', 'notnull' => false],
];

PaletteManipulator::create()
    ->addLegend('my_legend', null)
    ->addField('my_permissions', 'my_legend', PaletteManipulator::POSITION_APPEND)
    ->applyToPalette('default', 'tl_user_group')
;

Once that is done you can check for this permission in your own back end controller for example. These permissions can be checked via the security helper by using the contao_user.* attribute. We also check that the current user is not ROLE_ADMIN because administrators should always be able to access the controller.

// src/Controller/BackendController.php
namespace App\Controller;

use Contao\CoreBundle\Exception\AccessDeniedException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Twig\Environment;

#[Route('/contao/my-backend-route', name: BackendController::class, defaults: ['_scope' => 'backend'])]
class BackendController
{
    private $twig;
    private $security;

    public function __construct(Environment $twig, Security $security)
    {
        $this->twig = $twig;
        $this->security = $security;
    }

    public function __invoke(): Response
    {
        if (!$this->security->isGranted('ROLE_ADMIN') && !$this->security->isGranted('contao_user.my_permissions', 'first_permission')) {
            throw new AccessDeniedException('Not enough permissions to access this controller.');
        }

        return new Response($this->twig->render('my_backend_route.html.twig', []));
    }
}

We do not need to check whether a user is logged in this case, since the Contao firewall automatically checks this for this controller with the configured routing parameters (see also the back end route guide).

Instead of extending Contao’s own permissions system you are also free to implement your own voter. See the examples above.