diff --git a/code_samples/user_management/in_memory/config/packages/security.yaml b/code_samples/user_management/in_memory/config/packages/security.yaml new file mode 100644 index 0000000000..72f27b50a1 --- /dev/null +++ b/code_samples/user_management/in_memory/config/packages/security.yaml @@ -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 diff --git a/code_samples/user_management/in_memory/config/services.yaml b/code_samples/user_management/in_memory/config/services.yaml new file mode 100644 index 0000000000..a95531ace9 --- /dev/null +++ b/code_samples/user_management/in_memory/config/services.yaml @@ -0,0 +1,6 @@ +services: + App\EventSubscriber\AuthenticationTokenCreatedSubscriber: + arguments: + $userMap: + from_memory_user: generic_customer + from_memory_admin: admin diff --git a/code_samples/user_management/in_memory/src/EventSubscriber/AuthenticationTokenCreatedSubscriber.php b/code_samples/user_management/in_memory/src/EventSubscriber/AuthenticationTokenCreatedSubscriber.php new file mode 100644 index 0000000000..4f351028c5 --- /dev/null +++ b/code_samples/user_management/in_memory/src/EventSubscriber/AuthenticationTokenCreatedSubscriber.php @@ -0,0 +1,47 @@ + $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)); + } +} diff --git a/deptrac.baseline.yaml b/deptrac.baseline.yaml index b465d1d963..9fef31d4bc 100644 --- a/deptrac.baseline.yaml +++ b/deptrac.baseline.yaml @@ -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: diff --git a/docs/users/user_authentication.md b/docs/users/user_authentication.md index d2b7280301..9c3f23c104 100644 --- a/docs/users/user_authentication.md +++ b/docs/users/user_authentication.md @@ -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. 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`. +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. -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: -``` 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`. +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 -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`.