diff --git a/include/class_ldap.inc b/include/class_ldap.inc
index 07f07b0f16505bfee8cee985f7966597b20f8a83..df4418e12f1c2de7b13f3f5667e518f147bc5bb4 100644
--- a/include/class_ldap.inc
+++ b/include/class_ldap.inc
@@ -121,6 +121,16 @@ class LDAP
     return ldap_escape_f($dn);
   }
 
+  /*!
+   *  \brief Error text that must be returned for invalid user or password
+   *
+   *  This is useful to make sure the same error text is shown whether a user exists or not, when the password is not correct.
+   */
+  static function invalidCredentialsError (): string
+  {
+    return _(ldap_err2str(49));
+  }
+
   /*!
    *  \brief Create a connection to LDAP server
    *
@@ -151,11 +161,41 @@ class LDAP
         if (@ldap_parse_result($this->cid, $result, $errcode, $matcheddn, $errmsg, $referrals, $ctrls)) {
           if (isset($ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error'])) {
             $this->hascon = FALSE;
-            $this->error  = $ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error'];
+            switch ($ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error']) {
+              case 0:
+                /* passwordExpired - password has expired and must be reset */
+                $this->error = _('It seems your user password has expired. Please use <a href="recovery.php">password recovery</a> to change it.');
+                break;
+              case 1:
+                /* accountLocked */
+                $this->error = _('Account locked. Please contact your system administrator!');
+                break;
+              case 2:
+                /* changeAfterReset - password must be changed before the user will be allowed to perform any other operation */
+                $this->error = 'changeAfterReset';
+                break;
+              case 3:
+                /* passwordModNotAllowed */
+              case 4:
+                /* mustSupplyOldPassword */
+              case 5:
+                /* insufficientPasswordQuality */
+              case 6:
+                /* passwordTooShort */
+              case 7:
+                /* passwordTooYoung */
+              case 8:
+                /* passwordInHistory */
+              default:
+                $this->error = sprintf(_('Unexpected ppolicy error "%s", please contact the administrator'), $ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value']['error']);
+                break;
+            }
             // Note: Also available: expire, grace
           } else {
             $this->hascon = ($errcode == 0);
-            if (empty($errmsg)) {
+            if ($errcode == 49) {
+              $this->error = static::invalidCredentialsError();
+            } elseif (empty($errmsg)) {
               $this->error = ldap_err2str($errcode);
             } else {
               $this->error = $errmsg;
@@ -171,10 +211,10 @@ class LDAP
       } else {
         if ($this->reconnect) {
           if ($this->error != 'Success') {
-            $this->error = 'Could not rebind to ' . $this->binddn;
+            $this->error = static::invalidCredentialsError();
           }
         } else {
-          $this->error = 'Could not bind to ' . $this->binddn;
+          $this->error = static::invalidCredentialsError();
         }
       }
     } else {
diff --git a/include/class_userinfo.inc b/include/class_userinfo.inc
index 61995447703f1d60a843f7d6518d36d82f20a126..61a781246be1a2b5a0adfdc76d2c64016aab5740 100644
--- a/include/class_userinfo.inc
+++ b/include/class_userinfo.inc
@@ -62,6 +62,9 @@ class userinfo
   /*! \brief Current management base */
   protected $currentBase;
 
+  /*! \brief Password change should be forced */
+  protected $forcePasswordChange = FALSE;
+
   /* get acl's an put them into the userinfo object
      attr subtreeACL (userdn:components, userdn:component1#sub1#sub2,component2,...) */
   function __construct ($userdn)
@@ -903,6 +906,10 @@ class userinfo
   {
     global $config;
 
+    if ($this->forcePasswordChange) {
+      return POSIX_FORCE_PASSWORD_CHANGE;
+    }
+
     // Skip this for the admin account, we do not want to lock him out.
     if ($this->is_user_admin()) {
       return 0;
@@ -1209,7 +1216,7 @@ class userinfo
     $ui = static::getLdapUser($username);
 
     if ($ui === FALSE) {
-      throw new LoginFailureException('User not found');
+      throw new LoginFailureException(ldap::invalidCredentialsError());
     } elseif (is_string($ui)) {
       msg_dialog::display(_('Internal error'), $ui, FATAL_ERROR_DIALOG);
       exit();
@@ -1222,14 +1229,18 @@ class userinfo
     );
     $ldap = new ldapMultiplexer($ldapObj);
     if (!$ldap->success()) {
-      throw new LoginFailureException($ldap->get_error());
+      if ($ldap->get_error() == 'changeAfterReset') {
+        $ui->forcePasswordChange = TRUE;
+      } else {
+        throw new LoginFailureException($ldap->get_error());
+      }
     }
 
-    if (class_available('ppolicyAccount')) {
+    if (class_available('ppolicyAccount') && !function_exists('ldap_bind_ext')) {
       $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.'));
+        $ui->forcePasswordChange = TRUE;
       }
     }
 
diff --git a/include/login/class_LoginMethod.inc b/include/login/class_LoginMethod.inc
index 92797de643ac95e5610ef366b2aefe4018cf4e85..5f5e8f68a27e09a2f78065bdd9a3c8b12d2144d4 100644
--- a/include/login/class_LoginMethod.inc
+++ b/include/login/class_LoginMethod.inc
@@ -97,12 +97,6 @@ class LoginMethod
     /* Login as user, initialize user ACL's */
     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'].']: '.$e->getMessage());
@@ -110,7 +104,7 @@ class LoginMethod
         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.');
+      $message = $e->getMessage();
       $smarty->assign('focusfield', 'password');
       return FALSE;
     }