File modules/cms/auth/InternalAuth.class.php

Last commit: Fri Apr 15 20:45:19 2022 +0200	dankert	Refactoring: Code cleanup.
1 <?php 2 3 namespace cms\auth; 4 5 use cms\base\Configuration; 6 use cms\base\DB as Db; 7 use cms\base\Startup; 8 use cms\model\User; 9 use language\Messages; 10 use logger\Logger; 11 use LogicException; 12 use security\OTP; 13 use security\Password; 14 use util\mail\Mail; 15 16 /** 17 * Authentifizierungsmodul für die interne Benutzerdatenbank. 18 * 19 * @author Jan Dankert 20 * 21 */ 22 class InternalAuth implements Auth 23 { 24 /** 25 * Ueberpruefen des Kennwortes 26 * ueber die Benutzertabelle in der Datenbank. 27 */ 28 function login($username, $password, $token) 29 { 30 // Lesen des Benutzers aus der DB-Tabelle 31 $sql = Db::sql(<<<SQL 32 SELECT * FROM {{user}} 33 WHERE name={name} 34 SQL 35 ); 36 $sql->setString('name', $username); 37 38 $row_user = $sql->getRow(); 39 40 if (empty($row_user)) { 41 42 // Benutzer ist nicht vorhanden. 43 // Trotzdem das Kennwort hashen, um Timingattacken zu verhindern. 44 $unusedHash = Password::hash(Password::pepperPassword($password), Password::bestAlgoAvailable()); 45 if ( DEVELOPMENT ) 46 Logger::debug('user not found'); 47 return Auth::STATUS_FAILED ; 48 } 49 50 $lockedUntil = $row_user['password_locked_until']; 51 if ( $lockedUntil && $lockedUntil > Startup::getStartTime() ) { 52 if ( DEVELOPMENT ) 53 Logger::debug('user account ist locked until '.date('r',$lockedUntil)); 54 return Auth::STATUS_FAILED + Auth::STATUS_ACCOUNT_LOCKED; // Password is locked 55 } 56 57 // Pruefen ob Kennwort mit Datenbank uebereinstimmt. 58 if (!Password::check(Password::pepperPassword($password), $row_user['password_hash'], $row_user['password_algo'])) { 59 // Password does NOT match. 60 61 // Increase password fail counter 62 $sql = Db::sql(<<<SQL 63 UPDATE {{user}} 64 SET password_fail_count=password_fail_count+1 65 WHERE name={name} 66 SQL 67 ); 68 $sql->setString('name', $username); 69 $sql->execute(); 70 71 $row_user['password_fail_count']++; 72 73 $lockAfter = Configuration::subset(['security','password'])->get('lock_after_fail_count',10); 74 if ( $lockAfter && $row_user['password_fail_count'] % $lockAfter == 0 ) { 75 // exponentially increase the lock duration. 76 $factor = pow(2, intval($row_user['password_fail_count']/$lockAfter) - 1 ) ; 77 $lockedDuration = Configuration::subset(['security','password'])->get('lock_duration',120) * $factor * 60; 78 79 $lockedUntil = Startup::getStartTime() + $lockedDuration; 80 81 $sql = Db::sql(<<<SQL 82 UPDATE {{user}} 83 SET password_locked_until={locked_until} 84 WHERE name={name} 85 SQL 86 ); 87 $sql->setString('name' , $username ); 88 $sql->setInt ('locked_until',$lockedUntil); 89 $sql->execute(); 90 91 // Inform the user about the lock. 92 if ( $row_user['mail'] ) { 93 $mail = new Mail( $row_user['mail'],Messages::MAIL_PASSWORD_LOCKED_SUBJECT,Messages::MAIL_PASSWORD_LOCKED); 94 $mail->setVar('username',$row_user['name' ] ); 95 $mail->setVar('name' ,$row_user['fullname'] ); 96 $mail->setVar('until' ,date( \cms\base\Language::lang(Messages::DATE_FORMAT_FULL ), $lockedUntil ) ); 97 $mail->send(); 98 } 99 } 100 Db::get()->commit(); 101 102 return Auth::STATUS_FAILED; 103 } 104 105 // Password match :) 106 107 // Clear password fail counter 108 $sql = Db::sql(<<<SQL 109 UPDATE {{user}} 110 SET password_fail_count=0 111 WHERE name={name} 112 SQL 113 ); 114 $sql->setString('name', $username); 115 $sql->execute(); 116 117 // Behandeln von Klartext-Kennwoertern (Igittigitt). 118 if ($row_user['password_algo'] == Password::ALGO_PLAIN) { 119 if (Configuration::subset(['security', 'password'] )->is('force_change_if_cleartext',true)) 120 // Kennwort steht in der Datenbank im Klartext. 121 // Das Kennwort muss geaendert werden 122 return Auth::STATUS_FAILED + Auth::STATUS_PW_EXPIRED; 123 124 // Anderenfalls ist das Login zwar moeglich, aber das Kennwort wird automatisch neu gehasht, weil der beste Algo erzwungen wird. 125 // Das Klartextkennwort waere danach ueberschrieben. 126 } 127 128 if ($row_user['password_expires'] != null && $row_user['password_expires'] < time()) { 129 // Kennwort ist abgelaufen. 130 131 // Wenn das kennwort abgelaufen ist, kann es eine bestimmte Dauer noch benutzt und geändert werden. 132 // Nach Ablauf dieser Dauer wird das Login abgelehnt. 133 if ($row_user['password_expires'] + (Configuration::subset('security')->getSeconds('deny_after_expiration_duration',3*24*60*60)) < time()) 134 return Auth::STATUS_FAILED + Auth::STATUS_PW_EXPIRED; // Abgelaufenes Kennwort wird nicht mehr akzeptiert. 135 else 136 return Auth::STATUS_SUCCESS + Auth::STATUS_PW_EXPIRED; // Kennwort ist abgelaufen, kann aber noch geändert werden. 137 } 138 139 if ($row_user['totp'] == 1) { 140 $user = new User($row_user['id']); 141 $user->load(); 142 if ( DEVELOPMENT ) 143 Logger::info("valid TOTP tokens are \n".print_r(OTP::getValidTOTPCodes($user->otpSecret),true)); 144 if ( in_array( $token, OTP::getValidTOTPCodes($user->otpSecret) ) ) 145 return Auth::STATUS_SUCCESS; 146 else 147 return Auth::STATUS_FAILED + Auth::STATUS_TOKEN_NEEDED; 148 } 149 elseif ($row_user['hotp'] == 1) { 150 $user = new User($row_user['id']); 151 $user->load(); 152 $validHOTPCodes = OTP::getValidHOTPCodes($user->otpSecret,$user->hotpCount); 153 if ( DEVELOPMENT ) 154 Logger::info("valid HOTP tokens are \n".print_r($validHOTPCodes,true)); 155 if ( in_array( $token, $validHOTPCodes ) ) { 156 // Synchronize the internal counter 157 $newCount = array_flip($validHOTPCodes)[$token]+1; 158 $sql = Db::sql(<<<SQL 159 UPDATE {{user}} 160 SET hotp_counter={count} 161 WHERE name={name} 162 SQL 163 ); 164 $sql->setString('name' ,$username ); 165 $sql->setInt ('count',$newCount ); 166 $sql->execute(); 167 return Auth::STATUS_SUCCESS; 168 } 169 else 170 return Auth::STATUS_FAILED + Auth::STATUS_TOKEN_NEEDED; 171 } 172 173 // Benutzer wurde erfolgreich authentifiziert. 174 return Auth::STATUS_SUCCESS; 175 } 176 177 public function username() 178 { 179 return null; 180 } 181 }
Download modules/cms/auth/InternalAuth.class.php
History Fri, 15 Apr 2022 20:45:19 +0200 dankert Refactoring: Code cleanup. Fri, 15 Apr 2022 14:51:22 +0200 dankert Refactoring: User,Config and Database info is now stored in the Request, because so there is no session required for clients which are using Basic Authorization. Wed, 23 Feb 2022 00:38:24 +0100 dankert New: Enable HOTP with counter synchronization; New: TOTP of the last period are valid too. Mon, 7 Feb 2022 22:52:12 +0100 dankert Password lock check is moved into "InternalAuth", because it must be called on all authentication requests. Wed, 21 Apr 2021 21:15:17 +0200 Jan Dankert New: Accept human readable values for durations and memory sizes in the configuration. Mon, 30 Nov 2020 10:08:20 +0100 Jan Dankert Fix: Adding bits with '+', not '&' Sun, 29 Nov 2020 21:46:57 +0100 Jan Dankert Auth modules should only use the Auth::STATUS_* constants as return value. Sat, 28 Nov 2020 00:53:41 +0100 Jan Dankert New: Lock password after a number of login fails. Thu, 19 Nov 2020 00:45:44 +0100 Jan Dankert Security fix: We must update the login token on every login; Administrators are able to see the login tokens of users. Mon, 16 Nov 2020 13:21:57 +0100 Jan Dankert Code cleanup: Externalize calling the auth modules. Sun, 1 Nov 2020 00:36:50 +0100 Jan Dankert Refactoring: Only using the configuration object. Sun, 25 Oct 2020 02:51:56 +0200 Jan Dankert Using the object-based configuration. Tue, 29 Sep 2020 21:34:01 +0200 Jan Dankert Refactoring: Do not use global constants. Sat, 26 Sep 2020 04:26:55 +0200 Jan Dankert Refactoring: read configuration values with a class. Sat, 26 Sep 2020 02:26:39 +0200 Jan Dankert Refactoring: No global functions any more, the database object is read from the Db class. Sun, 23 Feb 2020 04:49:34 +0100 Jan Dankert Refactoring with Namespaces for the cms modules, part 2. Sun, 23 Feb 2020 04:01:30 +0100 Jan Dankert Refactoring with Namespaces for the cms modules, part 1: moving.