* @package Mammut\Auth */ class AuthADS extends \Mammut\StrictObject { const NORMAL_ACCOUNT = 805306368; const WORKSTATION_TRUST = 805306369; const INTERDOMAIN_TRUST = 805306370; const SECURITY_GLOBAL_GROUP = 268435456; const DISTRIBUTION_GROUP = 268435457; const SECURITY_LOCAL_GROUP = 536870912; const DISTRIBUTION_LOCAL_GROUP = 536870913; const CONTAINER = 'DC'; const FOLDER = 'OU'; const NAME = 'CN'; // Link: https://msdn.microsoft.com/en-us/library/cc223145.aspx // UF_??? = 1 Unused. Must be zero and ignored. const UF_ACCOUNT_DISABLE = 2; // UF_??? = 4 Unused. Must be zero and ignored. const UF_HOMEDIR_REQUIRED = 8; const UF_LOCKOUT = 16; const UF_PASSWD_NOTREQD = 32; const UF_PASSWD_CANT_CHANGE = 64; const UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED = 128; // UF_??? = 256 Unused. Must be zero and ignored. const UF_NORMAL_ACCOUNT = 512; // UF_??? = 1024 Unused. Must be zero and ignored. const UF_INTERDOMAIN_TRUST_ACCOUNT = 2048; const UF_WORKSTATION_TRUST_ACCOUNT = 4096; const UF_SERVER_TRUST_ACCOUNT = 8192; // UF_??? = 16384 Unused. Must be zero and ignored. // UF_??? = 32768 Unused. Must be zero and ignored. const UF_DONT_EXPIRE_PASSWD = 65536; const UF_MNS_LOGON_ACCOUNT = 131072; const UF_SMARTCARD_REQUIRED = 262144; const UF_TRUSTED_FOR_DELEGATION = 524288; const UF_NOT_DELEGATED = 1048576; const UF_USE_DES_KEY_ONLY = 2097152; const UF_DONT_REQUIRE_PREAUTH = 4194304; const UF_PASSWORD_EXPIRED = 8388608; const UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION = 16777216; const UF_NO_AUTH_DATA_REQUIRED = 33554432; const UF_PARTIAL_SECRETS_ACCOUNT = 67108864; // UF_??? = 134217728 Unused. Must be zero and ignored. // UF_??? = 268435456 Unused. Must be zero and ignored. // UF_??? = 536870912 Unused. Must be zero and ignored. // UF_??? = 1073741824 Unused. Must be zero and ignored. // UF_??? = 2147483648 Unused. Must be zero and ignored. /** * The domain name * * @var string */ private $domain = NULL; /** * The domain controllers * * @var array */ private $dc = array(); private $ctrlUser = NULL; private $ctrlPasswd = NULL; private $secure; private $ldap; private $bind; private $usedDC = false; /** * Creates a new AuthADS object * * @param string $domain * the name of the domain * @param string|array $domController * the domaincontroller or a list of controllers * @param boolean $secure * true if TLS/SSL should be used * @throws \Mammut\Exception\ExtensionException */ public function __construct($domain, $domController, $secure = false) { if(!extension_loaded('ldap')) throw new \Mammut\Exception\ExtensionException('ldap'); $this->domain = $domain; $this->dc = is_array($domController) ? $domController : array((string) $domController); $this->secure = $secure; } public function connect($ctrlUser = false, $passwd = false) { $dcs = $this->getDCList(); $this->ctrlUser = $ctrlUser; $this->ctrlPasswd = $passwd; foreach($dcs as $dc) { try { $port = 389; if(substr_count(':', $dc) == 1) list($dc, $port) = explode(':', $dc, 2); $conString = $this->secure ? 'ldaps://' . $dc : $dc; $this->ldap = ldap_connect($conString, $port); ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, 0); if($ctrlUser) $this->bind = @ldap_bind($this->ldap, $ctrlUser, $passwd); else $this->bind = @ldap_bind($this->ldap); if($this->bind) { $this->usedDC = $dc; return; } } catch(\Exception $e) { echo get_class($e).':'.$e->getMessage() . "\n"; } } throw new AuthenticationException(@ldap_error($this->ldap), @ldap_errno($this->ldap)); } public function disconnect() { ldap_unbind($this->ldap); } public function connectSSON() { } public static function convertLittleEndian($hex) { $result = ''; for($x = strlen($hex) - 2; $x >= 0; $x = $x - 2) $result .= substr($hex, $x, 2); return $result; } public static function bin2ldap($bin) { return self::escapeHex(bin2hex($bin)); } public static function escapeHex($hex) { $result = ''; for ($i = 0; $i < strlen($hex)/2; $i++) $result .= '\\'.substr($hex,$i*2,2); return $result; } /** * Converts a binary value to a windows SID stromg * * @param string $val */ protected static function bin2sid($val) { $hex = bin2hex($val); $rev = hexdec(substr($hex, 0, 2)); $subcount = hexdec(substr($hex, 2, 2)); $auth = hexdec(substr($hex, 4, 12)); $result = "S-{$rev}-{$auth}"; for($i = 0; $i < $subcount; $i++) $result .= "-" . hexdec(self::convertLittleEndian(substr($hex, 16 + ($i * 8), 8))); return $result; } /** * Converts a SID string to a binary value. * * @param string $val */ protected static function sid2bin($val) { $parts = explode('-', $val); unset($parts[0]); $hex_sid = sprintf('%1$02d', dechex($parts[1])); $hex_sid .= sprintf('%1$02d', dechex(count($parts)-2)); $hex_sid .= sprintf('%1$012d', dechex($parts[2])); for ($i = 3; $i <= count($parts); $i++) { $hex = sprintf("%1$08s",base_convert($parts[$i], 10, 16)); $hex_sid .= self::convertLittleEndian($hex); } $result = pack("H*", $hex_sid); return $result; } /** * Converts a binary value to a windows GUID. * * @param string $object_guid */ protected static function bin2guid($object_guid) { $hex_guid = bin2hex($object_guid); $hex_guid_to_guid_str = ''; for($k = 1; $k <= 4; ++$k) { $hex_guid_to_guid_str .= substr($hex_guid, 8 - 2 * $k, 2); } $hex_guid_to_guid_str .= '-'; for($k = 1; $k <= 2; ++$k) { $hex_guid_to_guid_str .= substr($hex_guid, 12 - 2 * $k, 2); } $hex_guid_to_guid_str .= '-'; for($k = 1; $k <= 2; ++$k) { $hex_guid_to_guid_str .= substr($hex_guid, 16 - 2 * $k, 2); } $hex_guid_to_guid_str .= '-' . substr($hex_guid, 16, 4); $hex_guid_to_guid_str .= '-' . substr($hex_guid, 20); return $hex_guid_to_guid_str; } /** * Converts a windows GUID to a binary value * * @param string $hex_guid_to_guid_str */ protected static function guid2bin($hex_guid_to_guid_str) { $parts = explode('-', $hex_guid_to_guid_str); for($i = 0; $i < 3; $i++) { $in = $parts[$i]; $conv = ''; for($k = 1; $k <= strlen($in) / 2; ++$k) { $conv .= substr($in, strlen($in) - 2 * $k, 2); } $parts[$i] = $conv; } $hex_guid = implode('', $parts); $object_guid = pack("H*", $hex_guid); return $object_guid; } /** * Converts a LDAP result element to a more usefull array * * @param array $e * the input * @return array the converted data */ protected static function simplifyLDAPEntry(array $e) { $result = array(); if($e['count'] == 1) { if(is_array($e[0])) $e[0] = self::simplifyLDAPEntry($e[0]); $result = $e[0]; } else { for($j = 0; $j < $e['count']; $j++) { $key = strtolower($e[$j]); if(in_array($key, array('logonhours','objectsid','objectguid'))) { switch($key) { case 'logonhours': $result[$e[$j]] = bindec($e[$e[$j]][0]); break; case 'objectsid': $result[$e[$j]] = self::bin2sid($e[$e[$j]][0]); break; case 'objectguid': $result[$e[$j]] = self::bin2guid($e[$e[$j]][0]); break; default: $result[$e[$j]] = bin2hex($e[$e[$j]][0]); break; } } elseif(key_exists($e[$j], $e) && is_array($e[$e[$j]])) { $result[$e[$j]] = self::simplifyLDAPEntry($e[$e[$j]]); if($e[$j] == 'memberof') { if(is_array($result[$e[$j]])) { foreach($result[$e[$j]] as $key=>$val) $result[$e[$j]][$key] = substr($val, 3, strpos($val, ',') - 3); } else // single group users dosen't return an array $result[$e[$j]] = substr($result[$e[$j]], 3, strpos($result[$e[$j]], ',') - 3); } } else { $result[] = $e[$j]; } } } return $result; } private function checkPreconditions() { if(is_null($this->domain)) throw new IllegalStateException('No domain defined'); if(count($this->dc) == 0) throw new IllegalStateException('No DCs defined'); } private function domainUserName($name) { return $name . '@' . $this->domain; } public function setDomain($dom) { $this->domain = $dom; } public function getDomain() { return $this->domain; } /** * * @return array the domain controller list */ public function getDomainControllers() { return $this->dc; } protected function getDC() { if(!$this->usedDC) $this->usedDC = $this->dc[rand(0, count($this->dc) - 1)]; return $this->usedDC; } protected function getDCList() { $result = $this->dc; shuffle($result); return $result; } public function getDomainSid($binary = false) { if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { $result = ldap_read($this->ldap, $this->getLDAPBase(), '(objectclass=*)',array('distinguishedName','objectGUID','objectSid')); if(($err = ldap_errno($this->ldap)) != 0) throw new \Exception(ldap_err2str($err)); if(ldap_count_entries($this->ldap, $result) != 1) { ldap_free_result($result); throw new \InvalidArgumentException(); } $entry = ldap_first_entry($this->ldap, $result); $attributes = ldap_get_attributes($this->ldap, $entry); ldap_free_result($result); if (!$binary) { $attributes = self::simplifyLDAPEntry($attributes); return $attributes['objectSid']; } else return $attributes['objectSid'][0]; } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($name); } } protected function getLDAPBase() { return self::CONTAINER.'=' . str_replace('.', ','.self::CONTAINER.'=', $this->domain); } /** * authenticates a user on the active directory * * @param string $user * the username * @param string $password * the password * @return boolean true on success, false otherwise */ public function authenticate($user, $password) { if(empty($user)) return false; $this->checkPreconditions(); $user = $this->domainUserName($user); $dc = $this->getDC(); $port = 389; if(substr_count(':', $dc) == 1) list($dc, $port) = explode(':', $dc, 2); $conString = $this->secure ? 'ldaps://' . $dc : $dc; $testCon = ldap_connect($conString, $port); ldap_set_option($testCon, LDAP_OPT_PROTOCOL_VERSION, 3); ldap_set_option($testCon, LDAP_OPT_REFERRALS, 0); $bind = ldap_bind($testCon, $user, $password); if($bind) { ldap_close($testCon); return true; } ldap_close($testCon); return false; } protected function getObjectFromAdPath($path, $fields = false, $filter = false) { if ($filter === false) $filter = '(objectClass=*)'; if ($fields) $result = ldap_search($this->ldap, $path, $filter, $fields); else $result = ldap_search($this->ldap, $path, $filter); if(($err = ldap_errno($this->ldap)) != 0) throw new \Exception(ldap_err2str($err)); if(($num =ldap_count_entries($this->ldap, $result)) != 1) { ldap_free_result($result); throw new \LengthException("Invalid number of entries returned: {$num}"); } $entry = ldap_first_entry($this->ldap, $result); $attributes = ldap_get_attributes($this->ldap, $entry); ldap_free_result($result); $result = self::simplifyLDAPEntry($attributes); return $result; } protected function getObjectFromAd($filter, $fields = false) { if ($fields) $result = ldap_search($this->ldap, $this->getLDAPBase(), $filter, $fields); else $result = ldap_search($this->ldap, $this->getLDAPBase(), $filter); if(($err = ldap_errno($this->ldap)) != 0) throw new \Exception(ldap_err2str($err)); if(($num =ldap_count_entries($this->ldap, $result)) != 1) { ldap_free_result($result); throw new \LengthException("Invalid number of entries returned: {$num}"); } $entry = ldap_first_entry($this->ldap, $result); $attributes = ldap_get_attributes($this->ldap, $entry); ldap_free_result($result); $result = self::simplifyLDAPEntry($attributes); return $result; } protected function getObjectListFromAd($filter, $fields = false) { if ($fields) $result = ldap_search($this->ldap, $this->getLDAPBase(), $filter, $fields); else $result = ldap_search($this->ldap, $this->getLDAPBase(), $filter); $rows = array(); $data = ldap_get_entries($this->ldap, $result); for ($i=0; $i<$data["count"]; $i++) { array_push($rows, self::simplifyLDAPEntry($data[$i])); } ldap_free_result($result); return $rows; } /** * Fetches an user by its name. * * @param string $name * the user login name * @throws MissingArgumentException * @throws \LogicException * @throws \BadMethodCallException * @return array the user data */ public function getUserByName($name) { if(empty($name)) throw new MissingArgumentException('name not set'); if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { $filter = "(&(objectClass=user)(samaccounttype=" . self::NORMAL_ACCOUNT . ")(objectCategory=person)(samaccountname=" . $name . "))"; return $this->getObjectFromAd($filter); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($name); } } /** * Fetches an user by its GUID. * * @param string $guid * the guid * @throws MissingArgumentException * @throws \LogicException * @throws \BadMethodCallException * @return array the user data */ public function getUserByGUID($guid) { if(empty($guid)) throw new MissingArgumentException('guid not set'); if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { $guid = self::guid2bin($guid); $guid = self::bin2ldap($guid); $filter = "(&(objectClass=user)(samaccounttype=" . self::NORMAL_ACCOUNT . ")(objectCategory=person)(objectGUID=" . $guid . "))"; return $this->getObjectFromAd($filter); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($name); } } public function getUserBySID($sid, $hex = false) { if(empty($sid)) throw new MissingArgumentException('sid not set'); if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { if ($hex) $sid = pack("H*", $sid); else $sid = self::sid2bin($sid); $sid = self::bin2ldap($sid); $filter = "(&(objectClass=user)(samaccounttype=" . self::NORMAL_ACCOUNT . ")(objectCategory=person)(objectSid=" . $sid . "))"; return $this->getObjectFromAd($filter); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($sid); } } public function getUserByDN($dn, $filter = false) { if(empty($dn)) throw new MissingArgumentException('dn not set'); if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { return $this->getObjectFromAdPath($dn, false, "(&(objectClass=user)(samaccounttype=" . self::NORMAL_ACCOUNT . ")(objectCategory=person))"); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($sid); } } /** * Fetches an user by its name. * * @param string $name * the user login name * @throws MissingArgumentException * @throws \LogicException * @throws \BadMethodCallException * @return array the user data */ public function getGroupByName($name) { if(empty($name)) throw new MissingArgumentException('name not set'); if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { $filter = "(&(objectClass=group)(samaccounttype=" . self::SECURITY_GLOBAL_GROUP . ")(objectCategory=group)(samaccountname=" . $name . "))"; return $this->getObjectFromAd($filter); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($name); } } /** * Fetches an user by its GUID. * * @param string $guid * the guid * @throws MissingArgumentException * @throws \LogicException * @throws \BadMethodCallException * @return array the user data */ public function getGroupByGUID($guid) { if(empty($guid)) throw new MissingArgumentException('guid not set'); if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { $guid = self::guid2bin($guid); $guid = self::bin2ldap($guid); $filter = "(&(objectClass=group)(samaccounttype=" . self::SECURITY_GLOBAL_GROUP . ")(objectCategory=group)(objectGUID=" . $guid . "))"; return $this->getObjectFromAd($filter); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($name); } } public function getGroupBySID($sid, $hex = false) { if(empty($sid)) throw new MissingArgumentException('sid not set'); if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { if ($hex) $sid = pack("H*", $sid); else $sid = self::sid2bin($sid); $sid = self::bin2ldap($sid); $filter = "(&(objectClass=group)(samaccounttype=" . self::SECURITY_GLOBAL_GROUP . ")(objectCategory=group)(objectSid=" . $sid . "))"; return $this->getObjectFromAd($filter); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($name); } } public function getGroupByDN($dn) { if(empty($dn)) throw new MissingArgumentException('dn not set'); if(!isset($this->ldap)) throw new \LogicException('no ldap connection found'); try { return $this->getObjectFromAdPath($dn); } catch (\InvalidArgumentException $e) { throw new \InvalidArgumentException($name); } } /** * Lists all users in the directory. * * @param string $name * an optional filter * @return array an array of all users (matching the filter) */ public function listUsers($name = '*') { $result = array(); if(empty($name)) $name = '*'; $filter = "(&(objectClass=user)(samaccounttype=" . self::NORMAL_ACCOUNT . ")(objectCategory=person)(cn=" . $name . "))"; $fields = array('samaccountname', 'displayname', 'objectSid','objectGUID'); return $this->getObjectListFromAd($filter, $fields); } /** * Lists all groups in the directory. * * @param string $name * an optional filter * @return array an array of all groups (matching the filter) */ public function listGroups($name = '*') { $result = array(); if(empty($name)) $name = '*'; $filter = '(&(objectCategory=group)(cn=' . $name . '))'; $fields = array('samaccountname','name','objectSid','objectGUID', 'distinguishedName'); return $this->getObjectListFromAd($filter, $fields); } }