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..a5239f50f1 --- /dev/null +++ b/code_samples/user_management/in_memory/config/packages/security.yaml @@ -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 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..83124bea6a --- /dev/null +++ b/code_samples/user_management/in_memory/config/services.yaml @@ -0,0 +1,6 @@ +services: + App\EventSubscriber\InteractiveLoginSubscriber: + arguments: + $userMap: + from_memory_user: customer + from_memory_admin: admin diff --git a/code_samples/user_management/in_memory/src/EventSubscriber/InteractiveLoginSubscriber.php b/code_samples/user_management/in_memory/src/EventSubscriber/InteractiveLoginSubscriber.php new file mode 100644 index 0000000000..ac000c0439 --- /dev/null +++ b/code_samples/user_management/in_memory/src/EventSubscriber/InteractiveLoginSubscriber.php @@ -0,0 +1,37 @@ + $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)); + } + } +} diff --git a/docs/users/user_authentication.md b/docs/users/user_authentication.md index d2b7280301..a15af3fcca 100644 --- a/docs/users/user_authentication.md +++ b/docs/users/user_authentication.md @@ -6,105 +6,46 @@ description: Customize 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 Ibexa 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 Ibexa 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, Symfony initiates a `SecurityEvents::INTERACTIVE_LOGIN` event. +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. +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. -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, +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: -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 +(it's automatically tagged `kernel.event_subscriber` as implementing the `EventSubscriberInterface`, the user service injection is auto-wired): -``` 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' )); - } -}  +``` yaml +[[= include_file('code_samples/user_management/in_memory/config/services.yaml') =]] ```