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.
<?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)
)
);
}
};
- Regex to match with the request path.
- Access scopes a user should have to be able to access the path.
- Identity types the user should have to be able to access the path.
- If set to
false, no other rules will be evaluated after this rule is matched. - 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.
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.