Skip to content

Update "User authentication" customization example (5.0)#3087

Open
adriendupuis wants to merge 26 commits into
5.0from
user_auth_5.0
Open

Update "User authentication" customization example (5.0)#3087
adriendupuis wants to merge 26 commits into
5.0from
user_auth_5.0

Conversation

@adriendupuis

@adriendupuis adriendupuis commented Mar 13, 2026

Copy link
Copy Markdown
Contributor
Question Answer
JIRA Ticket IBX-11465
Versions 5.0
Edition All

Update for DXP 5.0 SF 7.3 since ibexa/core#411 and other big changes

Related RP: #3086

Checklist

  • Text renders correctly
  • Text has been checked with vale
  • Description metadata is up to date
  • Redirects cover removed/moved pages
  • Code samples are working
  • PHP code samples have been fixed with PHP CS fixer
  • Added link to this PR in relevant JIRA ticket or code PR

@github-actions

Copy link
Copy Markdown

Preview of modified files

Preview of modified Markdown:

Comment on lines +33 to +34
$ibexaUser = $this->userService->loadUserByLogin($userLogin);
$event->getAuthenticationToken()->setUser(new User($ibexaUser));

@adriendupuis adriendupuis Mar 13, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could use Ibexa\Core\MVC\Symfony\Security\User\UsernameProvider::loadUserByIdentifier() to get the repo user already wrapped:

Suggested change
$ibexaUser = $this->userService->loadUserByLogin($userLogin);
$event->getAuthenticationToken()->setUser(new User($ibexaUser));
$ibexaUser = $this->userNameProvider->loadUserByIdentifier($userLogin);
$event->getAuthenticationToken()->setUser($ibexaUser);

See #3088 for complete integration of this idea.

Pros:

  • Relies on the user provider it replaces

Cons:

  • Hides Ibexa\Core\MVC\Symfony\Security\User usage

it's still out of Contracts namespace.

@adriendupuis adriendupuis marked this pull request as ready for review March 13, 2026 12:36
@wizhippo

Copy link
Copy Markdown
Contributor

Is the SecurityEvents::INTERACTIVE_LOGIN the correct event now? I believe this fires after https://github.com/ibexa/core/blob/main/src/lib/MVC/Symfony/Security/Authentication/EventSubscriber/OnAuthenticationTokenCreatedRepositoryUserSubscriber.php which sets the repository user.

Also I see no logic anymore that sets a UserWrapped for the InteractiveLogin event to use.

I have a working method for myself now. That is to listen for a "AuthenticationTokenCreatedEvent" of our own and create and set a wrapped user there. Also need to make sure that your custom userprovider refreshes as wrapped user properly considering both the apiUser and the customUser

Not sure if this helps you at all or not. Also note i included a switchuserlistenr here too as it is broken too by default and will not work with wrapped users otherwise.

I have removed some login from the below to post here so code may not run as is.

<?php

declare(strict_types=1);

namespace App\Security\EventSubscriber;

use App\Entity\AppUser;
use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\Repository;
use Ibexa\Contracts\Core\Repository\Values\User\User;
use Ibexa\Core\MVC\Symfony\Security\UserWrapped;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;

final readonly class AuthenticationTokenCreatedSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private Repository $repository,
        private PermissionResolver $permissionResolver,
    ) {
    }

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

    public function onAuthenticationTokenCreated(AuthenticationTokenCreatedEvent $event): void
    {
        $user = $event->getAuthenticatedToken()->getUser();

        if (!$user instanceof AppUser) {
            return;
        }

        $apiUser = $user->getIbexaUserId() ? $this->repository->sudo(
            function (Repository $repository) use ($user) {
                try {
                    return $repository->getUserService()->loadUser($user->getIbexaUserId());
                } catch (\Exception) {
                    return null;
                }
            }
        ) : $this->repository->sudo(
            function (Repository $repository) use ($user) {
                try {
                    return $repository->getUserService()->loadUserByLogin($user->getUserIdentifier());
                } catch (\Exception) {
                    return null;
                }
            }
        );

        if ($apiUser instanceof User) {
            $this->permissionResolver->setCurrentUserReference($apiUser);
            $event->getAuthenticatedToken()->setUser(new UserWrapped($user, $apiUser));
        }
    }
}

