Skip to content

Security

Authentication


Authenticators

Application modules can provide multiple authenticators which can be used to authenticate the user.

Authenticators are executed during the request event in the Amarant\Framework\EventSubscriber\Security\AuthenticationEventSubscriber event subscriber which is registered with a priority of Amarant\Framework\Enum\EventPriorityEnum::AUTHENTICATION.

Authenticators must implement the following interface:

<?php

declare(strict_types=1);

namespace Amarant\Framework\Contract\Security\Authenticator;

use Amarant\Framework\Contract\Application\Request\RequestContextInterface;

interface AuthenticatorInterface
{
    public function supports(RequestContextInterface $requestContext): bool;

    public function authenticate(RequestContextInterface $requestContext): AuthenticationResultInterface;
}

If an authenticator can specify a URL which the user should be redirected to, for example to a login page, it must implement the following interface:

<?php

declare(strict_types=1);

namespace Amarant\Framework\Contract\Security\Authenticator;

use Amarant\Framework\Contract\Application\Request\RequestContextInterface;

interface StartAuthenticationUrlAwareAuthenticatorInterface extends AuthenticatorInterface
{
    public function getAuthenticationUrl(RequestContextInterface $requestContext): ?string;
}

If a string value is returned, a redirect response will be returned using the value.

Dependency injection tags

Amarant\Framework\Enum\DiEnum::SECURITY_AUTHENTICATOR

Supports

This method should respond with true if it can immediately authenticate the user. This means if an authenticator is using a login form, it should only respond with true when the user has submitted data for authentication on a particular URL.

For example:

<?php

#[Override] public function supports(RequestContextInterface $requestContext): bool
{
    $request = $requestContext->getRequest();
    return $requestContext->getContext()->getArea() === AreaInterface::AREA_BACKEND &&
        $request->isMethod(Request::METHOD_POST) &&
        $request->getPathInfo() === '/user/account/login';
}

This authenticator will only be executed if the current application area is backend (back office), the request method is POST and the path of the request is /user/account/login.

On the other hand, if an API request is made, we should support every request using an authorization header.

For example:

<?php

#[Override] public function supports(RequestContextInterface $requestContext): bool
{
    return $requestContext->getContext()->getArea() === AreaInterface::AREA_BACKEND &&
            $requestContext->isApi() &&
            \str_starts_with(
                $requestContext->getRequest()->headers->get('Authorization', ''),
                'Bearer '
            );
}

Authenticate

If an authenticator supports authentication, it must return a result object implementing the following interface:

<?php

declare(strict_types=1);

namespace Amarant\Framework\Contract\Security\Authenticator;

use Amarant\Framework\Contract\Security\Identity\IdentityInterface;
use Amarant\Framework\Exception\BaseException;
use Symfony\Component\HttpFoundation\Response;

interface AuthenticationResultInterface
{
    public function getIdentity(): ?IdentityInterface;

    public function getException(): ?BaseException;

    public function getResponse(): ?Response;
}

The authentication event subscriber executes all supporting authenticators and checks this object.

If getIdentity does not return null, the authentication is considered to be successful. The identity is saved to security storage. If getResponse does not return null, it sets the response to the request event and stops further event subscribers. Finally, it returns (exits the event subscriber handling method).

The event subscriber then checks if the authenticator implements StartAuthenticationUrlAwareAuthenticatorInterface. If it does, and the start authentication URL hasn't been set yet, it calls the getAuthenticationUrl method and sets the start authentication URL, if not null.

If getException does not return null, the exception is added to a list of exceptions to be used later on.

Important

It only matters if the authenticator implements StartAuthenticationUrlAwareAuthenticatorInterface. The getAuthenticationUrl can return null if its logic decides so, depending on the request context.

If any of the authenticator results had an exception set, the request event is stopped. If the request is an API request, Amarant\Framework\Exception\SecurityException::becauseAuthenticationFailed is thrown, otherwise exception details are added to session error messages.

Finally, an existing identity is retrieved from supporting identity providers and if found, it's saved to security storage.

Note

Security storage is a simple object that stores the current user identity.
It implements Amarant\Framework\Contract\Security\SecurityStorageInterface.

