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:
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).
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);
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;
}
}
To implement your own back end access rights (e.g. for custom modules in the back end) the following steps are necessary:
$GLOBALS['TL_PERMISSIONS']
. This registers this permission to be used by Contao’s contao_user
voter.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.