Skip to content

Commit cbe2c95

Browse files
committed
Allow to use ldap in a chain provider
1 parent 532dcda commit cbe2c95

File tree

7 files changed

+261
-16
lines changed

7 files changed

+261
-16
lines changed

src/Symfony/Component/Ldap/Security/CheckLdapCredentialsListener.php

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ public function onCheckPassport(CheckPassportEvent $event)
4848

4949
/** @var LdapBadge $ldapBadge */
5050
$ldapBadge = $passport->getBadge(LdapBadge::class);
51-
if ($ldapBadge->isResolved()) {
52-
return;
53-
}
5451

5552
if (!$passport->hasBadge(PasswordCredentials::class)) {
5653
throw new \LogicException(sprintf('LDAP authentication requires a passport containing password credentials, authenticator "%s" does not fulfill these requirements.', $event->getAuthenticator()::class));
@@ -72,6 +69,9 @@ public function onCheckPassport(CheckPassportEvent $event)
7269
}
7370

7471
$user = $passport->getUser();
72+
if (!$user instanceof LdapUser) {
73+
return;
74+
}
7575

7676
/** @var LdapInterface $ldap */
7777
$ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId());
@@ -105,7 +105,6 @@ public function onCheckPassport(CheckPassportEvent $event)
105105
}
106106

107107
$passwordCredentials->markResolved();
108-
$ldapBadge->markResolved();
109108
}
110109

111110
public static function getSubscribedEvents(): array

src/Symfony/Component/Ldap/Security/LdapAuthenticator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function supports(Request $request): ?bool
6060
public function authenticate(Request $request): Passport
6161
{
6262
$passport = $this->authenticator->authenticate($request);
63-
$passport->addBadge(new LdapBadge($this->ldapServiceId, $this->dnString, $this->searchDn, $this->searchPassword, $this->queryString));
63+
$passport->addBadge(new LdapBadge($this->ldapServiceId, $this->dnString, $this->searchDn, $this->searchPassword, $this->queryString, true));
6464

6565
return $passport;
6666
}

src/Symfony/Component/Ldap/Security/LdapBadge.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,14 @@
2424
*/
2525
class LdapBadge implements BadgeInterface
2626
{
27-
private bool $resolved = false;
27+
private bool $resolved = true;
2828
private string $ldapServiceId;
2929
private string $dnString;
3030
private string $searchDn;
3131
private string $searchPassword;
3232
private ?string $queryString;
3333

34-
public function __construct(string $ldapServiceId, string $dnString = '{user_identifier}', string $searchDn = '', string $searchPassword = '', string $queryString = null)
34+
public function __construct(string $ldapServiceId, string $dnString = '{user_identifier}', string $searchDn = '', string $searchPassword = '', string $queryString = null, bool $resolved = false)
3535
{
3636
$this->ldapServiceId = $ldapServiceId;
3737
$dnString = str_replace('{username}', '{user_identifier}', $dnString, $replaceCount);
@@ -46,6 +46,10 @@ public function __construct(string $ldapServiceId, string $dnString = '{user_ide
4646
trigger_deprecation('symfony/ldap', '6.2', 'Using "{username}" parameter in LDAP configuration is deprecated, consider using "{user_identifier}" instead.');
4747
}
4848
$this->queryString = $queryString;
49+
$this->resolved = $resolved;
50+
if (false === $this->resolved) {
51+
trigger_deprecation('symfony/ldap', '6.4', 'Passing false as resolved initial value is deprecated, use true instead.');
52+
}
4953
}
5054

5155
public function getLdapServiceId(): string
@@ -75,6 +79,8 @@ public function getQueryString(): ?string
7579

7680
public function markResolved(): void
7781
{
82+
trigger_deprecation('symfony/ldap', '6.4', 'Calling %s is deprecated.', __METHOD__);
83+
7884
$this->resolved = true;
7985
}
8086

src/Symfony/Component/Ldap/Tests/Security/CheckLdapCredentialsListenerTest.php

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use Symfony\Component\Ldap\LdapInterface;
2424
use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener;
2525
use Symfony\Component\Ldap\Security\LdapBadge;
26+
use Symfony\Component\Ldap\Security\LdapUser;
2627
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
2728
use Symfony\Component\Security\Core\Exception\AuthenticationException;
2829
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
@@ -62,9 +63,10 @@ public static function provideShouldNotCheckPassport()
6263
yield [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret'))];
6364

6465
// ldap already resolved
65-
$badge = new LdapBadge('app.ldap');
66-
$badge->markResolved();
67-
yield [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret'), [$badge])];
66+
$ldapBadge = new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true);
67+
$userBadge = new UserBadge('test');
68+
$userBadge->setUserLoader(function () { return new InMemoryUser('test', 'pass', ['ROLE_USER']); });
69+
yield [new TestAuthenticator(), new Passport($userBadge, new PasswordCredentials('s3cret'), [$ldapBadge])];
6870
}
6971

7072
public function testPasswordCredentialsAlreadyResolvedThrowsException()
@@ -74,7 +76,7 @@ public function testPasswordCredentialsAlreadyResolvedThrowsException()
7476

7577
$badge = new PasswordCredentials('s3cret');
7678
$badge->markResolved();
77-
$passport = new Passport(new UserBadge('test'), $badge, [new LdapBadge('app.ldap')]);
79+
$passport = new Passport(new UserBadge('test'), $badge, [new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)]);
7880

