Skip to content

Commit 333493a

Browse files
tvdijendlundgren
andauthored
AD support (#32)
* Refactor Utils\Ldap to Connector\Ldap Co-authored-by: David Lundgren <[email protected]>
1 parent 3cfb54e commit 333493a

File tree

12 files changed

+753
-328
lines changed

12 files changed

+753
-328
lines changed

docs/ldap.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ authentication source:
6161
'network_timeout' => 3,
6262
],
6363

64+
/**
65+
* The connector to use.
66+
* Defaults to '\SimpleSAML\Module\ldap\Connector\Ldap', but can be set
67+
* to '\SimpleSAML\Module\ldap\Connector\ActiveDirectory' when
68+
* authenticating against Microsoft Active Directory. This will
69+
* provide you with more specific error messages.
70+
*/
71+
'connector' => '\SimpleSAML\Module\ldap\Connector\Ldap',
72+
6473
/**
6574
* Which attributes should be retrieved from the LDAP server.
6675
* This can be an array of attribute names, or NULL, in which case
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
<?php
2+
3+
namespace SimpleSAML\Module\ldap\Auth;
4+
5+
use function array_merge_recursive;
6+
use function explode;
7+
use function in_array;
8+
use function preg_match;
9+
use function strpos;
10+
use function str_replace;
11+
12+
/**
13+
* Class representing an InvalidCredential Result
14+
*
15+
* This is used for extended diagnostic information
16+
*
17+
* @package simplesamlphp/simplesamlphp-module-ldap
18+
*/
19+
class InvalidCredentialResult
20+
{
21+
/**
22+
* List of Active Directory Bind Error Short Description's
23+
*
24+
* @see https://ldapwiki.com/wiki/Common%20Active%20Directory%20Bind%20Errors
25+
*/
26+
public const LDAP_NO_SUCH_OBJECT = '525';
27+
public const ERROR_LOGON_FAILURE = '52e';
28+
public const ERROR_ACCOUNT_RESTRICTION = '52f';
29+
public const ERROR_INVALID_LOGON_HOURS = '530';
30+
public const ERROR_INVALID_WORKSTATION = '531';
31+
public const ERROR_PASSWORD_EXPIRED = '532';
32+
public const ERROR_ACCOUNT_DISABLED = '533';
33+
public const ERROR_TOO_MANY_CONTEXT_IDS = '568';
34+
public const ERROR_ACCOUNT_EXPIRED = '701';
35+
public const ERROR_PASSWORD_MUST_CHANGE = '773';
36+
public const ERROR_ACCOUNT_LOCKED_OUT = '775';
37+
38+
/**
39+
* List of Simple Bind error codes
40+
*
41+
* N.B. - This is an incomplete list
42+
*/
43+
public const NT_STATUS_PASSWORD_EXPIRED = 'PASSWORD_EXPIRED';
44+
public const NT_STATUS_PASSWORD_MUST_CHANGE = 'PASSWORD_MUST_CHANGE';
45+
public const NT_STATUS_LOGON_FAILURE = 'LOGON_FAILURE';
46+
47+
/**
48+
* List of keys for the code mapping
49+
*/
50+
public const KEY_INVALID_CREDENTIAL = 'invalid_credential';
51+
public const KEY_PASSWORD_ERROR = 'password_error';
52+
public const KEY_ACCOUNT_ERROR = 'account_error';
53+
public const KEY_RESTRICTION = 'restriction';
54+
55+
/**
56+
* Map of keys to check the code against when using is* methods
57+
*
58+
* @var array
59+
*/
60+
protected array $codeMap = [
61+
self::KEY_INVALID_CREDENTIAL => [
62+
self::ERROR_LOGON_FAILURE,
63+
self::LDAP_NO_SUCH_OBJECT,
64+
self::NT_STATUS_LOGON_FAILURE,
65+
],
66+
self::KEY_PASSWORD_ERROR => [
67+
self::ERROR_PASSWORD_EXPIRED,
68+
self::ERROR_PASSWORD_MUST_CHANGE,
69+
self::NT_STATUS_PASSWORD_EXPIRED,
70+
self::NT_STATUS_PASSWORD_MUST_CHANGE,
71+
],
72+
self::KEY_ACCOUNT_ERROR => [
73+
self::ERROR_ACCOUNT_DISABLED,
74+
self::ERROR_ACCOUNT_EXPIRED,
75+
self::ERROR_ACCOUNT_LOCKED_OUT,
76+
],
77+
self::KEY_RESTRICTION => [
78+
self::ERROR_ACCOUNT_RESTRICTION,
79+
self::ERROR_INVALID_LOGON_HOURS,
80+
self::ERROR_INVALID_WORKSTATION,
81+
self::ERROR_TOO_MANY_CONTEXT_IDS,
82+
],
83+
];
84+
85+
/**
86+
* For Simple Binds this is the part after NT_STATUS_
87+
* Otherwise it is the HEX code from `data ([0-9a-f]+)`
88+
*
89+
* @var string The error code.
90+
*/
91+
protected string $code;
92+
93+
/**
94+
* @var string the message as it came from LDAP
95+
*/
96+
protected string $rawMessage;
97+
98+
99+
/**
100+
* Parses the message when possible to determine what the actual error is
101+
*
102+
* @param string $message
103+
*
104+
* @return \SimpleSAML\Module\ldap\Auth\InvalidCredentialResult
105+
*/
106+
public static function fromDiagnosticMessage(string $message): self
107+
{
108+
if (strpos($message, 'Simple Bind Failed:') === 0) {
109+
list(, $tmp) = explode(':', $message, 2);
110+
$code = str_replace('NT_STATUS_', '', $tmp);
111+
} elseif (preg_match('/data\s(.*)?,/', $message, $match)) {
112+
$code = $match[1];
113+
}
114+
115+
return new self($code, $message);
116+
}
117+
118+
119+
/**
120+
* @param string $code
121+
* @param string $rawMessage
122+
*/
123+
protected function __construct(string $code, string $rawMessage)
124+
{
125+
$this->code = $code;
126+
$this->rawMessage = $rawMessage;
127+
}
128+
129+
130+
/**
131+
* Returns the code that was pulled from the raw message
132+
*
133+
* @return string
134+
*/
135+
public function getCode(): string
136+
{
137+
return $this->code;
138+
}
139+
140+
141+
/**
142+
* Returns the raw message
143+
*
144+
* @return string
145+
*/
146+
public function getRawMessage(): string
147+
{
148+
return $this->rawMessage;
149+
}
150+
151+
152+
/**
153+
* Allows the default code mappings to be updated
154+
* @param array $codes
155+
* @return void
156+
*/
157+
public function updateCodeMap(array $codes): void
158+
{
159+
$this->codeMap = array_merge_recursive($this->codeMap, $codes);
160+
}
161+
162+
163+
/**
164+
* Allows the default code mappings to be replaced
165+
*
166+
* @param array $codes
167+
* @return void
168+
*/
169+
public function replaceCodeMap(array $codes): void
170+
{
171+
$this->codeMap = $codes;
172+
}
173+
174+
175+
/**
176+
* @return bool Whether or not the password had an error
177+
*/
178+
public function isPasswordError(): bool
179+
{
180+
return in_array($this->code, $this->codeMap[self::KEY_PASSWORD_ERROR]);
181+
}
182+
183+
184+
/**
185+
* @return bool Whether or not the account had an error
186+
*/
187+
public function isAccountError(): bool
188+
{
189+
return in_array($this->code, $this->codeMap[self::KEY_ACCOUNT_ERROR]);
190+
}
191+
192+
193+
/**
194+
* @return bool Whether or not there was an auth problem
195+
*/
196+
public function isInvalidCredential(): bool
197+
{
198+
return in_array($this->code, $this->codeMap[self::KEY_INVALID_CREDENTIAL]);
199+
}
200+
201+
202+
/**
203+
* @return bool Whether or not there is a restriction in place
204+
*/
205+
public function isRestricted(): bool
206+
{
207+
return in_array($this->code, $this->codeMap[self::KEY_RESTRICTION]);
208+
}
209+
}

