Skip to content

User authentication

Authenticate user with multiple user providers

Symfony provides native support for multiple user providers. This makes it easier to integrate any kind of login handlers, including SSO and existing third party bundles (for example, FR3DLdapBundleHWIOauthBundleFOSUserBundle, or BeSimpleSsoAuthBundle).

However, to be able to use external user providers with Ibexa DXP, a valid Ibexa user needs to be injected into the repository. This is mainly for the kernel to be able to manage content-related permissions (but not limited to this).

Depending on your context, you either want to create and return an Ibexa user, or return an existing user, even a generic one.

Whenever a user is matched and authenticated, Symfony initiates an AuthenticationTokenCreatedEvent. Every service listening to this event receives an object containing the original security token, which holds the matched user, and a passport.

Then, it's up to a listener to retrieve an Ibexa user from the repository.

This Ibexa user can be:

  • embedded into Ibexa\Core\MVC\Symfony\Security\User while forgetting about the original user
  • wrapped into Ibexa\Core\MVC\Symfony\Security\UserWrapped with the original user if needed

Finally, the user is assigned back into the event's token for the rest of the process.

User mapping example

The following example uses the memory user provider, maps memory user to Ibexa repository user, and chains with the Ibexa user provider to be able to use both.

It's possible to customize the user class used by extending Ibexa\Core\MVC\Symfony\Security\EventListener\SecurityListener service, which defaults to Ibexa\Core\MVC\Symfony\Security\EventListener\SecurityListener.

You can override getUser() to return whatever user class you want, as long as it implements Ibexa\Core\MVC\Symfony\Security\UserInterface.

The following is an example of using the in-memory user provider:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# config/packages/security.yaml
security:
    providers:
        # Chaining in_memory and ibexa user providers
        chain_provider:
            chain:
                providers: [in_memory, ibexa]
        ibexa:
            id: ibexa.security.user_provider
        in_memory:
            memory:
                users:
                    # You will then be able to login with username "user" and password "userpass"
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
    # The "in memory" provider requires an encoder for Symfony\Component\Security\Core\User\User
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

Implement the listener

In the config/services.yaml file:

1
2
3
4
5
services:
    App\EventListener\InteractiveLoginListener:
        arguments: ['@ibexa.api.service.user']
        tags:
            - { name: kernel.event_subscriber }

Don't mix MVCEvents::INTERACTIVE_LOGIN event (specific to Ibexa DXP) and SecurityEvents::INTERACTIVE_LOGIN event (fired by Symfony security component).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php declare(strict_types=1);

namespace App\EventSubscriber;

use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
use Ibexa\Core\MVC\Symfony\Security\UserWrapped;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;

final readonly class AuthenticationTokenCreatedSubscriber implements EventSubscriberInterface
{
    /** @param array<string, string> $userMap */
    public function __construct(
        private readonly ConfigResolverInterface $configResolver,
        private readonly UserService $userService,
        private readonly array $userMap = [],
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            AuthenticationTokenCreatedEvent::class => ['onAuthenticationTokenCreated', 11],
        ];
    }

    public function onAuthenticationTokenCreated(AuthenticationTokenCreatedEvent $event): void
    {
        $token = $event->getAuthenticatedToken();
        $tokenUser = $token->getUser();
        if (!$tokenUser instanceof InMemoryUser) {
            return;
        }
        $userIdentifier = $token->getUserIdentifier();
        $ibexaUser = null;
        if (array_key_exists($userIdentifier, $this->userMap)) {
            $ibexaUser = $this->userService->loadUserByLogin($this->userMap[$userIdentifier]);
        }
        if (null === $ibexaUser) {
            $anonymousUserId = (int)$this->configResolver->getParameter('anonymous_user_id');
            $ibexaUser = $this->userService->loadUser($anonymousUserId);
        }
        $token->setUser(new UserWrapped($tokenUser, $ibexaUser));
    }
}

In config/packages/security.yaml, add the memory and chain user providers, store some in-memory users with their passwords in plain text and a basic role, set a plaintext password encoder for the memory provider's InMemoryUser, and configure the firewall to use the chain provider:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
security:
    password_hashers:
        # The in-memory provider requires an encoder
        Symfony\Component\Security\Core\User\InMemoryUser: plaintext
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'

    # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
    providers:
        in_memory:
            memory:
                users:
                    from_memory_user: { password: from_memory_pass, roles: [ 'ROLE_USER' ] } # Mapped to `generic_customer` user
                    from_memory_forgotten: { password: from_memory_anonym, roles: [ 'ROLE_USER' ] } # Not mapped so `anonymous` user is loaded
                    from_memory_admin: { password: from_memory_publish, roles: [ 'ROLE_USER' ] } # Mapped to `admin` user
        ibexa:
            id: ibexa.security.user_provider
        # Chaining in_memory and ibexa user providers
        chained:
            chain:
                providers: [ in_memory, ibexa ]

    firewalls:
        # …
        ibexa_front:
            pattern: ^/
            provider: chained
            user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker
            context: ibexa
            form_login:
                enable_csrf: true
                login_path: login
                check_path: login_check
            custom_authenticators:
                - Ibexa\PageBuilder\Security\EditorialMode\FragmentAuthenticator
            entry_point: form_login
            logout:
                path: logout

In the config/services.yaml file, declare the subscriber as a service to pass your user map. Since it implements the EventSubscriberInterface, it's automatically tagged as a kernel.event_subscriber. The config resolver and user service injections are auto-wired automatically.

1
2
3
4
5
6
services:
    App\EventSubscriber\AuthenticationTokenCreatedSubscriber:
        arguments:
            $userMap:
                from_memory_user: generic_customer
                from_memory_admin: admin

You can list the subscribers with the following command to check their order:

1
php bin/console debug:event-dispatcher AuthenticationTokenCreatedEvent

Notice that the example subscriber priority is 11 so it's executed before the Ibexa\Core\MVC\Symfony\Security\Authentication\EventSubscriber\OnAuthenticationTokenCreatedRepositoryUserSubscriber which set the Ibexa user as the current user.

From the back office, create the mapped users. For this example, create a new user with the login generic_customer and a random password so the mapping works correctly. This account can belong to either the Customers or the Anonymous users group.

You can now log in with an in-memory user. In the Symfony debug toolbar, you should see the in-memory user as this example uses UserWrapped.