7981
$listener = $this->createListener();
8082
$listener->onCheckPassport(new CheckPassportEvent(new TestAuthenticator(), $passport));
@@ -86,7 +88,7 @@ public function testInvalidLdapServiceId()
8688
$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?');
8789

8890
$listener = $this->createListener();
89-
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('not_existing_ldap_service')));
91+
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('not_existing_ldap_service', '{user_identifier}', '', '', null, true)));
9092
}
9193

9294
/**
@@ -104,7 +106,10 @@ public function testWrongPassport($passport)
104106
public static function provideWrongPassportData()
105107
{
106108
// no password credentials
107-
yield [new SelfValidatingPassport(new UserBadge('test'), [new LdapBadge('app.ldap')])];
109+
yield [new SelfValidatingPassport(
110+
new UserBadge('test'),
111+
[new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)]
112+
)];
108113
}
109114

110115
public function testEmptyPasswordShouldThrowAnException()
@@ -198,7 +203,7 @@ public function toArray(): array
198203
$this->ldap->expects($this->once())->method('query')->with('{user_identifier}', 'wouter_test')->willReturn($query);
199204

200205
$listener = $this->createListener();
201-
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test')));
206+
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test', true)));
202207
}
203208

204209
public function testEmptyQueryResultShouldThrowAnException()
@@ -226,14 +231,16 @@ public function testEmptyQueryResultShouldThrowAnException()
226231
$this->ldap->expects($this->once())->method('query')->willReturn($query);
227232

228233
$listener = $this->createListener();
229-
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test')));
234+
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test', true)));
230235
}
231236

232237
private function createEvent($password = 's3cr3t', $ldapBadge = null)
233238
{
239+
$ldapUser = new LdapUser(new Entry('cn=Wouter,dc=example,dc=com'), 'Wouter', null, ['ROLE_USER']);
240+
234241
return new CheckPassportEvent(
235242
new TestAuthenticator(),
236-
new Passport(new UserBadge('Wouter', fn () => new InMemoryUser('Wouter', null, ['ROLE_USER'])), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap')])
243+
new Passport(new UserBadge('Wouter', fn () => $ldapUser), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)])
237244
);
238245
}
239246

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Security;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
16+
use Symfony\Component\Ldap\Security\LdapBadge;
17+
18+
final class LdapBadgeTest extends TestCase
19+
{
20+
use ExpectDeprecationTrait;
21+
22+
/**
23+
* @group legacy
24+
*/
25+
public function testDeprecationOnResolvedInitialValue()
26+
{
27+
$this->expectDeprecation('Since symfony/ldap 6.4: Passing false as resolved initial value is deprecated, use true instead.');
28+
29+
new LdapBadge('foo');
30+
}
31+
32+
/**
33+
* @group legacy
34+
*/
35+
public function testDeprecationOnMarkAsResolved()
36+
{
37+
$this->expectDeprecation('Since symfony/ldap 6.4: Calling Symfony\Component\Ldap\Security\LdapBadge::markResolved is deprecated.');
38+
39+
$sut = new LdapBadge('foo', '{user_identifier}', '', '', null, true);
40+
$sut->markResolved();
41+
}
42+
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Core\Tests\Authentication\Provider;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Psr\Container\ContainerInterface;
16+
use Symfony\Component\EventDispatcher\EventDispatcher;
17+
use Symfony\Component\HttpFoundation\Request;
18+
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\HttpFoundation\Session\Session;
20+
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
21+
use Symfony\Component\Ldap\Adapter\AdapterInterface;
22+
use Symfony\Component\Ldap\Adapter\CollectionInterface;
23+
use Symfony\Component\Ldap\Adapter\ConnectionInterface;
24+
use Symfony\Component\Ldap\Adapter\QueryInterface;
25+
use Symfony\Component\Ldap\Entry;
26+
use Symfony\Component\Ldap\Exception\ConnectionException;
27+
use Symfony\Component\Ldap\Ldap;
28+
use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener;
29+
use Symfony\Component\Ldap\Security\LdapAuthenticator;
30+
use Symfony\Component\Ldap\Security\LdapUserProvider;
31+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
32+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
33+
use Symfony\Component\Security\Core\User\ChainUserProvider;
34+
use Symfony\Component\Security\Core\User\InMemoryUserProvider;
35+
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
36+
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
37+
use Symfony\Component\Security\Http\Authentication\AuthenticatorManager;
38+
use Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator;
39+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
40+
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
41+
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
42+
use Symfony\Component\Security\Http\EventListener\UserProviderListener;
43+
use Symfony\Component\Security\Http\HttpUtils;
44+
45+
class ChainProviderWithLdapTest extends TestCase
46+
{
47+
public function provideChainWithLdapAndInMemory(): array
48+
{
49+
return [
50+
'in memory' => ['foo', 'foopass'],
51+
'ldap' => ['bar', 'barpass'],
52+
];
53+
}
54+
55+
/**
56+
* @dataProvider provideChainWithLdapAndInMemory
57+
*/
58+
public function testChainWithLdapAndInMemory(string $userIdentifier, string $pass)
59+
{
60+
$inMemoryProvider = new InMemoryUserProvider([
61+
'foo' => ['password' => 'foopass', 'roles' => ['ROLE_USER']],
62+
]);
63+
64+
$ldapAdapteur = $this->createMock(AdapterInterface::class);
65+
$ldapAdapteur
66+
->method('getConnection')
67+
->willReturn($connection = $this->createMock(ConnectionInterface::class))
68+
;
69+
70+
$connection
71+
->method('bind')
72+
->willReturnCallback(static function (?string $user, ?string $pass): void {
73+
if ('admin' === $user && 'adminpass' === $pass) {
74+
return;
75+
}
76+
77+
if ('bar' === $user && 'barpass' === $pass) {
78+
return;
79+
}
80+
81+
throw new ConnectionException('failure when binding');
82+
})
83+
;
84+
85+
$ldapAdapteur
86+
->method('escape')
87+
->willReturnArgument(0)
88+
;
89+
90+
$ldapAdapteur
91+
->method('createQuery')
92+
->willReturn($query = $this->createMock(QueryInterface::class))
93+
;
94+
95+
$query
96+
->method('execute')
97+
->willReturn($collection = $this->createMock(CollectionInterface::class));
98+
99+
$collection
100+
->method('count')
101+
->willReturn(1)
102+
;
103+
104+
$collection
105+
->method('offsetGet')
106+
->with(0)
107+
->willReturn(new Entry('cn=bar,dc=example,dc=com', ['sAMAccountName' => ['bar'], 'userPassword' => ['barpass']]))
108+
;
109+
110+
$ldapProvider = new LdapUserProvider($ldap = new Ldap($ldapAdapteur), 'dc=example,dc=com', 'admin', 'adminpass', [], null, null, 'userPassword');
111+
112+
$chainUserProvider = new ChainUserProvider([$inMemoryProvider, $ldapProvider]);
113+
114+
$httpUtils = $this->createMock(HttpUtils::class);
115+
$httpUtils
116+
->method('checkRequestPath')
117+
->willReturn(true)
118+
;
119+
120+
$failureHandler = $this->createMock(AuthenticationFailureHandlerInterface::class);
121+
$failureHandler
122+
->method('onAuthenticationFailure')
123+
->willReturn(new Response())
124+
;
125+
126+
$formLoginAuthenticator = new FormLoginAuthenticator(
127+
$httpUtils,
128+
$chainUserProvider,
129+
$this->createMock(AuthenticationSuccessHandlerInterface::class),
130+
$failureHandler,
131+
[]
132+
);
133+
134+
$ldapAuthenticator = new LdapAuthenticator($formLoginAuthenticator, 'ldap-id');
135+
136+
$ldapLocator = new class($ldap) implements ContainerInterface {
137+
private $ldap;
138+
139+
public function __construct(Ldap $ldap)
140+
{
141+
$this->ldap = $ldap;
142+
}
143+
144+
public function get(string $id): Ldap
145+
{
146+
return $this->ldap;
147+
}
148+
149+
public function has(string $id): bool
150+
{
151+
return 'ldap-id' === $id;
152+
}
153+
};
154+
155+
$eventDispatcher = new EventDispatcher();
156+
$eventDispatcher->addListener(CheckPassportEvent::class, [new UserProviderListener($chainUserProvider), 'checkPassport']);
157+
$eventDispatcher->addListener(CheckPassportEvent::class, [new CheckLdapCredentialsListener($ldapLocator), 'onCheckPassport']);
158+
$eventDispatcher->addListener(CheckPassportEvent::class, function (CheckPassportEvent $event): void {
159+
$passport = $event->getPassport();
160+
$userBadge = $passport->getBadge(UserBadge::class);
161+
if (null === $userBadge || null === $userBadge->getUser()) {
162+
return;
163+
}
164+
$credentials = $passport->getBadge(PasswordCredentials::class);
165+
if ($credentials->isResolved()) {
166+
return;
167+
}
168+
169+
if ($credentials && 'foopass' === $credentials->getPassword()) {
170+
$credentials->markResolved();
171+
}
172+
});
173+
174+
$authenticatorManager = new AuthenticatorManager(
175+
[$ldapAuthenticator],
176+
$tokenStorage = new TokenStorage(),
177+
$eventDispatcher,
178+
'main'
179+
);
180+
181+
$request = Request::create('/login', 'POST', ['_username' => $userIdentifier, '_password' => $pass]);
182+
$request->setSession(new Session(new MockArraySessionStorage()));
183+
184+
$this->assertTrue($authenticatorManager->supports($request));
185+
$authenticatorManager->authenticateRequest($request);
186+
187+
$this->assertInstanceOf(UsernamePasswordToken::class, $token = $tokenStorage->getToken());
188+
$this->assertSame($userIdentifier, $token->getUserIdentifier());
189+
}
190+
}

src/Symfony/Component/Security/Core/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"symfony/expression-language": "^5.4|^6.0|^7.0",
3131
"symfony/http-foundation": "^5.4|^6.0|^7.0",
3232
"symfony/ldap": "^5.4|^6.0|^7.0",
33+
"symfony/security-http": "^5.4|^6.0|^7.0",
3334
"symfony/string": "^5.4|^6.0|^7.0",
3435
"symfony/translation": "^5.4|^6.0|^7.0",
3536
"symfony/validator": "^5.4|^6.0|^7.0",

0 commit comments

Comments
 (0)