Skip to content

Commit 4c6c323

Browse files
feature symfony#58042 [Ldap] Add support for sasl_bind and whoami LDAP operations (manu0401)
This PR was merged into the 7.2 branch. Discussion ---------- [Ldap] Add support for sasl_bind and whoami LDAP operations | Q | A | ------------- | --- | Branch? | 7.2 | Bug fix? | yes | New feature? | yes | Deprecations? | no | Issues | | License | MIT SASL bind let the caller supply various options, including proxy authentication, where one user uses its own credentials to login as another one (subjected to LDAP directory access contro usingl authzFrom/authzTo attributes). In this case, ldapwhoami is used to retreive the resulting authenticated and authorized DN after bind success. Tested with SimpleSAMLphp 2.2.2 with minor patches. Commits ------- 0df0653 [Ldap] Add support for sasl_bind and whoami LDAP operations
2 parents 5b35328 + 0df0653 commit 4c6c323

File tree

7 files changed

+124
-13
lines changed

7 files changed

+124
-13
lines changed

UPGRADE-7.2.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ FrameworkBundle
2929

3030
* [BC BREAK] The `secrets:decrypt-to-local` command terminates with a non-zero exit code when a secret could not be read
3131

32+
Ldap
33+
----
34+
35+
* Add methods for `saslBind()` and `whoami()` to `ConnectionInterface` and `LdapInterface`
36+
3237
Messenger
3338
---------
3439

src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,13 @@
1414
use Symfony\Component\Ldap\Exception\AlreadyExistsException;
1515
use Symfony\Component\Ldap\Exception\ConnectionTimeoutException;
1616
use Symfony\Component\Ldap\Exception\InvalidCredentialsException;
17+
use Symfony\Component\Ldap\Exception\LdapException;
1718

1819
/**
1920
* @author Charles Sarrazin <charles@sarraz.in>
21+
*
22+
* @method void saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null)
23+
* @method string whoami()
2024
*/
2125
interface ConnectionInterface
2226
{
@@ -33,4 +37,19 @@ public function isBound(): bool;
3337
* @throws InvalidCredentialsException When the connection can't be created because of an LDAP_INVALID_CREDENTIALS error
3438
*/
3539
public function bind(?string $dn = null, #[\SensitiveParameter] ?string $password = null): void;
40+
41+
/*
42+
* Binds the connection against a user's DN and password using SASL.
43+
*
44+
* @throws LdapException When SASL support is not available
45+
* @throws AlreadyExistsException When the connection can't be created because of an LDAP_ALREADY_EXISTS error
46+
* @throws ConnectionTimeoutException When the connection can't be created because of an LDAP_TIMEOUT error
47+
* @throws InvalidCredentialsException When the connection can't be created because of an LDAP_INVALID_CREDENTIALS error
48+
*/
49+
// public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void;
50+
51+
/*
52+
* Return authenticated and authorized (for SASL) DN.
53+
*/
54+
// public function whoami(): string;
3655
}

src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,21 +70,69 @@ public function bind(?string $dn = null, #[\SensitiveParameter] ?string $passwor
7070

7171
if (false === @ldap_bind($this->connection, $dn, $password)) {
7272
$error = ldap_error($this->connection);
73-
switch (ldap_errno($this->connection)) {
74-
case self::LDAP_INVALID_CREDENTIALS:
75-
throw new InvalidCredentialsException($error);
76-
case self::LDAP_TIMEOUT:
77-
throw new ConnectionTimeoutException($error);
78-
case self::LDAP_ALREADY_EXISTS:
79-
throw new AlreadyExistsException($error);
80-
}
81-
ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic_message);
82-
throw new ConnectionException($error.' '.$diagnostic_message);
73+
ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic);
74+
75+
throw match (ldap_errno($this->connection)) {
76+
self::LDAP_INVALID_CREDENTIALS => new InvalidCredentialsException($error),
77+
self::LDAP_TIMEOUT => new ConnectionTimeoutException($error),
78+
self::LDAP_ALREADY_EXISTS => new AlreadyExistsException($error),
79+
default => new ConnectionException($error.' '.$diagnostic),
80+
};
81+
}
82+
83+
$this->bound = true;
84+
}
85+
86+
/**
87+
* @param string $password WARNING: When the LDAP server allows unauthenticated binds, a blank $password will always be valid
88+
*/
89+
public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void
90+
{
91+
if (!\function_exists('ldap_sasl_bind')) {
92+
throw new LdapException('The LDAP extension is missing SASL support.');
93+
}
94+
95+
if (!$this->connection) {
96+
$this->connect();
97+
}
98+
99+
if (false === @ldap_sasl_bind($this->connection, $dn, $password, $mech, $realm, $authcId, $authzId, $props)) {
100+
$error = ldap_error($this->connection);
101+
ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic);
102+
103+
throw match (ldap_errno($this->connection)) {
104+
self::LDAP_INVALID_CREDENTIALS => new InvalidCredentialsException($error),
105+
self::LDAP_TIMEOUT => new ConnectionTimeoutException($error),
106+
self::LDAP_ALREADY_EXISTS => new AlreadyExistsException($error),
107+
default => new ConnectionException($error.' '.$diagnostic),
108+
};
83109
}
84110