lib/Auth/Process/AttributeAddFromLDAP.php

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use Exception;
1414
use SimpleSAML\Assert\Assert;
1515
use SimpleSAML\Logger;
16-
use SimpleSAML\Module\ldap\Utils\Ldap as LdapUtils;
1716
use Symfony\Component\Ldap\Adapter\ExtLdap\Query;
1817

1918
class AttributeAddFromLDAP extends BaseFilter
@@ -90,16 +89,14 @@ public function process(array &$state): void
9089
Assert::keyExists($state, 'Attributes');
9190
$attributes = &$state['Attributes'];
9291

93-
$ldapUtils = new LdapUtils();
94-
9592
// perform a merge on the ldap_search_filter
9693
// loop over the attributes and build the search and replace arrays
9794
$arrSearch = $arrReplace = [];
9895
foreach ($attributes as $attr => $val) {
9996
$arrSearch[] = '%' . $attr . '%';
10097

10198
if (strlen($val[0]) > 0) {
102-
$arrReplace[] = $ldapUtils->escapeFilterValue($val[0], true);
99+
$arrReplace[] = $this->connector->escapeFilterValue($val[0], true);
103100
} else {
104101
$arrReplace[] = '';
105102
}
@@ -117,15 +114,14 @@ public function process(array &$state): void
117114
return;
118115
}
119116

