Skip to content
Open
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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' ] }
from_memory_admin: { password: from_memory_publish, roles: [ 'ROLE_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
5 changes: 5 additions & 0 deletions code_samples/user_management/in_memory/config/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
App\EventSubscriber\InteractiveLoginSubscriber:
arguments:
$userMap:
from_memory_user: customer
from_memory_admin: admin
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types=1);

namespace App\EventSubscriber;

use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Core\MVC\Symfony\Security\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;

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

public static function getSubscribedEvents()
{
return [
SecurityEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin',
];
}

public function onInteractiveLogin(InteractiveLoginEvent $event): void
{
$tokenUser = $event->getAuthenticationToken()->getUser();
if ($tokenUser instanceof InMemoryUser) {
$userLogin = $this->userMap[$event->getAuthenticationToken()->getUserIdentifier()] ?? 'anonymous';
$ibexaUser = $this->userService->loadUserByLogin($userLogin);
$event->getAuthenticationToken()->setUser(new User($ibexaUser));

Check failure on line 34 in code_samples/user_management/in_memory/src/EventSubscriber/InteractiveLoginSubscriber.php

View workflow job for this annotation

GitHub Actions / Validate code samples (8.3)

App\EventSubscriber\InteractiveLoginSubscriber must not depend on Ibexa\Core\MVC\Symfony\Security\User (CodeSamples on IbexaNotAllowed)
Comment on lines +33 to +34
Copy link
Contributor Author

@adriendupuis adriendupuis Mar 13, 2026

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);

but it's still out of Contracts namespace.

}
}
}
107 changes: 24 additions & 83 deletions docs/users/user_authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,105 +6,46 @@

## Authenticate user with multiple user providers

Symfony provides native support for [multiple user providers]([[= symfony_doc =]]/security/user_providers.html).
Symfony provides native support for [multiple user providers]([[= symfony_doc =]]/security/user_providers.html).
This makes it easier to integrate any kind of login handlers, including SSO and existing third party bundles (for example, [FR3DLdapBundle](https://github.com/Maks3w/FR3DLdapBundle)[HWIOauthBundle](https://github.com/hwi/HWIOAuthBundle)[FOSUserBundle](https://github.com/FriendsOfSymfony/FOSUserBundle), or [BeSimpleSsoAuthBundle](https://github.com/BeSimple/BeSimpleSsoAuthBundle)).

However, to be able to use *external* user providers with [[= product_name =]], a valid Platform user needs to be injected into the repository.
However, to be able to use *external* user providers with [[= product_name =]], a valid Ibexa user needs to be injected into the repository.

Check failure on line 12 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/users/user_authentication.md#L12

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 12, "column": 89}}}, "severity": "ERROR"}

Check notice on line 12 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/users/user_authentication.md#L12

[Ibexa.Passive] Try to avoid passive tense, when possible.
Raw output
{"message": "[Ibexa.Passive] Try to avoid passive tense, when possible.", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 12, "column": 109}}}, "severity": "INFO"}
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 a Platform user, return an existing user, or even always use a generic user.
Depending on your context, you either want to create and return an Ibexa user, or return an existing user, even a generic one.

Check failure on line 15 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/users/user_authentication.md#L15

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 15, "column": 68}}}, "severity": "ERROR"}

Whenever an *external* user is matched (i.e. one that doesn't come from Platform repository, like coming from LDAP), [[= product_name =]] kernel initiates an `MVCEvents::INTERACTIVE_LOGIN` event.
Every service listening to this event receives an `Ibexa\Core\MVC\Symfony\Event\InteractiveLoginEvent` object which contains the original security token (that holds the matched user) and the request.
Whenever a user is matched, Symfony initiates a `SecurityEvents::INTERACTIVE_LOGIN` event.

Check notice on line 17 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/users/user_authentication.md#L17

[Ibexa.Passive] Try to avoid passive tense, when possible.
Raw output
{"message": "[Ibexa.Passive] Try to avoid passive tense, when possible.", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 17, "column": 17}}}, "severity": "INFO"}
Every service listening to this event receives an `InteractiveLoginEvent` object which contains the original security token (that holds the matched user) and the request.