85111
$this->bound = true;
86112
}
87113

114+
/**
115+
* ldap_exop_whoami accessor, returns authenticated DN.
116+
*/
117+
public function whoami(): string
118+
{
119+
if (false === $authzId = ldap_exop_whoami($this->connection)) {
120+
throw new LdapException(ldap_error($this->connection));
121+
}
122+
123+
$parts = explode(':', $authzId, 2);
124+
if ('dn' !== $parts[0]) {
125+
/*
126+
* We currently do not handle u:login authzId, which
127+
* would require a configuration-dependent LDAP search
128+
* to be turned into a DN
129+
*/
130+
throw new LdapException(\sprintf('Unsupported authzId "%s".', $authzId));
131+
}
132+
133+
return $parts[1];
134+
}
135+
88136
/**
89137
* @internal
90138
*/

src/Symfony/Component/Ldap/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.2
5+
---
6+
7+
* Add methods for `saslBind()` and `whoami()` to `ConnectionInterface` and `LdapInterface`
8+
49
7.1
510
---
611

src/Symfony/Component/Ldap/Ldap.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@ public function bind(?string $dn = null, #[\SensitiveParameter] ?string $passwor
3232
$this->adapter->getConnection()->bind($dn, $password);
3333
}
3434

35+
public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void
36+
{
37+
$this->adapter->getConnection()->saslBind($dn, $password, $mech, $realm, $authcId, $authzId, $props);
38+
}
39+
40+
public function whoami(): string
41+
{
42+
return $this->adapter->getConnection()->whoami();
43+
}
44+
3545
public function query(string $dn, string $query, array $options = []): QueryInterface
3646
{
3747
return $this->adapter->createQuery($dn, $query, $options);

src/Symfony/Component/Ldap/LdapInterface.php

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,35 @@
1616
use Symfony\Component\Ldap\Exception\ConnectionException;
1717

1818
/**
19-
* Ldap interface.
20-
*
2119
* @author Charles Sarrazin <charles@sarraz.in>
20+
*
21+
* @method void saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null)
22+
* @method string whoami()
2223
*/
2324
interface LdapInterface
2425
{
2526
public const ESCAPE_FILTER = 0x01;
2627
public const ESCAPE_DN = 0x02;
2728

2829
/**
29-
* Return a connection bound to the ldap.
30+
* Returns a connection bound to the ldap.
3031
*
3132
* @throws ConnectionException if dn / password could not be bound
3233
*/
3334
public function bind(?string $dn = null, #[\SensitiveParameter] ?string $password = null): void;
3435

36+
/**
37+
* Returns a connection bound to the ldap using SASL.
38+
*
39+
* @throws ConnectionException if dn / password could not be bound
40+
*/
41+
// public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void;
42+
43+
/**
44+
* Returns authenticated and authorized (for SASL) DN.
45+
*/
46+
// public function whoami(): string;
47+
3548
/**
3649
* Queries a ldap server for entries matching the given criteria.
3750
*/

src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ public function testLdapEscape()
3434
$this->assertEquals('\20foo\3dbar\0d(baz)*\20', $ldap->escape(" foo=bar\r(baz)* ", '', LdapInterface::ESCAPE_DN));
3535
}
3636

37+
/**
38+
* @group functional
39+
*/
40+
public function testSaslBind()
41+
{
42+
$ldap = new Adapter($this->getLdapConfig());
43+
44+
$ldap->getConnection()->saslBind('cn=admin,dc=symfony,dc=com', 'symfony');
45+
$this->assertEquals('cn=admin,dc=symfony,dc=com', $ldap->getConnection()->whoami());
46+
}
47+
3748
/**
3849
* @group functional
3950
*/

0 commit comments

Comments
 (0)