Identities


Identities are objects that implement the following interface:
Amarant\Framework\Contract\Security\Identity\IdentityInterface

They're used to represent users, in a storage-agnostic way, and can provide access to real user objects, for example a data model.

An identity can have a storage attached to it. The storage in this case is temporary, like a session.
It can be used to store settings, like the user's locale, or flash messages like notices and errors.

Such an identity must implement the following interface:
Amarant\Framework\Contract\Security\Identity\StorageAwareIdentityInterface

The following identities already exist in the framework module:

Amarant\Framework\Security\Identity\Identity
By default, not used by any of the core modules, but can be freely used by application modules.

Amarant\Framework\Security\Identity\StorageAwareIdentity
Same as the above, but with a storage attached to it.

Amarant\Framework\Security\Identity\UserAccountIdentity
This identity is used for backend user accounts.

Guest identity

If no other identity provider provides an identity, the Amarant\Framework\Security\Identity\Provider\GuestIdentityProvider will provide a guest identity.

This identity will be an instance of Amarant\Framework\Security\Identity\StorageAwareIdentity.

Its type will be guest with a global guest access scope and a random id.

Important

The guest provider has a very low tag priority to allow other providers to run before it. Also, this provider is supported only in the frontend application area.

Identity providers


Identity providers are used to retrieve an identity that was previously authenticated. For example, a provider might use a http-only cookie to store the user's (JWT) token. On each subsequent request it would verify the token and if valid, it would return the user's identity object.

Identity providers must implement the following interface:

<?php

declare(strict_types=1);

namespace Amarant\Framework\Contract\Security\Identity;

interface IdentityProviderInterface
{
    public function supports(): bool;

    public function getIdentity(): ?IdentityInterface;
}

Providers can also store an existing identity. This usually means that a provider creates a http-only cookie and stores the user's (JWT) token in it.

Such a provider must implement the following interface:

<?php

declare(strict_types=1);

namespace Amarant\Framework\Contract\Security\Identity;

interface PersistentIdentityProviderInterface extends IdentityProviderInterface
{
    public function canStore(IdentityInterface $identity): bool;

    public function store(IdentityInterface $identity): void;

    public function canDiscard(IdentityInterface $identity): bool;

    public function discard(IdentityInterface $identity): void;
}

The store and discard methods will only be called if the corresponding methods canStore and canDiscard return true.

Dependency injection tags

Amarant\Framework\Enum\DiEnum::SECURITY_IDENTITY_PROVIDER

Firewall


The http application is secured using firewall rules. Each application module can specify their own rules.

Rules are evaluated during the request event in the Amarant\Framework\EventSubscriber\Security\FirewallEventSubscriber event subscriber which is registered with a priority of Amarant\Framework\Enum\EventPriorityEnum::FIREWALL.

Create firewall rules


Check the annotations to know more.

Vendor/ModuleName/etc/security.php
<?php

declare(strict_types=1);

use Amarant\Framework\Contract\Application\ContextInterface;
use Amarant\Framework\Contract\Security\Firewall\FirewallRuleCollectionInterface;
use Amarant\Framework\Security\Firewall\FirewallRule;
use Amarant\Framework\Security\SecurityConfiguratorInterface;

return new class () implements SecurityConfiguratorInterface {
    public function configure(
        ContextInterface $context,
        FirewallRuleCollectionInterface $firewallRuleCollection
    ): void {
        $firewallRuleCollection
            ->addRule(
                FirewallRule::create(
                    pathExpression: '/^\/some-path/',   // (1)
                    accessScopes: ['super_scope'],      // (2)
                    types: ['some_identity_type'],      // (3)
                    fallthrough: false,                 // (4)
                    priority: 0                         // (5)
                )
            );
    }
};
  1. Regex to match with the request path.
  2. Access scopes a user should have to be able to access the path.
  3. Identity types the user should have to be able to access the path.
  4. If set to false, no other rules will be evaluated after this rule is matched.
  5. The priority of the rule. Rules of higher priority are evaluated first.

Important