Then, it's up to the listener to retrieve a Platform user from the repository and to assign it back to the event object.
Then, it's up to a listener to retrieve an Ibexa user from the repository and to assign it back to the event object.

Check failure on line 20 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/users/user_authentication.md#L20

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 20, "column": 44}}}, "severity": "ERROR"}
This user is injected into the repository and used for the rest of the request.

If no [[= product_name =]] user is returned, the Anonymous user is used.
### User mapping example

### User exposed and security token
The following example uses the [memory user provider]([[= symfony_doc =]]/security/user_providers.html#memory-user-provider),
maps memory user to Ibexa repository user,

Check failure on line 26 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/users/user_authentication.md#L26

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 26, "column": 21}}}, "severity": "ERROR"}
and [chains]([[= symfony_doc =]]/security/user_providers.html#chain-user-provider) with the Ibexa user provider to be able to use both:

When an *external* user is matched, a different token is injected into the security context, the `InteractiveLoginToken`.
This token holds a `UserWrapped` instance which contains the originally matched user and the *API user* (the one from the [[= product_name =]] repository).
Create as `src/EventSubscriber/InteractiveLoginSubscriber.php` a subscriber listening to the `SecurityEvents::INTERACTIVE_LOGIN` event
and mapping when needed an in-memory authenticated user to an Ibexa user:

Check failure on line 30 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/users/user_authentication.md#L30

[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'
Raw output
{"message": "[Ibexa.VariablesGlobal] Use global variable '[[= product_name_base =]]' instead of 'Ibexa'", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 30, "column": 63}}}, "severity": "ERROR"}

The *API user* is mainly used for permission checks against the repository and thus stays *under the hood*.

### Customize the user class

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:

``` yaml
# 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
``` php
[[= include_file('code_samples/user_management/in_memory/src/EventSubscriber/InteractiveLoginSubscriber.php') =]]
```

### Implement the listener
In the `config/services.yaml` file:
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:

``` yaml
services:
App\EventListener\InteractiveLoginListener:
arguments: ['@ibexa.api.service.user']
tags:
- { name: kernel.event_subscriber } 
[[= include_file('code_samples/user_management/in_memory/config/packages/security.yaml') =]]
```

Don't mix `MVCEvents::INTERACTIVE_LOGIN` event (specific to [[= product_name =]]) and `SecurityEvents::INTERACTIVE_LOGIN` event (fired by Symfony security component).
In the `config/services.yaml` file, declare the subscriber as a service to pass your user map

Check notice on line 46 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

[vale] docs/users/user_authentication.md#L46

[Ibexa.SentenceLength] Keep your sentences to less than 30 words.
Raw output
{"message": "[Ibexa.SentenceLength] Keep your sentences to less than 30 words.", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 46, "column": 1}}}, "severity": "INFO"}
(it's automatically tagged `kernel.event_subscriber` as implementing the `EventSubscriberInterface`, the user service injection is auto-wired):

``` php
<?php
namespace App\EventListener;
use Ibexa\Contracts\Core\Repository\UserService;
use eIbexa\Core\MVC\Symfony\Event\InteractiveLoginEvent;
use Ibexa\Core\MVC\Symfony\MVCEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class InteractiveLoginListener implements EventSubscriberInterface
{
/**
* @var \Ibexa\Contracts\Core\Repository\UserService
*/
private $userService;
public function __construct(UserService $userService)
{
$this->userService = $userService;
}
public static function getSubscribedEvents()
{
return [
MVCEvents::INTERACTIVE_LOGIN => 'onInteractiveLogin'
];
}
public function onInteractiveLogin(InteractiveLoginEvent $event)
{
// This loads a generic User and assigns it back to the event.
// You may want to create Users here, or even load predefined Users depending on your own rules.
$event->setApiUser($this->userService->loadUserByLogin( 'lolautruche' ));
}
``` yaml
[[= include_file('code_samples/user_management/in_memory/config/services.yaml') =]]
```
Loading