Creating your own security attribute with Symfony
Tags: php, symfony, attribute, security
Published: 2024-06-29 10:02:54

### Introduction

[Symfony security](https://symfony.com/doc/current/security.html) has a great feature to check authorization named Voters. [Voters](https://symfony.com/doc/current/security/voters.html) are services which allows you to check user authorization in the way you need.
In this article, I would like to share with you another way to define custom authorization checking: _Create our custom security attribute_
Let’s imagine we store user roles out of the User class (the one which implements the [Symfony UserInterface](https://github.com/symfony/symfony/blob/7.1/src/Symfony/Component/Security/Core/User/UserInterface.php)) and we have a controller on which we only want to allow ROLE_SUPERUSER users.

### Creating the Attribute
Let’s create a simple attribute which receive the roles we want to allow

```php
#[\Attribute]
class IsUserGranted
{
    public function __construct(public readonly array $roles){ }
}
```

### Creating the Event Subscriber

Now, let’s create a subscriber which will keep listening to _KernelEvents::CONTROLLER_ARGUMENTS_.

```php
class UserGrantedSubscriber implements EventSubscriberInterface
{

    public static function getSubscribedEvents(): array
    {
        return [
            KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments']
        ];
    }

    public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
    {
        $attrs = $event->getAttributes();

        /** @var IsUserGranted[]|null $isUserGrantedAttrs */
        $isUserGrantedAttrs = $attrs[IsUserGranted::class] ?? null;

        if(!$isUserGrantedAttrs){
            return;
        }

        $isUserGranted = $isUserGrantedAttrs[0];
        // Some code to get user roles from outside the User
        $roles = ['......'];

        // ------------------------------------

        $commonRoles = array_intersect(
            $roles,
            $isUserGranted->roles
        );

        if(count($commonRoles) === 0){
            throw new AccessDeniedException('You are not granted to get this resource');
        }
    }
}
```
After the **KernelEvents::CONTROLLER_ARGUMENTS** event is received, the function **onKernelControllerArguments** is executed. The function gets an object of class **ControllerArgumentsEvent** as a parameter. This object gives us access to controller attributes through the **getAttributes** method. If the **IsUserGranted** attribute is present, the user roles will be compared against granted roles and, if there are no matching roles, an **AccessDeniedException** will be thrown.

### Protecting a Controller

The last step is to use our new attribute in a controller. Let’s create a controller and set it our attribute:

```php
#[IsUserGranted(roles: ["ROLE_SUPERUSER"])]
class AdminController extends AbstractController
{
    #[Route('/admin/domains', name: 'get_admin_domains', methods: ['GET'])]
    public function getAdminDomains(): JsonResponse
    {
        return new JsonResponse([
            'ares.com',
            'atila.com',
        ], Response::HTTP_OK);
    }
}
```
And that’s all. If we try to access the resource with a user who does not have a **ROLE_SUPERUSER**, access will be denied.

### Conclusion

This is another way you can consider if you want to add an extra authorization layer. As I've said in the introduction, this way can be useful when you want to apply the same authorization logic to all controller actions but the [IsGranted](https://symfony.com/doc/current/security/expressions.html) attribute does not match your needs.

> PHP attributes combined with the PHP reflection capabilities are a fantastic combination that allow developers to add extra behavior to classes in a really descriptive way. In my last published book, I use attributes to mark services as API operations, mark operations as background etc. If you want to know more, you can find the book here: [https://amzn.eu/d/3eO1DDi].