Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
96a7f2f
user_authentication.md: fixes, encoders → password_hashers
adriendupuis Mar 12, 2026
f2d32a3
user_authentication.md: fix subscriber
adriendupuis Mar 13, 2026
d6d7933
user_authentication.md: move code to external files
adriendupuis Mar 13, 2026
8e2f619
user_authentication.md: start to update explanations
adriendupuis Mar 13, 2026
9f61b09
InteractiveLoginSubscriber::onInteractiveLogin() return type
adriendupuis Mar 13, 2026
0e6a2a6
PHP & JS CS Fixes
adriendupuis Mar 13, 2026
a1cb9a9
user_authentication.md: Rewrite example description
adriendupuis Mar 13, 2026
8b944e2
PHP & JS CS Fixes
adriendupuis Mar 13, 2026
0e5516e
InteractiveLoginSubscriber: Fix missingType.iterableValue
adriendupuis Mar 13, 2026
aab4c05
InteractiveLoginSubscriber: No need to return the event
adriendupuis Mar 13, 2026
d846a46
services.yaml: Format
adriendupuis Mar 13, 2026
c6ef3c5
user_authentication.md: closer to reality
adriendupuis Mar 13, 2026
9db8d05
user_authentication.md: wording
adriendupuis Mar 16, 2026
daec0d1
deptrac.baseline.yaml: Ignore InteractiveLoginSubscriber Security\User
adriendupuis Mar 16, 2026
4a37250
InteractiveLoginSubscriber: typehint
adriendupuis Mar 16, 2026
5cae73e
Merge branch '5.0' into user_auth_5.0
adriendupuis Mar 16, 2026
fccf5cb
Apply suggestion from @konradoboza
adriendupuis Jun 8, 2026
924405f
readability around instanceof test
adriendupuis Jun 8, 2026
88a7d34
Use anonymous_user_id
adriendupuis Jun 8, 2026
9b561aa
UserWrapped and more about example users
adriendupuis Jun 8, 2026
a6804bd
UserWrapped and more about example users
adriendupuis Jun 8, 2026
b420efb
UserWrapped and more about example users
adriendupuis Jun 8, 2026
929ff6f
SecurityEvents::INTERACTIVE_LOGIN → AuthenticationTokenCreatedEvent
adriendupuis Jun 8, 2026
dd5fadd
Merge branch '5.0' into user_auth_5.0
adriendupuis Jun 8, 2026
2aa888f
Merge branch 'user_auth_5.0' into user_auth_5.0-alt2
adriendupuis Jun 8, 2026
ef889f0
Fix deptrac
adriendupuis Jun 8, 2026
02a1999
Fix deptrac
adriendupuis Jun 8, 2026
203a5d4
Fix deptrac
adriendupuis Jun 8, 2026
152729f
Fix deptrac
adriendupuis Jun 8, 2026
4eed19b
Merge branch 'user_auth_5.0' into user_auth_5.0-alt2
adriendupuis Jun 8, 2026
d7de84e
Fix deptrac
adriendupuis Jun 8, 2026
2e39549
Apply suggestion from @adriendupuis
adriendupuis Jun 9, 2026
f3f7786
AuthenticationTokenCreatedSubscriber: facto, clean-up
adriendupuis Jun 9, 2026
32693c6
Apply suggestion from @adriendupuis
adriendupuis Jun 9, 2026
1d35a77
AuthenticationTokenCreatedSubscriber: increase priority
adriendupuis Jun 9, 2026
1f7e81c
user_authentication.md: about priority
adriendupuis Jun 9, 2026
29f48b8
Merge branch '5.0' into user_auth_5.0
adriendupuis Jun 9, 2026
feec9ce
user_authentication.md month_change: true
adriendupuis Jun 9, 2026
8bc4c2e
Merge branch '5.0' into user_auth_5.0
adriendupuis Jun 9, 2026
e05f9ea
Apply suggestions from code review
adriendupuis Jun 10, 2026
e3ca1de
Update docs/users/user_authentication.md
adriendupuis Jun 11, 2026
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,38 @@
<?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;

final readonly 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) {
return;
}
$userLogin = $this->userMap[$event->getAuthenticationToken()->getUserIdentifier()] ?? 'anonymous';
$ibexaUser = $this->userService->loadUserByLogin($userLogin);
$event->getAuthenticationToken()->setUser(new User($ibexaUser));
}
}
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"}
Comment thread
adriendupuis marked this conversation as resolved.
Outdated
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"}
Comment thread
adriendupuis marked this conversation as resolved.
Outdated

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"}
Comment thread
adriendupuis marked this conversation as resolved.
Outdated
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),

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

View workflow job for this annotation

GitHub Actions / vale

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

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

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

View workflow job for this annotation

GitHub Actions / vale

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

[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": 27, "column": 93}}}, "severity": "ERROR"}
Comment thread
adriendupuis marked this conversation as resolved.
Outdated

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:
Comment thread
adriendupuis marked this conversation as resolved.
Outdated

``` 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"}
Comment thread
adriendupuis marked this conversation as resolved.
Outdated
(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