Feature: #107826 - Introduce Extbase action authorization attribute 

See forge#107826

Description 

A new authorization mechanism has been introduced for Extbase controller actions using PHP attributes. Extension authors can now implement declarative access control logic directly on action methods using the #[Authorize] attribute.

The #[Authorize] attribute supports multiple authorization strategies:

Built-in checks - Require frontend user login via requireLogin - Require specific frontend user groups via requireGroups

Custom authorization logic - Dedicated authorization class (recommended for complex logic) - Public controller method (for simple checks)

Multiple #[Authorize] attributes can be stacked on a single action. All authorization checks must pass for access to be granted. If any check fails, a PropagateResponseException is thrown with an HTTP 403 response, which immediately stops the Extbase dispatching process.

Examples 

Require frontend user login 

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(requireLogin: true)]
    public function listAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }
}
Copied!

Require specific user groups 

The requireGroups parameter accepts an array of frontend user group identifiers. Groups can be specified either by their UID (recommended) or by their title. If multiple groups are specified, the user must be a member of at least one of the groups (OR logic).

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    // Recommended: Use group UIDs
    #[Authorize(requireGroups: [1, 2])]
    public function adminListAction(): ResponseInterface
    {
        // Only accessible to users in groups 1 or 2
        return $this->htmlResponse();
    }

    // Alternative: Use group titles (not recommended)
    #[Authorize(requireGroups: ['administrators', 'editors'])]
    public function editorListAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }

    // Mixed: UIDs and titles can be combined (not recommended)
    #[Authorize(requireGroups: [1, 'editors'])]
    public function mixedListAction(): ResponseInterface
    {
        return $this->htmlResponse();
    }
}
Copied!

Custom authorization class 

For complex authorization logic, create a dedicated authorization class. This class supports dependency injection and can be reused across controllers.

EXT:my_extension/Classes/Authorization/MyObjectAuthorization.php
namespace MyVendor\MyExtension\Authorization;

use MyVendor\MyExtension\Domain\Model\MyObject;
use TYPO3\CMS\Core\Context\Context;

class MyObjectAuthorization
{
    public function __construct(
        private readonly Context $context
    ) {}

    public function checkOwnership(MyObject $myObject): bool
    {
        $userAspect = $this->context->getAspect('frontend.user');
        return $myObject->getOwner()->getUid() === $userAspect->get('id');
    }
}
Copied!
EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Authorization\MyObjectAuthorization;
use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        $this->view->assign('myObject', $myObject);
        return $this->htmlResponse();
    }

    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function deleteAction(MyObject $myObject): ResponseInterface
    {
        // Delete the object
        return $this->htmlResponse();
    }
}
Copied!

Public controller method 

For simple checks, a public controller method can be used as callback.

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    public function __construct(
        private readonly Context $context
    ) {}

    #[Authorize(callback: 'checkOwnership')]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        $this->view->assign('myObject', $myObject);
        return $this->htmlResponse();
    }

    public function checkOwnership(MyObject $myObject): bool
    {
        $userAspect = $this->context->getAspect('frontend.user');
        return $myObject->getOwner()->getUid() === $userAspect->get('id');
    }
}
Copied!

Combining multiple authorization checks 

Multiple #[Authorize] attributes can be stacked. All checks must pass.

EXT:my_extension/Classes/Controller/MyController.php
namespace MyVendor\MyExtension\Controller;

use MyVendor\MyExtension\Authorization\MyObjectAuthorization;
use MyVendor\MyExtension\Domain\Model\MyObject;
use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Extbase\Attribute\Authorize;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;

class MyController extends ActionController
{
    #[Authorize(requireLogin: true)]
    #[Authorize(requireGroups: [1, 2])]
    #[Authorize(callback: [MyObjectAuthorization::class, 'checkOwnership'])]
    public function editAction(MyObject $myObject): ResponseInterface
    {
        // Only accessible to logged-in users in groups 1 or 2 who own the object
        return $this->htmlResponse();
    }
}
Copied!

Authorization checks can also be combined within a single attribute:

#[Authorize(requireLogin: true, requireGroups: [1, 2])]
public function adminAction(): ResponseInterface
{
    return $this->htmlResponse();
}
Copied!

Customizing the authorization denied response 

By default, the authorization check will throw a \TYPO3\CMS\Core\Http\PropagateResponseException with a HTTP 403 response. This response can be handled by the TYPO3 page error handler configured in site settings.

The PSR-14 event BeforeActionAuthorizationDeniedEvent can be used to provide a custom PSR-7 response, which will be returned by Extbase.

EXT:my_extension/Classes/EventListener/CustomAuthorizationResponseListener.php
namespace MyVendor\MyExtension\EventListener;

use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use TYPO3\CMS\Extbase\Authorization\AuthorizationFailureReason;
use TYPO3\CMS\Extbase\Event\Mvc\BeforeActionAuthorizationDeniedEvent;

final class CustomAuthorizationResponseListener
{
    public function __construct(
        private readonly ResponseFactoryInterface $responseFactory,
        private readonly StreamFactoryInterface $streamFactory
    ) {}

    public function __invoke(BeforeActionAuthorizationDeniedEvent $event): void
    {
        // Customize response based on failure reason
        $message = match ($event->getFailureReason()) {
            AuthorizationFailureReason::NOT_LOGGED_IN => 'Please log in to access this page',
            AuthorizationFailureReason::MISSING_GROUP => 'You do not have permission to access this page',
            AuthorizationFailureReason::CALLBACK_DENIED => 'Access to this resource is denied',
        };

        $response = $this->responseFactory->createResponse()
            ->withHeader('Content-Type', 'text/html; charset=utf-8')
            ->withStatus(403)
            ->withBody($this->streamFactory->createStream($message));

        $event->setResponse($response);
    }
}
Copied!

Security Considerations 

Impact 

Extension authors can now implement secure, declarative authorization checks for Extbase controller actions using the #[Authorize] attribute.