The pathExpression value is used as-is to call preg_match. Format and/or quote this value properly so it can be used with preg_match directly.

Note

To add rules for a particular application area, place this file in frontend or backend subdirectory instead:

Vendor/ModuleName/etc/frontend/security.php
Vendor/ModuleName/etc/backend/security.php

Security manager


Security manager is used to get the current user identity and to check if the user has appropriate rights to do something or to access some resources.

To use the security manager, inject the following:
Amarant\Framework\Contract\Security\SecurityManagerInterface

Checking for access scopes


Calling the has method checks if the current user has all the access scopes given in the method's accessScopes parameter.

<?php

if ($securityManager->has(['some_scope_a', 'some_scope_b'])) {
    // user has both 'scope_a' and 'scope_b' access scopes
}

Checking for identity type


Calling the isType method checks if the current user's identity type is the same type as given in the method's type parameter.

<?php

if ($securityManager->isType('some_type')) {
    // user's identity type is 'some_type'
}

Checking for data scope access


Calling the canAccessDataScope and canAccessDataScopes checks if the user has access to all the data scopes given in the method's scopeValue or scopeValues parameters.

<?php

if ($securityManager->canAccessDataScope('some_data_scope_a')) {
    // user has access to 'some_data_scope_a' data scope
}

if ($securityManager->canAccessDataScopes(['some_data_scope_a', 'some_data_scope_b'])) {
    // user has access to both 'some_data_scope_a' and 'some_data_scope_b' data scopes
}

Checking if user's identity type is any of types


Calling the isAnyOfTypes method checks if the current user's identity type is any of the types given in the method's types parameter.

<?php

if ($securityManager->isAnyOfTypes(['some_type_a', 'some_type_b'])) {
    // user's identity type is 'some_type_a' or 'some_type_b'
}

Checking if access is granted


Calling the isGranted method checks if the current user is allowed to perform actions on a subject.

<?php

if ($securityManager->isGranted($valueOrObject, ['attribute_a', 'attribute_b'])) {
    // user is allowed to perform on the subject $valueOrObject
}

These checks are evaluated by security guards. The only and default strategy at the moment is absolute. This means all guards that support the subject and attributes, must grant access for the isGranted to return true.

Guards


Guards are used to grant access using a given subject and a list of attributes.

A security guard must implement the following interface:

<?php

declare(strict_types=1);

namespace Amarant\Framework\Contract\Security;

interface SecurityGuardInterface
{
    /**
     * @param string[] $attributes
     */
    public function supports(mixed $subject, array $attributes, SecurityManagerInterface $securityManager): bool;

    /**
     * @param string[] $attributes
     */
    public function isGranted(mixed $subject, array $attributes, SecurityManagerInterface $securityManager): bool;
}

Examples

Amarant\Sales\Security\Guard\CustomerCartGuard
Amarant\Sales\Security\Guard\ActiveCartGuard

Dependency injection tags

Amarant\Framework\Enum\DiEnum::SECURITY_GUARD

CSRF storage


To work with CSRF storage, use the:
Amarant\Framework\Contract\Security\CsrfStorageInterface

From a presentation layer template, use the function csrf_token to get or create a token by name.

Password generation


To generate passwords, use the:
Amarant\Framework\Security\PasswordGenerator

Cryptography


JWT

To work with JWT tokens, use the:
Amarant\Framework\Contract\Security\Jwt\TokenManagerInterface

Creating tokens

Use the create method and specify a payload and headers.

Example

Amarant\Framework\Security\UserAccountFacade::createToken

Validating tokens

Use the load method and specify a token as a string.
Result will be an object of type Amarant\Framework\Security\Jwt\Jwt that can be used to check if the token is valid and to access its data.

Example

Amarant\Framework\Security\UserAccountFacade::extractToken

Encryption

To encrypt and decrypt data, use the:
Amarant\Framework\Contract\Security\Cryptography\EncryptorInterface

Under the hood, the default encryptor implementation uses sodium_crypto_secretbox and sodium_crypto_secretbox_open.

Encrypted values are prefixed using: Amarant\Framework\Contract\Security\Cryptography\EncryptorInterface::PREFIX.