In UserProvider

    public function refreshUser(UserInterface $user): UserInterface
    {
        if ($user instanceof UserWrapped && $user->getWrappedUser() instanceof AppUser) {
            $user = $this->loadUserByIdentifier($user->getWrappedUser()->getUserIdentifier());
        } else {
            $user = $this->loadUserByIdentifier($user->getUserIdentifier());
        }

        /** @var AppUser $user */
        $apiUser = $user->getIbexaUserId() ? $this->repository->sudo(
            fn (Repository $repository) => $repository->getUserService()->loadUser($user->getIbexaUserId())
        ) : $this->repository->sudo(
            fn (Repository $repository) => $repository->getUserService()->loadUserByLogin(
                $user->getUserIdentifier()
            )
        );

        return new UserWrapped($user, $apiUser);
    }
<?php

declare(strict_types=1);

namespace App\Security\EventSubscriber;

use App\Entity\AppUser;
use Doctrine\ORM\EntityManagerInterface;
use Ibexa\Contracts\Core\Repository\Repository;
use Ibexa\Core\MVC\Symfony\Security\UserWrapped;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\SecurityEvents;

final readonly class SwitchUserEventSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private Repository $repository,
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            SecurityEvents::SWITCH_USER => ['onSwitchUser', 0],
        ];
    }

    public function onSwitchUser(SwitchUserEvent $event): void
    {
        $token = $event->getToken();

        if (
            $token instanceof SwitchUserToken
            && $token->getUser() instanceof AppUser
            && $token->getOriginalToken()->getUser() instanceof UserWrapped
            && $token->getOriginalToken()->getUser()->getWrappedUser() instanceof AppUser
        ) {
            $currentUsername = $token->getOriginalToken()->getUserIdentifier();
            $targetUsername = $token->getUserIdentifier();

            $user = $token->getUser();

            $apiUser = $user->getIbexaUserId() ? $this->repository->sudo(
                function (Repository $repository) use ($user) {
                    try {
                        return $repository->getUserService()->loadUser($user->getIbexaUserId());
                    } catch (\Exception) {
                        return null;
                    }
                }
            ) : $this->repository->sudo(
                function (Repository $repository) use ($user) {
                    try {
                        return $repository->getUserService()->loadUserByLogin($user->getUserIdentifier());
                    } catch (\Exception) {
                        return null;
                    }
                }
            );

            $wrappedUser = new UserWrapped($user, $apiUser);
            $token->setUser($wrappedUser);
        }

        $event->getRequest()->getSession()->migrate();
    }
}

@sonarqubecloud

Copy link
Copy Markdown

@konradoboza konradoboza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work, thank you for looking into this!

One idea worth mentioning is concept of hooking into security events and setting a repository user, like here

adriendupuis and others added 2 commits June 8, 2026 11:19
Co-authored-by: Konrad Oboza <konrad.oboza@ibexa.co>
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

code_samples/ change report

Before (on target branch)After (in current PR)

code_samples/user_management/in_memory/config/packages/security.yaml


code_samples/user_management/in_memory/config/packages/security.yaml

docs/users/user_authentication.md@48:``` yaml hl_lines="4 9-14 18-20 26"
docs/users/user_authentication.md@49:[[= include_file('code_samples/user_management/in_memory/config/packages/security.yaml') =]]
docs/users/user_authentication.md@50:```

