GeoNetAgent, LDAPWeb, server-audit, server-connection
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

<?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);
}
}
}
}