diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -107,6 +107,7 @@ 'PhutilAuthAdapterOAuthTwitch' => 'auth/PhutilAuthAdapterOAuthTwitch.php', 'PhutilAuthAdapterOAuthTwitter' => 'auth/PhutilAuthAdapterOAuthTwitter.php', 'PhutilAuthAdapterPersona' => 'auth/PhutilAuthAdapterPersona.php', + 'PhutilAuthCredentialException' => 'auth/exception/PhutilAuthCredentialException.php', 'PhutilAuthException' => 'auth/exception/PhutilAuthException.php', 'PhutilAuthUserAbortedException' => 'auth/exception/PhutilAuthUserAbortedException.php', 'PhutilBallOfPHP' => 'phage/util/PhutilBallOfPHP.php', @@ -519,6 +520,7 @@ 'PhutilAuthAdapterOAuthTwitch' => 'PhutilAuthAdapterOAuth', 'PhutilAuthAdapterOAuthTwitter' => 'PhutilAuthAdapterOAuth1', 'PhutilAuthAdapterPersona' => 'PhutilAuthAdapter', + 'PhutilAuthCredentialException' => 'PhutilAuthException', 'PhutilAuthException' => 'Exception', 'PhutilAuthUserAbortedException' => 'PhutilAuthException', 'PhutilBufferedIterator' => 'Iterator', diff --git a/src/auth/PhutilAuthAdapterLDAP.php b/src/auth/PhutilAuthAdapterLDAP.php --- a/src/auth/PhutilAuthAdapterLDAP.php +++ b/src/auth/PhutilAuthAdapterLDAP.php @@ -9,7 +9,7 @@ private $port = 389; private $baseDistinguishedName; - private $searchAttribute; + private $searchAttributes = array(); private $usernameAttribute; private $realNameAttributes = array(); private $ldapVersion = 3; @@ -18,7 +18,6 @@ private $anonymousUsername; private $anonymousPassword; private $activeDirectoryDomain; - private $searchFirst; private $loginUsername; private $loginPassword; @@ -49,8 +48,8 @@ return $this; } - public function setSearchAttribute($search_attribute) { - $this->searchAttribute = $search_attribute; + public function setSearchAttributes(array $search_attributes) { + $this->searchAttributes = $search_attributes; return $this; } @@ -100,11 +99,6 @@ return $this; } - public function setSearchFirst($search) { - $this->searchFirst = $search; - return $this; - } - public function setActiveDirectoryDomain($domain) { $this->activeDirectoryDomain = $domain; return $this; @@ -129,7 +123,7 @@ public function readLDAPRecordAccountID(array $record) { $key = $this->usernameAttribute; if (!strlen($key)) { - $key = $this->searchAttribute; + $key = head($this->searchAttributes); } return $this->readLDAPData($record, $key); } @@ -180,8 +174,7 @@ // 0 => 'actual-value-we-want', // ) // - // However, in at least the case of 'dn' after we "searchFirst", the - // property is a bare string. + // However, in at least the case of 'dn', the property is a bare string. if (is_scalar($list) && strlen($list)) { return $list; @@ -192,36 +185,66 @@ } } + private function formatLDAPAttributeSearch($attribute, $login_user) { + // If the attribute contains the literal token "${login}", treat it as a + // query and substitute the user's login name for the token. + + if (strpos($attribute, '${login}') !== false) { + $escaped_user = ldap_sprintf('%S', $login_user); + $attribute = str_replace('${login}', $escaped_user, $attribute); + return $attribute; + } + + // Otherwise, treat it as a simple attribute search. + + return ldap_sprintf( + '%Q=%S', + $attribute, + $login_user); + } + private function loadLDAPUserData() { $conn = $this->establishConnection(); $login_user = $this->loginUsername; $login_pass = $this->loginPassword; - $distinguished_name = ldap_sprintf( - '%Q=%s,%Q', - $this->searchAttribute, - $login_user, - $this->baseDistinguishedName); - - if ($this->searchFirst) { - $user = $this->searchLDAPForUser($this->usernameAttribute, $login_user); - if (!$user) { - throw new Exception("Invalid credentials."); - } - $login_user = $this->readLDAPData($user, $this->searchAttribute); - $distinguished_name = $this->readLDAPData($user, 'dn'); - } - if ($this->activeDirectoryDomain) { - $distinguished_name = ldap_sprintf( - '%s@%Q', - $login_user, - $this->activeDirectoryDomain); + if ($this->anonymousUsername) { + $distinguished_name = null; + $search_query = null; + foreach ($this->searchAttributes as $attribute) { + $search_query = $this->formatLDAPAttributeSearch( + $attribute, + $login_user); + $record = $this->searchLDAPForRecord($search_query); + if ($record) { + $distinguished_name = $this->readLDAPData($record, 'dn'); + break; + } + } + if ($distinguished_name === null) { + throw new PhutilAuthCredentialException(); + } + } else { + $search_query = $this->formatLDAPAttributeSearch( + head($this->searchAttributes), + $login_user); + if ($this->activeDirectoryDomain) { + $distinguished_name = ldap_sprintf( + '%s@%Q', + $login_user, + $this->activeDirectoryDomain); + } else { + $distinguished_name = ldap_sprintf( + '%Q,%Q', + $search_query, + $this->baseDistinguishedName); + } } $this->bindLDAP($conn, $distinguished_name, $login_pass); - $result = $this->searchLDAPForUser($this->searchAttribute, $login_user); + $result = $this->searchLDAPForRecord($search_query); if (!$result) { // This is unusual (since the bind succeeded) but we've seen it at least // once in the wild, where the anonymous user is allowed to search but @@ -248,7 +271,7 @@ $pass = $this->anonymousPassword; $this->bindLDAP($conn, $user, $pass); - $result = $this->searchLDAPForUser($this->searchAttribute, $login_user); + $result = $this->searchLDAPForRecord($search_query); if (!$result) { throw new Exception( pht( @@ -342,10 +365,10 @@ } - private function searchLDAPForUser($attribute, $username) { + private function searchLDAPForRecord($dn) { $conn = $this->establishConnection(); - $results = $this->searchLDAP('%Q=%S', $attribute, $username); + $results = $this->searchLDAP('%Q', $dn); if (!$results) { return null; @@ -353,8 +376,9 @@ if (count($results) > 1) { throw new Exception( - "LDAP user query returned more than one result. It must uniquely ". - "identify a user."); + pht( + 'LDAP record query returned more than one result. The query must '. + 'uniquely identify a record.')); } return head($results); @@ -405,6 +429,11 @@ $errno = @ldap_errno($conn); $error = @ldap_error($conn); + // This is `LDAP_INVALID_CREDENTIALS`. + if ($errno == 49) { + throw new PhutilAuthCredentialException(); + } + if ($errno || $error) { $full_message = pht( "LDAP Exception: %s\nLDAP Error #%d: %s", diff --git a/src/auth/exception/PhutilAuthCredentialException.php b/src/auth/exception/PhutilAuthCredentialException.php new file mode 100644 --- /dev/null +++ b/src/auth/exception/PhutilAuthCredentialException.php @@ -0,0 +1,8 @@ +