001⫶security:
002⫶ password_hashers:
003⫶ # The in-memory provider requires an encoder
004❇️ Symfony\Component\Security\Core\User\InMemoryUser: plaintext
005⫶ Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
006⫶
007⫶ # https://symfony.com/doc/current/security.html#b-configuring-how-users-are-loaded
008⫶ providers:
009❇️ in_memory:
010❇️ memory:
011❇️ users:
012❇️ from_memory_user: { password: from_memory_pass, roles: [ 'ROLE_USER' ] } # Mapped to `generic_customer` user
013❇️ from_memory_forgotten: { password: from_memory_anonym, roles: [ 'ROLE_USER' ] } # Not mapped so `anonymous` user is loaded
014❇️ from_memory_admin: { password: from_memory_publish, roles: [ 'ROLE_USER' ] } # Mapped to `admin` user
015⫶ ibexa:
016⫶ id: ibexa.security.user_provider
017⫶ # Chaining in_memory and ibexa user providers
018❇️ chained:
019❇️ chain:
020❇️ providers: [ in_memory, ibexa ]
021⫶
022⫶ firewalls:
023⫶ # …
024⫶ ibexa_front:
025⫶ pattern: ^/
026❇️ provider: chained
027⫶ user_checker: Ibexa\Core\MVC\Symfony\Security\UserChecker
028⫶ context: ibexa
029⫶ form_login:
030⫶ enable_csrf: true
031⫶ login_path: login
032⫶ check_path: login_check
033⫶ custom_authenticators:
034⫶ - Ibexa\PageBuilder\Security\EditorialMode\FragmentAuthenticator
035⫶ entry_point: form_login
036⫶ logout:
037⫶ path: logout


code_samples/user_management/in_memory/config/services.yaml


code_samples/user_management/in_memory/config/services.yaml

docs/users/user_authentication.md@55:``` yaml
docs/users/user_authentication.md@56:[[= include_file('code_samples/user_management/in_memory/config/services.yaml') =]]
docs/users/user_authentication.md@57:```

001⫶services:
002⫶ App\EventSubscriber\InteractiveLoginSubscriber:
003⫶ arguments:
004⫶ $userMap:
005⫶ from_memory_user: generic_customer
006⫶ from_memory_admin: admin


code_samples/user_management/in_memory/src/EventSubscriber/InteractiveLoginSubscriber.php


code_samples/user_management/in_memory/src/EventSubscriber/InteractiveLoginSubscriber.php

docs/users/user_authentication.md@38:``` php
docs/users/user_authentication.md@39:[[= include_file('code_samples/user_management/in_memory/src/EventSubscriber/InteractiveLoginSubscriber.php') =]]
docs/users/user_authentication.md@40:```

001⫶<?php declare(strict_types=1);
002⫶
003⫶namespace App\EventSubscriber;
004⫶
005⫶use Ibexa\Contracts\Core\Repository\UserService;
006⫶use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
007⫶//use Ibexa\Core\MVC\Symfony\Security\User;
008⫶use Ibexa\Core\MVC\Symfony\Security\UserWrapped;
009⫶use Symfony\Component\EventDispatcher\EventSubscriberInterface;
010⫶use Symfony\Component\Security\Core\User\InMemoryUser;
011⫶use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
012⫶use Symfony\Component\Security\Http\SecurityEvents;
013⫶
014⫶final readonly class InteractiveLoginSubscriber implements EventSubscriberInterface
015⫶{
016⫶ /** @param array<string, string> $userMap */
017⫶ public function __construct(
018⫶ private readonly ConfigResolverInterface $configResolver,
019⫶ private readonly UserService $userService,
020⫶ private readonly array $userMap = [],
021⫶ ) {
022⫶ }
023⫶
024⫶ public static function getSubscribedEvents(): array
025⫶ {
026⫶ return [
027⫶ SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
028⫶ ];
029⫶ }
030⫶
031⫶ public function onInteractiveLogin(InteractiveLoginEvent $event): void
032⫶ {
033⫶ $tokenUser = $event->getAuthenticationToken()->getUser();
034⫶ if (!$tokenUser instanceof InMemoryUser) {
035⫶ return;
036⫶ }
037⫶ $userIdentifier = $event->getAuthenticationToken()->getUserIdentifier();
038⫶ $ibexaUser = null;
039⫶ if (array_key_exists($userIdentifier, $this->userMap)) {
040⫶ $ibexaUser = $this->userService->loadUserByLogin($this->userMap[$userIdentifier]);
041⫶ }
042⫶ if (null === $ibexaUser) {
043⫶ $anonymousUserId = (int)$this->configResolver->getParameter('anonymous_user_id');
044⫶ $ibexaUser = $this->userService->loadUser($anonymousUserId);
045⫶ }
046⫶ //$event->getAuthenticationToken()->setUser(new User($ibexaUser));
047⫶ $event->getAuthenticationToken()->setUser(new UserWrapped($tokenUser, $ibexaUser));
048⫶ }
049⫶}

Download colorized diff

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants