-
Notifications
You must be signed in to change notification settings - Fork 81
Update "User authentication" customization example (5.0) #3087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 39 commits
96a7f2f
f2d32a3
d6d7933
8e2f619
9f61b09
0e6a2a6
a1cb9a9
8b944e2
0e5516e
aab4c05
d846a46
c6ef3c5
9db8d05
daec0d1
4a37250
5cae73e
fccf5cb
924405f
88a7d34
9b561aa
a6804bd
b420efb
929ff6f
dd5fadd
2aa888f
ef889f0
02a1999
203a5d4
152729f
4eed19b
d7de84e
2e39549
f3f7786
32693c6
1d35a77
1f7e81c
29f48b8
feec9ce
8bc4c2e
e05f9ea
e3ca1de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,110 +1,75 @@ | ||
| --- | ||
| 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 Ibexa user needs to be injected into the repository. | ||
|
Check failure on line 13 in docs/users/user_authentication.md
|
||
|
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 16 in docs/users/user_authentication.md
|
||
|
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 and authenticated, Symfony initiates an `AuthenticationTokenCreatedEvent`. | ||
|
Check notice on line 18 in docs/users/user_authentication.md
|
||
| Every service listening to this event receives an object which contains the original security token (that holds the matched user) and a [passport]([[= symfony_doc =]]/security/custom_authenticator.html#security-passports). | ||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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 21 in docs/users/user_authentication.md
|
||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| If no [[= product_name =]] user is returned, the Anonymous user is used. | ||
| This Ibexa user can be | ||
|
Check failure on line 23 in docs/users/user_authentication.md
|
||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### 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, this 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
|
||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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), | ||
|
Check notice on line 32 in docs/users/user_authentication.md
|
||
| maps memory user to Ibexa repository user, | ||
|
Check failure on line 33 in docs/users/user_authentication.md
|
||
| 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 34 in docs/users/user_authentication.md
|
||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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 as `src/EventSubscriber/AuthenticationTokenCreatedSubscriber.php` subscribing to the `AuthenticationTokenCreatedEvent` event | ||
| and mapping when needed an in-memory authenticated user to an Ibexa user: | ||
|
Check failure on line 37 in docs/users/user_authentication.md
|
||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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: | ||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| ``` 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 | ||
|
Check notice on line 53 in docs/users/user_authentication.md
|
||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
| (it's automatically tagged `kernel.event_subscriber` as implementing the `EventSubscriberInterface`, the config resolver and user service injections are auto-wired): | ||
|
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/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 Ibexa user as the current user. | ||
|
Check failure on line 68 in docs/users/user_authentication.md
|
||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| From the back office, create the mapped users. | ||
| For the example, a new user with the login `generic_customer` and a random password for the mapping to work, | ||
| this account can be in the **Customers** or the **Anonymous users** group. | ||
|
adriendupuis marked this conversation as resolved.
Outdated
|
||
|
|
||
| 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`. | ||
Uh oh!
There was an error while loading. Please reload this page.