From 8dbeb60e50ff6e111db3a37182b7d6265eb4fd02 Mon Sep 17 00:00:00 2001 From: Laurent VOULLEMIER Date: Fri, 28 Jul 2023 11:35:01 +0200 Subject: [PATCH] Allow to use ldap in a chain provider --- .../Ldap/Attribute/WithLdapPassword.php | 23 ++ .../Security/CheckLdapCredentialsListener.php | 20 +- .../Ldap/Security/LdapAuthenticator.php | 2 +- .../Component/Ldap/Security/LdapBadge.php | 10 +- .../CheckLdapCredentialsListenerTest.php | 94 +++++-- .../Ldap/Tests/Security/LdapBadgeTest.php | 42 ++++ .../Provider/ChainProviderWithLdapTest.php | 237 ++++++++++++++++++ .../Component/Security/Core/composer.json | 1 + 8 files changed, 406 insertions(+), 23 deletions(-) create mode 100644 src/Symfony/Component/Ldap/Attribute/WithLdapPassword.php create mode 100644 src/Symfony/Component/Ldap/Tests/Security/LdapBadgeTest.php create mode 100644 src/Symfony/Component/Security/Core/Tests/Authentication/Provider/ChainProviderWithLdapTest.php diff --git a/src/Symfony/Component/Ldap/Attribute/WithLdapPassword.php b/src/Symfony/Component/Ldap/Attribute/WithLdapPassword.php new file mode 100644 index 0000000000000..12c35a4f62b8f --- /dev/null +++ b/src/Symfony/Component/Ldap/Attribute/WithLdapPassword.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Attribute; + +/** + * Marker to allow or not using ldap credentials for a non-ldap user. + * + * @author Laurent VOULLEMIER + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class WithLdapPassword +{ + public function __construct(public readonly bool $enabled = true) {} +} diff --git a/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php b/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php index 7e535ebde1267..5fa1fb2bf730d 100644 --- a/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php +++ b/src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php @@ -13,11 +13,13 @@ use Psr\Container\ContainerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Ldap\Attribute\WithLdapPassword; use Symfony\Component\Ldap\Exception\InvalidCredentialsException; use Symfony\Component\Ldap\Exception\InvalidSearchCredentialsException; use Symfony\Component\Ldap\LdapInterface; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\Exception\LogicException; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; use Symfony\Component\Security\Http\Event\CheckPassportEvent; @@ -48,9 +50,6 @@ public function onCheckPassport(CheckPassportEvent $event) /** @var LdapBadge $ldapBadge */ $ldapBadge = $passport->getBadge(LdapBadge::class); - if ($ldapBadge->isResolved()) { - return; - } if (!$passport->hasBadge(PasswordCredentials::class)) { throw new \LogicException(sprintf('LDAP authentication requires a passport containing password credentials, authenticator "%s" does not fulfill these requirements.', $event->getAuthenticator()::class)); @@ -72,6 +71,16 @@ public function onCheckPassport(CheckPassportEvent $event) } $user = $passport->getUser(); + $nonLdapUserWithoutLdapPasswordAttribute = false; + if (!$user instanceof LdapUser) { + $reflectionClass = new \ReflectionClass($user); + $attr = $reflectionClass->getAttributes(WithLdapPassword::class); + + $nonLdapUserWithoutLdapPasswordAttribute = count($attr) === 0; + if (!$nonLdapUserWithoutLdapPasswordAttribute && !$attr[0]->newInstance()->enabled) { + return; + } + } /** @var LdapInterface $ldap */ $ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId()); @@ -104,8 +113,11 @@ public function onCheckPassport(CheckPassportEvent $event) throw new BadCredentialsException('The presented password is invalid.'); } + if ($nonLdapUserWithoutLdapPasswordAttribute) { + trigger_deprecation('symfony/ldap', '6.4', 'Authenticate a user that is not an instance of %s is deprecated and won\'t be the default behavior anymore in 7.0. Use the %s attribute to keep this behavior.', LdapUser::class, WithLdapPassword::class); + } + $passwordCredentials->markResolved(); - $ldapBadge->markResolved(); } public static function getSubscribedEvents(): array diff --git a/src/Symfony/Component/Ldap/Security/LdapAuthenticator.php b/src/Symfony/Component/Ldap/Security/LdapAuthenticator.php index c2999e9efc6f1..a9c77bdfebd3b 100644 --- a/src/Symfony/Component/Ldap/Security/LdapAuthenticator.php +++ b/src/Symfony/Component/Ldap/Security/LdapAuthenticator.php @@ -60,7 +60,7 @@ public function supports(Request $request): ?bool public function authenticate(Request $request): Passport { $passport = $this->authenticator->authenticate($request); - $passport->addBadge(new LdapBadge($this->ldapServiceId, $this->dnString, $this->searchDn, $this->searchPassword, $this->queryString)); + $passport->addBadge(new LdapBadge($this->ldapServiceId, $this->dnString, $this->searchDn, $this->searchPassword, $this->queryString, true)); return $passport; } diff --git a/src/Symfony/Component/Ldap/Security/LdapBadge.php b/src/Symfony/Component/Ldap/Security/LdapBadge.php index 2f8b1d7bd307d..ac729c7ff2761 100644 --- a/src/Symfony/Component/Ldap/Security/LdapBadge.php +++ b/src/Symfony/Component/Ldap/Security/LdapBadge.php @@ -24,14 +24,14 @@ */ class LdapBadge implements BadgeInterface { - private bool $resolved = false; + private bool $resolved = true; private string $ldapServiceId; private string $dnString; private string $searchDn; private string $searchPassword; private ?string $queryString; - public function __construct(string $ldapServiceId, string $dnString = '{user_identifier}', string $searchDn = '', string $searchPassword = '', string $queryString = null) + public function __construct(string $ldapServiceId, string $dnString = '{user_identifier}', string $searchDn = '', string $searchPassword = '', string $queryString = null, bool $resolved = false) { $this->ldapServiceId = $ldapServiceId; $dnString = str_replace('{username}', '{user_identifier}', $dnString, $replaceCount); @@ -46,6 +46,10 @@ public function __construct(string $ldapServiceId, string $dnString = '{user_ide trigger_deprecation('symfony/ldap', '6.2', 'Using "{username}" parameter in LDAP configuration is deprecated, consider using "{user_identifier}" instead.'); } $this->queryString = $queryString; + $this->resolved = $resolved; + if (false === $this->resolved) { + trigger_deprecation('symfony/ldap', '6.4', 'Passing "false" as resolved initial value is deprecated, use "true" instead.'); + } } public function getLdapServiceId(): string @@ -75,6 +79,8 @@ public function getQueryString(): ?string public function markResolved(): void { + trigger_deprecation('symfony/ldap', '6.4', '%s is deprecated and will be removed in 7.0. %s is intended to bear LDAP information and doesn\'t need to be resolved anymore.', __METHOD__, __CLASS__); + $this->resolved = true; } diff --git a/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php b/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php index 6392c0d3b4265..d34b93655db11 100644 --- a/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php +++ b/src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php @@ -14,19 +14,23 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Ldap\Adapter\CollectionInterface; use Symfony\Component\Ldap\Adapter\QueryInterface; +use Symfony\Component\Ldap\Attribute\WithLdapPassword; use Symfony\Component\Ldap\Entry; use Symfony\Component\Ldap\Exception\InvalidCredentialsException; use Symfony\Component\Ldap\LdapInterface; use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener; use Symfony\Component\Ldap\Security\LdapBadge; +use Symfony\Component\Ldap\Security\LdapUser; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; @@ -38,6 +42,7 @@ class CheckLdapCredentialsListenerTest extends TestCase { + use ExpectDeprecationTrait; private MockObject&LdapInterface $ldap; protected function setUp(): void @@ -46,25 +51,42 @@ protected function setUp(): void } /** - * @dataProvider provideShouldNotCheckPassport + * @group legacy + * + * @dataProvider provideShouldCheckPassport */ - public function testShouldNotCheckPassport($authenticator, $passport) + public function testShouldCheckPassport(AuthenticatorInterface $authenticator, Passport $passport, bool $expectedBindCalled, bool $expectDeprecation) { - $this->ldap->expects($this->never())->method('bind'); + if ($expectDeprecation) { + $this->expectDeprecation('Since symfony/ldap 6.4: Authenticate a user that is not an instance of Symfony\Component\Ldap\Security\LdapUser is deprecated and won\'t be the default behavior anymore in 7.0. Use the Symfony\Component\Ldap\Attribute\WithLdapPassword attribute to keep this behavior.'); + } + + if ($expectedBindCalled) { + $this->ldap->expects($this->once())->method('bind'); + } else { + $this->ldap->expects($this->never())->method('bind'); + } $listener = $this->createListener(); $listener->onCheckPassport(new CheckPassportEvent($authenticator, $passport)); } - public static function provideShouldNotCheckPassport() + public static function provideShouldCheckPassport() { - // no LdapBadge - yield [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret'))]; + yield 'no LdapBadge' => [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret')), false, false]; - // ldap already resolved - $badge = new LdapBadge('app.ldap'); - $badge->markResolved(); - yield [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret'), [$badge])]; + $ldapBadge = new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true); + $userBadge = new UserBadge('test'); + $userBadge->setUserLoader(function () { return new InMemoryUser('test', 'pass', ['ROLE_USER']); }); + yield 'non ldap user' => [new TestAuthenticator(), new Passport($userBadge, new PasswordCredentials('s3cret'), [$ldapBadge]), true, true]; + + $userBadge = new UserBadge('test'); + $userBadge->setUserLoader(function () { return new UserWithLdapPasswordEnabled('test', ['ROLE_USER']); }); + yield 'withLdapPassword enabled' => [new TestAuthenticator(), new Passport($userBadge, new PasswordCredentials('s3cret'), [$ldapBadge]), true, false]; + + $userBadge = new UserBadge('test'); + $userBadge->setUserLoader(function () { return new UserWithLdapPasswordDisabled('test', ['ROLE_USER']); }); + yield 'withLdapPassword disabled' => [new TestAuthenticator(), new Passport($userBadge, new PasswordCredentials('s3cret'), [$ldapBadge]), false, false]; } public function testPasswordCredentialsAlreadyResolvedThrowsException() @@ -74,7 +96,7 @@ public function testPasswordCredentialsAlreadyResolvedThrowsException() $badge = new PasswordCredentials('s3cret'); $badge->markResolved(); - $passport = new Passport(new UserBadge('test'), $badge, [new LdapBadge('app.ldap')]); + $passport = new Passport(new UserBadge('test'), $badge, [new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)]); $listener = $this->createListener(); $listener->onCheckPassport(new CheckPassportEvent(new TestAuthenticator(), $passport)); @@ -86,7 +108,7 @@ public function testInvalidLdapServiceId() $this->expectExceptionMessage('Cannot check credentials using the "not_existing_ldap_service" ldap service, as such service is not found. Did you maybe forget to add the "ldap" service tag to this service?'); $listener = $this->createListener(); - $listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('not_existing_ldap_service'))); + $listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('not_existing_ldap_service', '{user_identifier}', '', '', null, true))); } /** @@ -104,7 +126,10 @@ public function testWrongPassport($passport) public static function provideWrongPassportData() { // no password credentials - yield [new SelfValidatingPassport(new UserBadge('test'), [new LdapBadge('app.ldap')])]; + yield [new SelfValidatingPassport( + new UserBadge('test'), + [new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)] + )]; } public function testEmptyPasswordShouldThrowAnException() @@ -171,6 +196,9 @@ public static function queryForDnProvider(): iterable yield ['{user_identifier}', '{user_identifier}_test']; } + /** + * @group legacy + */ public function testQueryForDn() { $collection = new class([new Entry('')]) extends \ArrayObject implements CollectionInterface { @@ -198,7 +226,7 @@ public function toArray(): array $this->ldap->expects($this->once())->method('query')->with('{user_identifier}', 'wouter_test')->willReturn($query); $listener = $this->createListener(); - $listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test'))); + $listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test', true))); } public function testEmptyQueryResultShouldThrowAnException() @@ -226,14 +254,21 @@ public function testEmptyQueryResultShouldThrowAnException() $this->ldap->expects($this->once())->method('query')->willReturn($query); $listener = $this->createListener(); - $listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test'))); + $listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test', true))); } private function createEvent($password = 's3cr3t', $ldapBadge = null) { + $ldapUser = new LdapUser(new Entry('cn=Wouter,dc=example,dc=com'), 'Wouter', null, ['ROLE_USER']); + + /*return new CheckPassportEvent( + new TestAuthenticator(), + new Passport(new UserBadge('Wouter', fn () => $ldapUser), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)]) + );*/ + return new CheckPassportEvent( new TestAuthenticator(), - new Passport(new UserBadge('Wouter', fn () => new InMemoryUser('Wouter', null, ['ROLE_USER'])), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap')]) + new Passport(new UserBadge('Wouter', fn () => new InMemoryUser('Wouter', null, ['ROLE_USER'])), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)]) ); } @@ -278,3 +313,30 @@ public function createToken(Passport $passport, string $firewallName): TokenInte } } } + +class BaseUser implements UserInterface +{ + public function __construct(private string $identifier, private array $roles) + { + } + + public function getRoles(): array + { + return $this->roles; + } + + public function eraseCredentials(): void + { + } + + public function getUserIdentifier(): string + { + return $this->identifier; + } +} + +#[WithLdapPassword] +class UserWithLdapPasswordEnabled extends BaseUser {} + +#[WithLdapPassword(false)] +class UserWithLdapPasswordDisabled extends BaseUser {} diff --git a/src/Symfony/Component/Ldap/Tests/Security/LdapBadgeTest.php b/src/Symfony/Component/Ldap/Tests/Security/LdapBadgeTest.php new file mode 100644 index 0000000000000..c3959ab819910 --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/Security/LdapBadgeTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Security; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; +use Symfony\Component\Ldap\Security\LdapBadge; + +final class LdapBadgeTest extends TestCase +{ + use ExpectDeprecationTrait; + + /** + * @group legacy + */ + public function testDeprecationOnResolvedInitialValue() + { + $this->expectDeprecation('Since symfony/ldap 6.4: Passing "false" as resolved initial value is deprecated, use "true" instead.'); + + new LdapBadge('foo'); + } + + /** + * @group legacy + */ + public function testDeprecationOnMarkAsResolved() + { + $this->expectDeprecation('Since symfony/ldap 6.4: Symfony\Component\Ldap\Security\LdapBadge::markResolved is deprecated and will be removed in 7.0. Symfony\Component\Ldap\Security\LdapBadge is intended to bear LDAP information and doesn\'t need to be resolved anymore.'); + + $sut = new LdapBadge('foo', '{user_identifier}', '', '', null, true); + $sut->markResolved(); + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/ChainProviderWithLdapTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/ChainProviderWithLdapTest.php new file mode 100644 index 0000000000000..ec6b87a4d0dd8 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/ChainProviderWithLdapTest.php @@ -0,0 +1,237 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Symfony\Component\Ldap\Adapter\AdapterInterface; +use Symfony\Component\Ldap\Adapter\CollectionInterface; +use Symfony\Component\Ldap\Adapter\ConnectionInterface; +use Symfony\Component\Ldap\Adapter\QueryInterface; +use Symfony\Component\Ldap\Attribute\WithLdapPassword; +use Symfony\Component\Ldap\Entry; +use Symfony\Component\Ldap\Exception\ConnectionException; +use Symfony\Component\Ldap\Ldap; +use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener; +use Symfony\Component\Ldap\Security\LdapAuthenticator; +use Symfony\Component\Ldap\Security\LdapUserProvider; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; +use Symfony\Component\Security\Core\User\ChainUserProvider; +use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticatorManager; +use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Component\Security\Http\EventListener\UserProviderListener; +use Symfony\Component\Security\Http\HttpUtils; + +class ChainProviderWithLdapTest extends TestCase +{ + public function provideChainWithLdap(): array + { + return [ + 'user from custom provider' => ['foo', 'foopass'], + 'user from ldap provider' => ['bar', 'barpass'], + ]; + } + + /** + * @dataProvider provideChainWithLdap + */ + public function testChainWithLdap(string $userIdentifier, string $pass) + { + $customUserProvider = new CustomUserProvider(); + + $ldapAdapteur = $this->createMock(AdapterInterface::class); + $ldapAdapteur + ->method('getConnection') + ->willReturn($connection = $this->createMock(ConnectionInterface::class)) + ; + + $connection + ->method('bind') + ->willReturnCallback(static function (?string $user, ?string $pass): void { + if ('admin' === $user && 'adminpass' === $pass) { + return; + } + + if ('bar' === $user && 'barpass' === $pass) { + return; + } + + throw new ConnectionException('failure when binding'); + }) + ; + + $ldapAdapteur + ->method('escape') + ->willReturnArgument(0) + ; + + $ldapAdapteur + ->method('createQuery') + ->willReturn($query = $this->createMock(QueryInterface::class)) + ; + + $query + ->method('execute') + ->willReturn($collection = $this->createMock(CollectionInterface::class)); + + $collection + ->method('count') + ->willReturn(1) + ; + + $collection + ->method('offsetGet') + ->with(0) + ->willReturn(new Entry('cn=bar,dc=example,dc=com', ['sAMAccountName' => ['bar'], 'userPassword' => ['barpass']])) + ; + + $ldapProvider = new LdapUserProvider($ldap = new Ldap($ldapAdapteur), 'dc=example,dc=com', 'admin', 'adminpass', [], null, null, 'userPassword'); + + $chainUserProvider = new ChainUserProvider([$customUserProvider, $ldapProvider]); + + $httpUtils = $this->createMock(HttpUtils::class); + $httpUtils + ->method('checkRequestPath') + ->willReturn(true) + ; + + $failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class); + $failureHandler + ->method('onAuthenticationFailure') + ->willReturn(new Response()) + ; + + $formLoginAuthenticator = new FormLoginAuthenticator( + $httpUtils, + $chainUserProvider, + $this->createMock(AuthenticationSuccessHandlerInterface::class), + $failureHandler, + [] + ); + + $ldapAuthenticator = new LdapAuthenticator($formLoginAuthenticator, 'ldap-id'); + + $ldapLocator = new class($ldap) implements ContainerInterface { + private $ldap; + + public function __construct(Ldap $ldap) + { + $this->ldap = $ldap; + } + + public function get(string $id): Ldap + { + return $this->ldap; + } + + public function has(string $id): bool + { + return 'ldap-id' === $id; + } + }; + + $eventDispatcher = new EventDispatcher(); + $eventDispatcher->addListener(CheckPassportEvent::class, [new UserProviderListener($chainUserProvider), 'checkPassport']); + $eventDispatcher->addListener(CheckPassportEvent::class, [new CheckLdapCredentialsListener($ldapLocator), 'onCheckPassport']); + $eventDispatcher->addListener(CheckPassportEvent::class, function (CheckPassportEvent $event): void { + $passport = $event->getPassport(); + $userBadge = $passport->getBadge(UserBadge::class); + if (null === $userBadge || null === $userBadge->getUser()) { + return; + } + $credentials = $passport->getBadge(PasswordCredentials::class); + if ($credentials->isResolved()) { + return; + } + + if ($credentials && 'foopass' === $credentials->getPassword()) { + $credentials->markResolved(); + } + }); + + $authenticatorManager = new AuthenticatorManager( + [$ldapAuthenticator], + $tokenStorage = new TokenStorage(), + $eventDispatcher, + 'main' + ); + + $request = Request::create('/login', 'POST', ['_username' => $userIdentifier, '_password' => $pass]); + $request->setSession(new Session(new MockArraySessionStorage())); + + $this->assertTrue($authenticatorManager->supports($request)); + $authenticatorManager->authenticateRequest($request); + + $this->assertInstanceOf(UsernamePasswordToken::class, $token = $tokenStorage->getToken()); + $this->assertSame($userIdentifier, $token->getUserIdentifier()); + } +} + +#[WithLdapPassword(false)] +class FooUser implements UserInterface, PasswordAuthenticatedUserInterface +{ + public function getPassword(): ?string + { + return 'foopass'; + } + + public function getRoles(): array + { + return ['ROLE_USER']; + } + + public function eraseCredentials(): void + { + } + + public function getUserIdentifier(): string + { + return 'foo'; + } +} + +class CustomUserProvider implements UserProviderInterface +{ + public function refreshUser(UserInterface $user): UserInterface + { + return $user; + } + + public function supportsClass(string $class): bool + { + return $class === FooUser::class; + } + + public function loadUserByIdentifier(string $identifier): UserInterface + { + if ($identifier !== 'foo') { + throw new UserNotFoundException('User foo not found'); + } + + return new FooUser(); + } +} diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index dc1e195139ae6..997c7d3b866ea 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -30,6 +30,7 @@ "symfony/expression-language": "^5.4|^6.0|^7.0", "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/ldap": "^5.4|^6.0|^7.0", + "symfony/security-http": "^5.4|^6.0|^7.0", "symfony/string": "^5.4|^6.0|^7.0", "symfony/translation": "^5.4|^6.0|^7.0", "symfony/validator": "^5.4|^6.0|^7.0",