120-
$ldapUtils->bind($this->ldapObject, $this->searchUsername, $this->searchPassword);
117+
$this->connector->bind($this->searchUsername, $this->searchPassword);
121118

122119
$options = [
123120
'scope' => $this->config->getOptionalString('search.scope', Query::SCOPE_SUB),
124121
'timeout' => $this->config->getOptionalInteger('timeout', 3),
125122
];
126123

127-
$entries = $ldapUtils->searchForMultiple(
128-
$this->ldapObject,
124+
$entries = $this->connector->searchForMultiple(
129125
$this->searchBase,
130126
$filter,
131127
$options,

lib/Auth/Process/AttributeAddUsersGroups.php

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use SimpleSAML\Assert\Assert;
1616
use SimpleSAML\Error;
1717
use SimpleSAML\Logger;
18-
use SimpleSAML\Module\ldap\Utils\Ldap as LdapUtils;
1918
use SimpleSAML\Utils;
2019
use Symfony\Component\Ldap\Adapter\ExtLdap\Query;
2120

@@ -136,8 +135,7 @@ protected function getGroups(array $attributes): array
136135
$this->title
137136
));
138137

139-
$ldapUtils = new LdapUtils();
140-
$ldapUtils->bind($this->ldapObject, $this->searchUsername, $this->searchPassword);
138+
$this->connector->bind($this->searchUsername, $this->searchPassword);
141139

142140
$options = [
143141
'scope' => $this->config->getOptionalString('search.scope', Query::SCOPE_SUB),
@@ -208,8 +206,7 @@ protected function getGroups(array $attributes): array
208206
$attributes[$dn_attribute][0],
209207
);
210208

211-
$entries = $ldapUtils->searchForMultiple(
212-
$this->ldapObject,
209+
$entries = $this->connector->searchForMultiple(
213210
$this->searchBase,
214211
$filter,
215212
$options,
@@ -239,8 +236,7 @@ protected function getGroups(array $attributes): array
239236
$attributes[$map['username']][0]
240237
);
241238

242-
$entries = $ldapUtils->searchForMultiple(
243-
$this->ldapObject,
239+
$entries = $this->connector->searchForMultiple(
244240
$this->searchBase,
245241
$filter,
246242
$options,
@@ -353,8 +349,9 @@ protected function search(array $memberOf, array $options): array
353349

354350
// Init the groups variable
355351
$entries = [];
356-
$ldapUtils = new LdapUtils();
357-
$options['scope'] = 'base';
352+
353+
// Set scope to 'base'
354+
$options['scope'] = Query::SCOPE_BASE;
358355

359356
// Check each DN of the passed memberOf
360357
foreach ($memberOf as $dn) {
@@ -368,8 +365,7 @@ protected function search(array $memberOf, array $options): array
368365
$searched[$dn] = $dn;
369366

370367
// Query LDAP for the attribute values for the DN
371-
$entry = $ldapUtils->search(
372-
$this->ldapObject,
368+
$entry = $this->connector->search(
373369
[$dn],
374370
sprintf("(%s=%s)", $map['type'], $this->type_map['group']),
375371
$options,

0 commit comments

Comments
 (0)