OroCustomerBundle
OroCustomerBundle enables B2B-customer-related features in Oro applications and provides UI to manage B2B customers, customers groups, customer users, and customer user roles in the back-office and the storefront UI.
The bundle also allows back-office administrators to configure B2B-customer-related settings in the system configuration UI for the entire system, individual organizations, and websites.
Bundle Responsibilities
OroCustomerBundle is responsible for:
Customer user CRUD
Assigning roles to customer users
Activating and deactivating customer users
Sending welcome emails
Editing password and automatically generating password for a new customer user
Configure Frontend Permissions (ACL)
OroCustomerBundle extends security model of OroSecurityBundle
for entities which should be accessible for customer users in the storefront. It adds few new fields to ownership configuration of entities.
The example of the frontend permissions configuration for entity is provided below.
#[ORM\Entity]
#[Config(
defaultValues: [
'ownership' => [
'frontend_owner_type' => 'FRONTEND_USER',
'frontend_owner_field_name' => 'customerUser',
'frontend_owner_column_name' => 'customer_user_id',
'frontend_customer_field_name' => 'customer',
'frontend_customer_column_name' => 'customer_id'
],
'security' => [
'type' => 'ACL',
'group_name' => 'commerce'
]
]
)]
class SomeEntity implements ExtendEntityInterface
{
/**
* @var Customer
*/
#[ORM\ManyToOne(targetEntity: 'Oro\Bundle\CustomerBundle\Entity\Customer')]
#[ORM\JoinColumn(name: 'customer_id', referencedColumnName: 'id', onDelete: 'SET NULL')]
protected $customer;
/**
* @var CustomerUser
*/
#[ORM\ManyToOne(targetEntity: 'Oro\Bundle\CustomerBundle\Entity\CustomerUser')]
#[ORM\JoinColumn(name: 'customer_user_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
protected $customerUser;
...
}
Configure Anonymous Customer User
Anonymous customer user functionality consists of the sections below.
AnonymousCustomerUserToken
Oro\Bundle\CustomerBundle\Security\Token\AnonymousCustomerUserToken is the token class that is extended from AnonymousToken
. It is tied with the CustomerVisitor
entity class which persisted anonymous customer user data for later use. Besides it, the token stores the info taken from the visitor_id
and session_id
cookies.
$token = new AnonymousCustomerUserToken(
$customerVisitor, //instanceof UserInterface
$this->rolesProvider->getRoles(), // optional - array of roles
$organization // optional - an organization
);
The AnonymousCustomerUserToken
is created in the createToken
method of Oro\Bundle\CustomerBundle\Security\AnonymousCustomerUserAuthenticationListener.
CustomerVisitor Entity
The Oro\Bundle\CustomerBundle\Entity\CustomerVisitor class has the following properties:
id
lastVisit - tracks guest last visit datetime
sessionId - a unique identifier
customerUser - one-to-one relation to
CustomerUser
entity. Used to retrieve the customer info from the token.
The session id property is generated through Doctrine PrePersist
Lifecycle Event:
$this->sessionId = bin2hex(random_bytes(10));
Authenticator
Note
See How to Write a Custom Authenticator for more details on the custom authenticator.
The Oro\Bundle\CustomerBundle\Security\AnonymousCustomerUserAuthenticationListener class represents a significant evolution in the authentication process of the storefront’s anonymous users in Symfony 6.4. This class replaces the traditional combination of a Listener and an Authentication Provider, streamlining the process with advanced techniques and new methodologies.
Functionality and Workflow
Support Check: The supports(Request $request): bool method determines if the current request should be authenticated as a customer visitor. This involves checking the request type and the presence of an existing token in the token storage.
Authentication Process: The authenticate(Request $request): Passport method is responsible for the core authentication process. It retrieves the current website and its associated organization, ensuring that they are valid and exist. It then creates an
AnonymousSelfValidatingPassport
with anAnonymousCustomerUserBadge
, which is built using the credentials from the customer visitor cookie.Token Creation: The createToken(Passport $passport, string $firewallName): TokenInterface method generates an
AnonymousCustomerUserToken
. This token is populated with user, roles, organization data, and holds aCustomerVisitor
object. It ensures the token is saved in the token storage for future use.Authentication Success and Failure: The methods onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response and onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response handle the outcomes of the authentication process, logging the appropriate information.
Credential Management: The saveCredentials(Request $request, Passport $passport): void method manages the credentials, setting the necessary attributes and cookie information in the request.
Visitor Authentication: The getVisitor(string $credentials): CustomerVisitor method is a crucial component of the authenticator. It decodes the credentials and retrieves or creates a
CustomerVisitor
entity using theCustomerVisitorManager
.
This authenticator integrates seamlessly with Symfony’s security system, offering a more efficient and streamlined approach to handling anonymous customer user authentication. The use of badges, passports, and a modernized token system enhances the security and robustness of the process, aligning it with contemporary web application standards.
If the authentication of AnonymousCustomerUserToken
object is successful, you need to update cookie using the lifetime parameter, oro_customer.customer_visitor_cookie_lifetime_days
.
By default, this param is 30 days, and it is accessible through the System > Configuration > Commerce > Customer > Customer User section on the global and organization levels:
const COOKIE_ATTR_NAME = '_security_customer_visitor_cookie';
const COOKIE_NAME = 'customer_visitor';
$cookieLifetime = $this->configManager->get('oro_customer.customer_visitor_cookie_lifetime_days');
$cookieLifetime = $cookieLifetime * Configuration::SECONDS_IN_DAY;
$request->attributes->set(
self::COOKIE_ATTR_NAME,
new Cookie(
self::COOKIE_NAME,
base64_encode(json_encode([$visitor->getId(), $visitor->getSessionId()])),
time() + $cookieLifetime
)
);
The Oro\Bundle\CustomerBundle\Security\Listener\CustomerVisitorCookieResponseListener listens kernel.response
events. If the request has an _security_customer_visitor_cookie
attribute, it sets a cookie to it.
AnonymousCustomerUserFactory
The Oro\Bundle\CustomerBundle\DependencyInjection\Security\AnonymousCustomerUserFactory class ties Oro\Bundle\CustomerBundle\Security\AnonymousCustomerUserAuthenticationListener.
Also, it defines the update_latency
configuration option. It helps prevent sending too many requests to the database when updating the lastVisit
datetime of the AnonymousCustomerUser
entity. Its default value is set in the DI container and is expressed in seconds:
oro_customer.anonymous_customer_user.update_latency: 600 # 10 minutes in seconds
Firewall Configuration
To activate anonymous customer user functionality for some routes or apply it to the existing ones, define it in the security
section with the anonymous_customer_user: true
property:
security:
firewalls:
frontend:
anonymous_customer_user: true
In this example, we enable guest functionality for the application storefront.
Guest Customer User
Guest Customer User is a customer user with the following DB properties:
confirmed
=false
enabled
=false
is_guest
=true
The Oro\Bundle\CustomerBundle\Entity\GuestCustomerUserManager class has a logic of creation Guest Customer User
.
It is used for creating some business products under Anonymous Customer, like RFQ or Order, in the storefront.
For example, when creating one of the mentioned products, we can tie it with Guest Customer info taken from AnonymousCustomerUserToken
token:
// $request is a some Request object
$token = $this->tokenAccessor->getToken();
if ($token instanceof AnonymousCustomerUserToken) {
$visitor = $token->getVisitor();
$user = $visitor->getCustomerUser();
if ($user === null) {
$user = $this->guestCustomerUserManager
->generateGuestCustomerUser(
[
'email' => $request->getEmail(),
'first_name' => $request->getFirstName(),
'last_name' => $request->getLastName(),
...
]
);
$visitor->setCustomerUser($user);
}
$request->setCustomerUser($user);
}
Ownership
When using guest functionality for some business products, you should specify their owner. With Oro\Bundle\CustomerBundle\Entity\CustomerVisitorOwnerAwareInterface and Oro\Bundle\CustomerBundle\Owner\AnonymousOwnershipDecisionMaker, you can do it using the following conditions:
entity should implement
CustomerVisitorOwnerAwareInterface
token should be instance of
AnonymousCustomerUserToken
entity should contain
CustomerVisitor
and it should equal the current visitor in the session
Configure Guest Access and Permissions
When we implement guest functionality for some product, it should be tied with the related feature and added to system configuration on the global, organization, and website levels. By default, it should be disabled:
//.../DependencyInjection/Configuration.php
'guest_product_toggle' => ['type' => 'boolean','value' => false],
'guest_product_owner' => ['type' => 'string', 'value' => null]
#...Resources/config/oro/system_configuration.yml
system_configuration:
groups:
guest_product_section:
title: some.title
guest_product_owner_section:
title: some.title
fields:
guest_product:
data_type: boolean
type: Oro\Bundle\ConfigBundle\Form\Type\ConfigCheckbox
options:
label: some.title
tooltip: some.tooltip
guest_product_owner:
ui_only: true
data_type: string
type: Oro\Bundle\UserBundle\Form\Type\UserSelectType
options:
label: some.title
tooltip: some.tooltip
required: true
tree:
system_configuration:
commerce:
children:
sales:
children:
guest_product_section:
children:
- guest_product
guest_product_owner_section:
children:
- guest_product_owner
#...Resources/config/oro/features.yml
features:
guest_product_feature:
label: some.label
description: some.description
toggle: guest_product_toggle
Next, we should activate feature toggle voter in the DI configuration:
oro_bundle.voter.guest_product:
parent: oro_customer.voter.anonymous_customer_user
calls:
- [ setFeatureName, ['guest_product_feature'] ]
tags:
- { name: oro_featuretoggle.voter }
oro_bundle.voter.guest_customer_user:
parent: oro_customer.voter.customer_user
calls:
- [ setFeatureName, ['guest_product_feature'] ]
tags:
- { name: oro_featuretoggle.voter }
Sometimes, it is necessary to open some business entity or action for guests using ACL configuration.
So, to enable certain entities and actions for the Anonymous Customer User role by default, use the following code example:
#.../Migrations/Data/ORM/data/frontend_roles.yml
ANONYMOUS:
permissions:
entity|Oro\Bundle\SomeBundle\Entity\Some: [VIEW_BASIC, CREATE_BASIC, EDIT_BASIC, DELETE_BASIC]
action|some_action: [EXECUTE]
Once the application is installed, the predefined Non-Authenticated Visitors role will have the mentioned permissions/capabilities enabled.