diff --git a/include/class_exceptions.inc b/include/class_exceptions.inc
index 1dc822e6c69d34e4cdf4436af8f0671e919aa0cc..ecaa716f15b15faee8e8a218c72a1a7f053973b2 100644
--- a/include/class_exceptions.inc
+++ b/include/class_exceptions.inc
@@ -78,3 +78,11 @@ class UnknownClassException extends FusionDirectoryException
 class LDAPFailureException extends FusionDirectoryException
 {
 }
+
+class LoginFailureException extends FusionDirectoryException
+{
+}
+
+class LoginFailurePpolicyException extends LoginFailureException
+{
+}
diff --git a/include/class_userinfo.inc b/include/class_userinfo.inc
index 101473a8dad480ed70993d10c74941dc317ce779..61995447703f1d60a843f7d6518d36d82f20a126 100644
--- a/include/class_userinfo.inc
+++ b/include/class_userinfo.inc
@@ -30,7 +30,6 @@ define('POSIX_ACCOUNT_EXPIRED',           1);
 define('POSIX_WARN_ABOUT_EXPIRATION',     2);
 define('POSIX_FORCE_PASSWORD_CHANGE',     4);
 define('POSIX_DISALLOW_PASSWORD_CHANGE',  8);
-define('PPOLICY_ACCOUNT_EXPIRED',         16);
 
 /*!
  * \brief Class userinfo
@@ -912,12 +911,6 @@ class userinfo
     $ldap = $config->get_ldap_link();
 
     if (class_available('ppolicyAccount')) {
-      $ldap->cd($config->current['BASE']);
-      $ldap->search('(objectClass=*)', [], 'one');
-      if (!$ldap->success()) {
-        return PPOLICY_ACCOUNT_EXPIRED;
-      }
-
       try {
         list($policy, $attrs) = user::fetchPpolicy($this->dn);
         if (
@@ -1209,17 +1202,17 @@ class userinfo
    *
    * \return TRUE on SUCCESS, NULL or FALSE on error
    */
-  public static function loginUser (string $username, string $password)
+  public static function loginUser (string $username, string $password): userinfo
   {
     global $config;
 
     $ui = static::getLdapUser($username);
 
     if ($ui === FALSE) {
-      return NULL;
+      throw new LoginFailureException('User not found');
     } elseif (is_string($ui)) {
       msg_dialog::display(_('Internal error'), $ui, FATAL_ERROR_DIALOG);
-      return NULL;
+      exit();
     }
 
     /* password check, bind as user with supplied password  */
@@ -1229,7 +1222,15 @@ class userinfo
     );
     $ldap = new ldapMultiplexer($ldapObj);
     if (!$ldap->success()) {
-      return NULL;
+      throw new LoginFailureException($ldap->get_error());
+    }
+
+    if (class_available('ppolicyAccount')) {
+      $ldap->cd($config->current['BASE']);
+      $ldap->search('(objectClass=*)', [], 'one');
+      if (!$ldap->success()) {
+        throw new LoginFailurePpolicyException(_('It seems your user password has expired. Please use <a href="recovery.php">password recovery</a> to change it.'));
+      }
     }
 
     /* Username is set, load subtreeACL's now */
diff --git a/include/login/class_LoginMethod.inc b/include/login/class_LoginMethod.inc
index 18a86edec6ffb2f0d405f9214a8a80523aaa1958..92797de643ac95e5610ef366b2aefe4018cf4e85 100644
--- a/include/login/class_LoginMethod.inc
+++ b/include/login/class_LoginMethod.inc
@@ -95,13 +95,21 @@ class LoginMethod
   {
     global $ui, $config, $message, $smarty;
     /* Login as user, initialize user ACL's */
-    $ui = userinfo::loginUser(static::$username, static::$password);
-    if ($ui === NULL) {
+    try {
+      $ui = userinfo::loginUser(static::$username, static::$password);
+    } catch (LoginFailurePpolicyException $e) {
+      msg_dialog::display(_('Authentication error'), $e->getMessage(), ERROR_DIALOG);
+      logging::log('security', 'login', '', [], 'Account for user "'.static::$username.'" has expired (ppolicy)');
+      $message = _('Password expired');
+      $smarty->assign('focusfield', 'username');
+      return FALSE;
+    } catch (LoginFailureException $e) {
       if (isset($_SERVER['REMOTE_ADDR'])) {
-        logging::log('security', 'login', '', [], 'Authentication failed for user "'.static::$username.'" [from '.$_SERVER['REMOTE_ADDR'].']');
+        logging::log('security', 'login', '', [], 'Authentication failed for user "'.static::$username.'" [from '.$_SERVER['REMOTE_ADDR'].']: '.$e->getMessage());
       } else {
-        logging::log('security', 'login', '', [], 'Authentication failed for user "'.static::$username.'"');
+        logging::log('security', 'login', '', [], 'Authentication failed for user "'.static::$username.'": '.$e->getMessage());
       }
+      /* Show the same message whether the user exists or not to avoid information leak */
       $message = _('Please check the username/password combination.');
       $smarty->assign('focusfield', 'password');
       return FALSE;
@@ -133,17 +141,6 @@ class LoginMethod
 
     /* Check account expiration */
     $expired = $ui->expired_status();
-    if ($expired == PPOLICY_ACCOUNT_EXPIRED) {
-      msg_dialog::display(
-        _('Authentication error'),
-        _('It seems your user password has expired. Please use <a href="recovery.php">password recovery</a> to change it.'),
-        ERROR_DIALOG
-      );
-      logging::log('security', 'login', '', [], 'Account for user "'.static::$username.'" has expired (ppolicy)');
-      $message = _('Password expired');
-      $smarty->assign('focusfield', 'username');
-      return FALSE;
-    }
 
     if ($expired == POSIX_ACCOUNT_EXPIRED) {
       logging::log('security', 'login', '', [], 'Account for user "'.static::$username.'" has expired');