Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,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
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\AuthenticationTokenCreatedSubscriber:
arguments:
$userMap:
from_memory_user: generic_customer
from_memory_admin: admin
Original file line number Diff line number Diff line change
@@ -0,0 +1,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));
}
}
2 changes: 2 additions & 0 deletions deptrac.baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ deptrac:
App\EventListener\TextAnchorMenuTabListener:
- Ibexa\AdminUi\Menu\ContentEditAnchorMenuBuilder
- Ibexa\AdminUi\Menu\Event\ConfigureMenuEvent
App\EventSubscriber\AuthenticationTokenCreatedSubscriber:
- Ibexa\Core\MVC\Symfony\Security\UserWrapped
App\EventSubscriber\BreadcrumbsMenuSubscriber:
- Ibexa\Bundle\Storefront\Menu\Builder\BreadcrumbsMenuBuilder
App\EventSubscriber\FormFieldDefinitionSubscriber:
Expand Down
115 changes: 36 additions & 79 deletions docs/users/user_authentication.md
Original file line number Diff line number Diff line change
@@ -1,110 +1,67 @@
---
description: Customize user authentication.
month_change: true
---

# User authentication

## 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 [[= product_name_base =]] user needs to be injected into the repository.

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

View workflow job for this annotation

GitHub Actions / vale

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

[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": 13, "column": 129}}}, "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 [[= product_name_base =]] user, or return an existing user, even a generic one.

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 and authenticated, Symfony initiates an `AuthenticationTokenCreatedEvent`.

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

View workflow job for this annotation

GitHub Actions / vale

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

[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": 18, "column": 17}}}, "severity": "INFO"}
Every service listening to this event receives an object containing the original security token, which holds the matched user, and a [passport]([[= symfony_doc =]]/security/custom_authenticator.html#security-passports).

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 [[= product_name_base =]] user from the repository.

If no [[= product_name =]] user is returned, the Anonymous user is used.
This [[= product_name_base =]] user can be:

### User exposed and security token
- 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

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).
Finally, the user is assigned back into the event's token for the rest of the process.

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

View workflow job for this annotation

GitHub Actions / vale

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

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

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

### Customize the user class
The following example uses the [memory user provider]([[= symfony_doc =]]/security/user_providers.html#memory-user-provider), maps memory user to [[= product_name_base =]] repository user, and [chains]([[= symfony_doc =]]/security/user_providers.html#chain-user-provider) with the [[= product_name_base =]] 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`.
Create a `src/EventSubscriber/AuthenticationTokenCreatedSubscriber.php` that subscribes to the `AuthenticationTokenCreatedEvent` event and maps an authenticated in-memory user to an [[= product_name_base =]] user when necessary:

You can override `getUser()` to return whatever user class you want, as long as it implements `Ibexa\Core\MVC\Symfony\Security\UserInterface`.
``` php
[[= include_file('code_samples/user_management/in_memory/src/EventSubscriber/AuthenticationTokenCreatedSubscriber.php') =]]
```

The following is an example of using the in-memory user provider:
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:

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

View workflow job for this annotation

GitHub Actions / vale

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

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

``` 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
``` yaml hl_lines="4 9-14 18-20 26"
[[= include_file('code_samples/user_management/in_memory/config/packages/security.yaml') =]]
```

### Implement the listener

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

Check warning on line 47 in docs/users/user_authentication.md

View workflow job for this annotation

GitHub Actions / vale

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

[Ibexa.EOLWhitespace] Remove whitespace characters from the end of the line.
Raw output
{"message": "[Ibexa.EOLWhitespace] Remove whitespace characters from the end of the line.", "location": {"path": "docs/users/user_authentication.md", "range": {"start": {"line": 47, "column": 110}}}, "severity": "WARNING"}
The config resolver and user service injections are auto-wired automatically.

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

Don't mix `MVCEvents::INTERACTIVE_LOGIN` event (specific to [[= product_name =]]) and `SecurityEvents::INTERACTIVE_LOGIN` event (fired by Symfony security component).
You can list the subscribers with the following command to check their order:

``` 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' ));
}
``` bash
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 [[= product_name_base =]] 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`.
Loading