Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 6 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,6 @@
services:
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(): array
{
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));
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);

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.

}
}
}
2 changes: 2 additions & 0 deletions deptrac.baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ deptrac:
- Ibexa\FormBuilder\Event\FormEvents
App\EventSubscriber\HelpMenuSubscriber:
- Ibexa\AdminUi\Menu\Event\ConfigureMenuEvent
App\EventSubscriber\InteractiveLoginSubscriber:
- Ibexa\Core\MVC\Symfony\Security\User
App\EventSubscriber\MyMenuSubscriber:
- Ibexa\AdminUi\Menu\Event\ConfigureMenuEvent
- Ibexa\AdminUi\Menu\MainMenuBuilder
Expand Down
109 changes: 25 additions & 84 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.
This user is injected into the repository and used for the rest of the request.
Then, it's up to a listener to retrieve an Ibexa user from the repository.

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 wrapped within `Ibexa\Core\MVC\Symfony\Security\User` and assigned back into the event's token for the rest of the request.

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

View workflow job for this annotation

GitHub Actions / vale

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

[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": 21, "column": 11}}}, "severity": "INFO"}

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` subscribing 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