You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
179 lines
5.1 KiB
179 lines
5.1 KiB
<?php |
|
|
|
namespace App\Services; |
|
|
|
use RuntimeException; |
|
|
|
class LdapAuthService |
|
{ |
|
public function __construct( |
|
private readonly LdapConnection $ldap |
|
) { |
|
} |
|
|
|
/** |
|
* @return array{username: string, display_name: string, email: string, is_admin: bool}|null |
|
*/ |
|
public function attempt(string $username, string $password): ?array |
|
{ |
|
$username = trim($username); |
|
|
|
if ($username === '' || $password === '') { |
|
return null; |
|
} |
|
|
|
if (! preg_match('/^[a-zA-Z0-9._@-]+$/', $username)) { |
|
return null; |
|
} |
|
|
|
$sam = str_contains($username, '@') ? explode('@', $username)[0] : $username; |
|
$domain = $this->ldap->domainFromBaseDn(); |
|
$userInfo = $this->lookupUser($sam); |
|
|
|
if ($userInfo === null) { |
|
return null; |
|
} |
|
|
|
$identities = array_unique(array_filter([ |
|
$userInfo['dn'], |
|
$sam . '@' . $domain, |
|
$username, |
|
])); |
|
|
|
foreach ($identities as $identity) { |
|
if ($this->tryBind($identity, $password)) { |
|
return [ |
|
'username' => $sam, |
|
'display_name' => $userInfo['display_name'], |
|
'email' => $userInfo['email'], |
|
'is_admin' => $this->isAdministrator($userInfo['groups']), |
|
]; |
|
} |
|
} |
|
|
|
return null; |
|
} |
|
|
|
public function isAdministrator(array $groups): bool |
|
{ |
|
$adminGroups = array_map( |
|
'mb_strtolower', |
|
config('ldap.admin_groups', ['Domain Admins', 'Administrators']) |
|
); |
|
|
|
foreach ($groups as $group) { |
|
if (in_array(mb_strtolower($group), $adminGroups, true)) { |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
/** |
|
* @return array{dn: string, display_name: string, email: string, groups: list<string>}|null |
|
*/ |
|
public function lookupUser(string $username): ?array |
|
{ |
|
$connection = null; |
|
|
|
try { |
|
$connection = $this->ldap->bind(); |
|
$filter = '(sAMAccountName=' . ldap_escape($username, '', LDAP_ESCAPE_FILTER) . ')'; |
|
$result = @ldap_search( |
|
$connection, |
|
config('ldap.base_dn'), |
|
$filter, |
|
['dn', 'displayname', 'mail', 'memberof', 'useraccountcontrol', 'lastlogontimestamp', 'samaccountname'] |
|
); |
|
|
|
if ($result === false) { |
|
return null; |
|
} |
|
|
|
$entries = ldap_get_entries($connection, $result); |
|
|
|
if (($entries['count'] ?? 0) < 1) { |
|
return null; |
|
} |
|
|
|
$entry = $entries[0]; |
|
$uac = (int) ($this->ldap->attributeValue($entry, 'useraccountcontrol') ?? 0); |
|
|
|
if ($uac & 2) { |
|
return null; |
|
} |
|
|
|
$groups = []; |
|
|
|
if (isset($entry['memberof']) && is_array($entry['memberof'])) { |
|
for ($i = 0; $i < ($entry['memberof']['count'] ?? 0); $i++) { |
|
$dn = $entry['memberof'][$i] ?? ''; |
|
|
|
if (preg_match('/^CN=([^,]+)/i', $dn, $matches)) { |
|
$groups[] = $matches[1]; |
|
} |
|
} |
|
} |
|
|
|
sort($groups); |
|
|
|
return [ |
|
'username' => $this->ldap->attributeValue($entry, 'samaccountname') ?? $username, |
|
'dn' => $entry['dn'], |
|
'display_name' => $this->ldap->attributeValue($entry, 'displayname') ?? $username, |
|
'email' => $this->ldap->attributeValue($entry, 'mail') ?? '', |
|
'enabled' => ! ($uac & 2), |
|
'last_logon' => $this->filetimeToDate($this->ldap->attributeValue($entry, 'lastlogontimestamp')), |
|
'groups' => $groups, |
|
'is_admin' => $this->isAdministrator($groups), |
|
'domain' => $this->ldap->domainFromBaseDn(), |
|
]; |
|
} catch (RuntimeException) { |
|
return null; |
|
} finally { |
|
if ($connection !== null) { |
|
@ldap_unbind($connection); |
|
} |
|
} |
|
} |
|
|
|
public function getProfile(string $username): ?array |
|
{ |
|
return $this->lookupUser($username); |
|
} |
|
|
|
private function filetimeToDate(?string $filetime): ?string |
|
{ |
|
if ($filetime === null || $filetime === '' || $filetime === '0') { |
|
return null; |
|
} |
|
|
|
$unix = ((int) $filetime / 10000000) - 11644473600; |
|
|
|
if ($unix <= 0) { |
|
return null; |
|
} |
|
|
|
return gmdate('Y-m-d H:i:s', $unix); |
|
} |
|
|
|
private function tryBind(string $identity, string $password): bool |
|
{ |
|
$connection = null; |
|
|
|
try { |
|
$connection = $this->ldap->connect(ssl: false); |
|
ldap_set_option($connection, LDAP_OPT_PROTOCOL_VERSION, 3); |
|
ldap_set_option($connection, LDAP_OPT_REFERRALS, 0); |
|
|
|
return @ldap_bind($connection, $identity, $password); |
|
} catch (RuntimeException) { |
|
return false; |
|
} finally { |
|
if ($connection !== null) { |
|
@ldap_unbind($connection); |
|
} |
|
} |
|
} |
|
}
|
|
|