openrat-cms

OpenRat Content Management System
git clone http://git.code.weiherhei.de/openrat-cms.git
Log | Files | Refs | README

commit 72143f9479486d54f8df3784a78fb59da18f0860
parent 836eda3c7ca6909883f936544da74be0b8a44d73
Author: Jan Dankert <develop@jandankert.de>
Date:   Sun, 29 Nov 2020 21:46:57 +0100

Auth modules should only use the Auth::STATUS_* constants as return value.

Diffstat:
Mmodules/cms/Dispatcher.class.php | 2+-
Mmodules/cms/action/login/LoginLoginAction.class.php | 187+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mmodules/cms/action/login/LoginOidcAction.class.php | 23+++++++++++++++++------
Mmodules/cms/auth/Auth.class.php | 29++++++++++++++++-------------
Mmodules/cms/auth/AuthRunner.class.php | 36+++++++++++++++++++++++++++++++-----
Mmodules/cms/auth/CookieAuth.class.php | 2+-
Mmodules/cms/auth/DefaultUserAuth.class.php | 2+-
Mmodules/cms/auth/GuestAuth.class.php | 8+++-----
Mmodules/cms/auth/HttpAuth.class.php | 2+-
Mmodules/cms/auth/IdentAuth.class.php | 9+++------
Mmodules/cms/auth/InternalAuth.class.php | 10+++++-----
Mmodules/cms/auth/README.md | 6+++---
Mmodules/cms/auth/RememberAuth.class.php | 4+---
Mmodules/cms/auth/SSLAuth.class.php | 3+--
Mmodules/cms/auth/SingleSignonAuth.class.php | 5++---
15 files changed, 189 insertions(+), 139 deletions(-)

diff --git a/modules/cms/Dispatcher.class.php b/modules/cms/Dispatcher.class.php @@ -424,7 +424,7 @@ class Dispatcher try { - $key = $this->request->isAction?'write':'read'; + $key = $this->request->isAction && !Startup::readonly() ?'write':'read'; $db = new Database( $dbConfig->merge( $dbConfig->subset($key))->getConfig() ); $db->id = $dbid; diff --git a/modules/cms/action/login/LoginLoginAction.class.php b/modules/cms/action/login/LoginLoginAction.class.php @@ -14,6 +14,7 @@ use language\Language; use language\Messages; use logger\Logger; use security\Password; +use util\Browser; use util\exception\ObjectNotFoundException; use util\exception\SecurityException; use util\Mail; @@ -87,11 +88,7 @@ class LoginLoginAction extends LoginAction implements Method { $authResult = AuthRunner::checkLogin('authenticate',$loginName,$loginPassword, $token ); Password::delay(); - $mustChangePassword = ( $authResult === Auth::STATUS_PW_EXPIRED ); - $tokenFailed = ( $authResult === Auth::STATUS_TOKEN_NEEDED ); - $loginOk = ( $authResult === Auth::STATUS_SUCCESS ); - - if ( $mustChangePassword ) { + if ( $authResult & Auth::STATUS_PW_EXPIRED ) { // Der Benutzer hat zwar ein richtiges Kennwort eingegeben, aber dieses ist abgelaufen. // Wir versuchen hier, das neue zu setzen (sofern eingegeben). @@ -118,30 +115,37 @@ class LoginLoginAction extends LoginAction implements Method { $user->setPassword( $newPassword1,true ); $loginPassword = $newPassword1; - $loginOk = true; - $mustChangePassword = false; + $authResult -= Auth::STATUS_PW_EXPIRED; } } } - if ( $tokenFailed ) { + if ( $authResult & Auth::STATUS_TOKEN_NEEDED ) { // Token falsch. $this->addErrorFor(null,Messages::LOGIN_FAILED_TOKEN_FAILED ); $this->addValidationError('user_token',''); return; } - if ( $mustChangePassword ) { - // Anmeldung gescheitert, Benutzer muss Kennwort ?ndern. - $this->addErrorFor( null,Messages::LOGIN_FAILED_MUSTCHANGEPASSWORD); - $this->addValidationError('password1',''); - $this->addValidationError('password2',''); - return; + if ( $authResult & Auth::STATUS_PW_EXPIRED ) { + + if ( $authResult & Auth::STATUS_FAILED ) { + + // Anmeldung gescheitert, Benutzer muss Kennwort ?ndern. + $this->addErrorFor( null,Messages::LOGIN_FAILED_MUSTCHANGEPASSWORD); + $this->addValidationError('password1',''); + $this->addValidationError('password2',''); + return; + } + + if ( $authResult & Auth::STATUS_FAILED ) { + $this->addWarningFor(null,Messages::LOGIN_FAILED_MUSTCHANGEPASSWORD); + } } $ip = getenv("REMOTE_ADDR"); - if ( ! $loginOk ) { + if ( $authResult & Auth::STATUS_FAILED ) { Logger::debug(TextMessage::create('login failed for user ${name} from IP ${ip}', [ 'name' => $loginName, @@ -157,14 +161,19 @@ class LoginLoginAction extends LoginAction implements Method { // Increase fail counter $user = User::loadWithName($loginName,User::AUTH_TYPE_INTERNAL); - $isLocked = $user->passwordLockedUntil && $user->passwordLockedUntil > Startup::getStartTime(); - - if ( ! $isLocked ) { - + if ( $authResult & Auth::STATUS_ACCOUNT_LOCKED ) { + ; + // the account is locked, so the login failed. + // we are NOT informing the UI about this. The user is already informed about the lock. + } + else { + // Increase password fail counter $user->increaseFailedPasswordCounter(); + // $user->passwordFailedCount is now at least 1. $lockAfter = Configuration::subset(['security','password'])->get('lock_after_fail_count',10); if ( $lockAfter && $user->passwordFailedCount % $lockAfter == 0 ) { + // exponentially increase the lock duration. $factor = pow(2, intval($user->passwordFailedCount/$lockAfter) - 1 ) ; $lockedDuration = Configuration::subset(['security','password'])->get('lock_duration',120) * $factor * 60; @@ -172,6 +181,7 @@ class LoginLoginAction extends LoginAction implements Method { $user->passwordLockedUntil = $lockedUntil; $user->persist(); + // Inform the user about the lock. if ( $user->mail ) { $mail = new Mail( $user->mail,Messages::MAIL_PASSWORD_LOCKED_SUBJECT,Messages::MAIL_PASSWORD_LOCKED); $mail->setVar('username',$user->name); @@ -181,81 +191,90 @@ class LoginLoginAction extends LoginAction implements Method { } } } - - return; } + else if ( $authResult & Auth::STATUS_SUCCESS ) { + + Logger::info(TextMessage::create('Login successful for user ${0}', [$loginName])); - Logger::info(TextMessage::create('Login successful for user ${0}', [$loginName])); - Logger::debug("Login successful for user '$loginName' from IP $ip"); - - try { - // Benutzer über den Benutzernamen laden. - $user = User::loadWithName($loginName, User::AUTH_TYPE_INTERNAL, null); - $user->setCurrent(); - $user->updateLoginTimestamp(); - - if ($user->passwordAlgo != Password::bestAlgoAvailable()) - // Re-Hash the password with a better hash algo. - $user->setPassword($loginPassword); - - } catch (ObjectNotFoundException $ex) { - // Benutzer wurde zwar authentifiziert, ist aber in der - // internen Datenbank nicht vorhanden - if (Configuration::subset(['security', 'newuser'])->is('autoadd', true)) { - - // Neue Benutzer in die interne Datenbank uebernehmen. - $user = new User(); - $user->name = $loginName; - $user->fullname = $loginName; - $user->persist(); - Logger::debug( TextMessage::create('user ${0} authenticated successful and added to internal user table',[$loginName]) ); + $browser = new Browser(); + Logger::debug(TextMessage::create('Login successful for user ${0} from IP ${1} with ${2} (${3})',[$loginName,$ip,$browser->name,$browser->platform])); + + try { + // Benutzer über den Benutzernamen laden. + $user = User::loadWithName($loginName, User::AUTH_TYPE_INTERNAL, null); + $user->setCurrent(); $user->updateLoginTimestamp(); - } else { - // Benutzer soll nicht angelegt werden. - // Daher ist die Anmeldung hier gescheitert. - // Anmeldung gescheitert. - Logger::warn( TextMessage::create('user ${0} authenticated successful, but not found in internal user table',[$loginName]) ); - $this->addErrorFor(null,Messages::LOGIN_FAILED, ['name' => $loginName ]); - $this->addValidationError('login_name', ''); - $this->addValidationError('login_password', ''); + if ($user->passwordAlgo != Password::bestAlgoAvailable()) + // Re-Hash the password with a better hash algo. + $user->setPassword($loginPassword); - return; + } catch (ObjectNotFoundException $ex) { + // Benutzer wurde zwar authentifiziert, ist aber in der + // internen Datenbank nicht vorhanden + if (Configuration::subset(['security', 'newuser'])->is('autoadd', true)) { + + if ( Startup::readonly() ) + throw new \LogicException('System is readonly so this user cannot be inserted.'); + + // Neue Benutzer in die interne Datenbank uebernehmen. + $user = new User(); + $user->name = $loginName; + $user->fullname = $loginName; + $user->persist(); + Logger::debug( TextMessage::create('user ${0} authenticated successful and added to internal user table',[$loginName]) ); + $user->updateLoginTimestamp(); + } else { + // Benutzer soll nicht angelegt werden. + // Daher ist die Anmeldung hier gescheitert. + // Anmeldung gescheitert. + Logger::warn( TextMessage::create('user ${0} authenticated successful, but not found in internal user table',[$loginName]) ); + + $this->addErrorFor(null,Messages::LOGIN_FAILED, ['name' => $loginName ]); + $this->addValidationError('login_name' , ''); + $this->addValidationError('login_password', ''); + + return; + } } - } - // Cookie setzen - $this->setCookie(Action::COOKIE_DB_ID ,DB::get()->id ); - $this->setCookie(Action::COOKIE_USERNAME,$user->name ); + // Cookie setzen + $this->setCookie(Action::COOKIE_DB_ID ,DB::get()->id ); + $this->setCookie(Action::COOKIE_USERNAME,$user->name ); - if ( $this->hasRequestVar('remember') ) { - // Sets the login token cookie - $this->setCookie(Action::COOKIE_TOKEN ,$user->createNewLoginToken() ); - } + if ( $this->hasRequestVar('remember') ) { + // Sets the login token cookie + $this->setCookie(Action::COOKIE_TOKEN ,$user->createNewLoginToken() ); + } - // Anmeldung erfolgreich. - if ( Configuration::subset('security')->is('renew_session_login',false) ) - $this->recreateSession(); - - // Send mail to user to inform about the new login. - if ( $user->mail && Configuration::subset('security')->is('inform_user_about_new_login',true) ) { - $mail = new Mail( $user->mail, Messages::MAIL_NEW_LOGIN_SUBJECT, Messages::MAIL_NEW_LOGIN_TEXT ); - $browser = new \util\Browser(); - $mail->setVar( 'platform',$browser->platform ); - $mail->setVar( 'browser' ,$browser->name ); - $mail->setVar( 'username',$user->name ); - $mail->setVar( 'name' ,$user->getName() ); - $mail->send(); - } + // Anmeldung erfolgreich. + if ( Configuration::subset('security')->is('renew_session_login',false) ) + $this->recreateSession(); + + // Send mail to user to inform about the new login. + if ( $user->mail && Configuration::subset('security')->is('inform_user_about_new_login',true) ) { + $mail = new Mail( $user->mail, Messages::MAIL_NEW_LOGIN_SUBJECT, Messages::MAIL_NEW_LOGIN_TEXT ); + $browser = new \util\Browser(); + $mail->setVar( 'platform',$browser->platform ); + $mail->setVar( 'browser' ,$browser->name ); + $mail->setVar( 'username',$user->name ); + $mail->setVar( 'name' ,$user->getName() ); + $mail->send(); + } - $this->addNoticeFor( $user,Messages::LOGIN_OK, array('name' => $user->getName() )); + $this->addNoticeFor( $user,Messages::LOGIN_OK, array('name' => $user->getName() )); - // Setting the user-defined language - $config = Session::getConfig(); - $language = new Language(); - $config['language'] = $language->getLanguage($user->language); - $config['language']['language_code'] = $user->language; + // Setting the user-defined language + $config = Session::getConfig(); + $language = new Language(); + $config['language'] = $language->getLanguage($user->language); + $config['language']['language_code'] = $user->language; - Session::setConfig( $config ); - } + Session::setConfig( $config ); + } + else { + throw new \LogicException('unreachable code: Auth module must return either SUCCESS or FAIL'); + } + + } } diff --git a/modules/cms/action/login/LoginOidcAction.class.php b/modules/cms/action/login/LoginOidcAction.class.php @@ -4,6 +4,7 @@ use cms\action\LoginAction; use cms\action\Method; use cms\action\RequestParams; use cms\base\Configuration; +use cms\base\Startup; use cms\model\User; use Exception; use openid_connect\OpenIDConnectClient; @@ -40,12 +41,22 @@ class LoginOidcAction extends LoginAction implements Method { $user = User::loadWithName( $subjectIdentifier,User::AUTH_TYPE_OIDC,$providerName ); if ( ! $user ) { - // Create user - $user = new User(); - $user->name = $subjectIdentifier; - $user->type = User::AUTH_TYPE_OIDC; - $user->issuer = $providerName; - $user->persist(); + + if ( Startup::readonly() ) { + throw new \LogicException('Cannot add authenticated user to database, because the system is readonly'); + } + elseif (Configuration::subset(['security', 'newuser'])->is('autoadd', true)) { + + // Create user + $user = new User(); + $user->name = $subjectIdentifier; + $user->type = User::AUTH_TYPE_OIDC; + $user->issuer = $providerName; + $user->persist(); + } + else { + throw new \LogicException('Cannot add authenticated user to database, because auto adding is disabled.'); + } } diff --git a/modules/cms/auth/Auth.class.php b/modules/cms/auth/Auth.class.php @@ -2,25 +2,27 @@ namespace cms\auth; -use Benutzername; -use Kennwort; +/** + * Interface for Authentication + */ interface Auth { - - const STATUS_SUCCESS = 1; - const STATUS_FAILED = 2; - const STATUS_PW_EXPIRED = 3; - const STATUS_TOKEN_NEEDED = 4; + const STATUS_SUCCESS = 1; + const STATUS_FAILED = 2; + const STATUS_PW_EXPIRED = 4; + const STATUS_TOKEN_NEEDED = 8; + const STATUS_ACCOUNT_LOCKED = 16; const NS = __NAMESPACE__; /** - * Prüft den eingegebenen Benutzernamen und das Kennwort - * auf Richtigkeit. + * Checks the provided login data. * - * @param string Benutzername - * @param string Kennwort + * @param string $username username + * @param string $password password + * @param string $token token + * @return int a bitmask with Auth::STATUS_* */ function login($username, $password, $token); @@ -28,7 +30,7 @@ interface Auth /** * Ermittelt den Benutzernamen. * Der Benutzername wird verwendet, um die Loginmaske vorauszufüllen. + * @return string the username or null */ function username(); -} - +}+ \ No newline at end of file diff --git a/modules/cms/auth/AuthRunner.class.php b/modules/cms/auth/AuthRunner.class.php @@ -7,9 +7,20 @@ namespace cms\auth; use cms\base\Configuration; use logger\Logger; +/** + * Executes multiple authentication modules. + */ class AuthRunner { - protected static function getModulesFor($section, $callback ) + /** + * Executes the callback with all modules of this section + * + * @param $section string a configuration setting under security/modules which must contain an array of strings + * @param $callback callable anonymous function with a auth module as first parameter + * @param $emptyResult mixed if the callback returns this value, the next value is executed. + * @return mixed + */ + protected static function getModulesFor($section, $callback, $emptyResult ) { $securityConfig = Configuration::subset('security'); @@ -27,16 +38,22 @@ class AuthRunner $auth = new $moduleClass; $result = $callback( $auth ); - if ( $result ) + if ( $result != $emptyResult ) return $result; // next module. } - return null; + return $emptyResult; } + /** + * Search for a username in all modules of this section. + * + * @param $section + * @return string username of null (if none found) + */ public static function getUsername( $section ) { return self::getModulesFor(/** @@ -48,10 +65,19 @@ class AuthRunner Logger::debug('Preselecting User ' . $username . ' from ' . get_class($auth) ); return $username; - }); + },null); } + /** + * Makes an autorization through all modules of this section. + * + * @param $section + * @param $user + * @param $password + * @param $token + * @return int a bitmask of Auth::STATUS_* + */ public static function checkLogin( $section, $user,$password,$token ) { return self::getModulesFor($section, /** @@ -61,7 +87,7 @@ class AuthRunner Logger::info('Trying to login with module '.get_class($auth)); return $auth->login($user,$password,$token); - } + }, Auth::STATUS_FAILED ); } } \ No newline at end of file diff --git a/modules/cms/auth/CookieAuth.class.php b/modules/cms/auth/CookieAuth.class.php @@ -24,7 +24,7 @@ class CookieAuth implements Auth */ public function login($user, $password, $token) { - return false; + return Auth::STATUS_FAILED; } } diff --git a/modules/cms/auth/DefaultUserAuth.class.php b/modules/cms/auth/DefaultUserAuth.class.php @@ -22,7 +22,7 @@ class DefaultUserAuth implements Auth */ public function login($user, $password, $token) { - return false; + return Auth::STATUS_FAILED; } } diff --git a/modules/cms/auth/GuestAuth.class.php b/modules/cms/auth/GuestAuth.class.php @@ -30,8 +30,6 @@ class GuestAuth implements Auth */ public function login($user, $password, $token) { - return false; + return Auth::STATUS_FAILED; } -} - -?>- \ No newline at end of file +}+ \ No newline at end of file diff --git a/modules/cms/auth/HttpAuth.class.php b/modules/cms/auth/HttpAuth.class.php @@ -39,6 +39,6 @@ class HttpAuth implements Auth $ok = $http->request(); - return $ok; + return $ok ? Auth::STATUS_SUCCESS : Auth::STATUS_FAILED; } } diff --git a/modules/cms/auth/IdentAuth.class.php b/modules/cms/auth/IdentAuth.class.php @@ -2,7 +2,6 @@ namespace cms\auth; -use cms\auth\Auth; use logger\Logger; use util\Http; @@ -50,8 +49,6 @@ class IdentAuth implements Auth */ public function login($user, $password, $token) { - return null; + return Auth::STATUS_FAILED; } -} - -?>- \ No newline at end of file +}+ \ No newline at end of file diff --git a/modules/cms/auth/InternalAuth.class.php b/modules/cms/auth/InternalAuth.class.php @@ -38,12 +38,12 @@ SQL // Benutzer ist nicht vorhanden. // Trotzdem das Kennwort hashen, um Timingattacken zu verhindern. $unusedHash = Password::hash(User::pepperPassword($password), Password::bestAlgoAvailable()); - return null; + return Auth::STATUS_FAILED ; } $lockedUntil = $row_user['password_locked_until']; if ( $lockedUntil && $lockedUntil > Startup::getStartTime() ) { - return Auth::STATUS_FAILED; // Password is locked + return Auth::STATUS_FAILED & Auth::STATUS_ACCOUNT_LOCKED; // Password is locked } // Pruefen ob Kennwort mit Datenbank uebereinstimmt. @@ -68,9 +68,9 @@ SQL // Wenn das kennwort abgelaufen ist, kann es eine bestimmte Dauer noch benutzt und geändert werden. // Nach Ablauf dieser Dauer wird das Login abgelehnt. if ($row_user['password_expires'] + (Configuration::subset('security')->get('deny_after_expiration_duration',72) * 60 * 60) < time()) - return Auth::STATUS_FAILED; // Abgelaufenes Kennwort wird nicht mehr akzeptiert. + return Auth::STATUS_FAILED & Auth::STATUS_PW_EXPIRED; // Abgelaufenes Kennwort wird nicht mehr akzeptiert. else - return Auth::STATUS_PW_EXPIRED; // Kennwort ist abgelaufen, kann aber noch geändert werden. + return Auth::STATUS_SUCCESS & Auth::STATUS_PW_EXPIRED; // Kennwort ist abgelaufen, kann aber noch geändert werden. } if ($row_user['totp'] == 1) { @@ -79,7 +79,7 @@ SQL if (Password::getTOTPCode($user->otpSecret) == $token) return Auth::STATUS_SUCCESS; else - return Auth::STATUS_TOKEN_NEEDED; + return Auth::STATUS_FAILED & Auth::STATUS_TOKEN_NEEDED; } if ($row_user['hotp'] == 1) { diff --git a/modules/cms/auth/README.md b/modules/cms/auth/README.md @@ -4,5 +4,5 @@ These authentication backends are used for user identification and authenticatio Every Authentication must implement [Auth](Auth.class.php) and must provide the 2 methods -1. `login()` must do an authentification. On successful logins, it should return `OR_AUTH_STATUS_SUCCESS`. If this is not possible, this methode must return `false` or `OR_AUTH_STATUS_FAILED`. -1. `username()` may find out the username of the user which want to log in. If this is not possible, this method must return `false` or `OR_AUTH_STATUS_FAILED`.- \ No newline at end of file +1. `login()` must do an authentification. On successful logins, it should return `Auth::STATUS_SUCCESS`, otherwise this method must return `Auth::STATUS_FAILED`. +1. `username()` may find out the username of the user which want to log in. If this is not possible, this method must return `null`.+ \ No newline at end of file diff --git a/modules/cms/auth/RememberAuth.class.php b/modules/cms/auth/RememberAuth.class.php @@ -3,11 +3,9 @@ namespace cms\auth; use cms\action\Action; -use cms\auth\Auth; use cms\base\Configuration; use cms\base\DB; use cms\base\Startup; -use cms\model\Text; use database\Database; use cms\model\User; use logger\Logger; @@ -114,7 +112,7 @@ SQL */ public function login($user, $password, $token) { - return null; + return Auth::STATUS_FAILED; } protected function makeDBWritable( $dbid ) { diff --git a/modules/cms/auth/SSLAuth.class.php b/modules/cms/auth/SSLAuth.class.php @@ -2,7 +2,6 @@ namespace cms\auth; -use cms\auth\Auth; use cms\base\Configuration; /** @@ -30,7 +29,7 @@ class SSLAuth implements Auth */ public function login($user, $password, $token) { - return ( $this->username() == $user ) ? Auth::STATUS_SUCCESS : null; + return ( $this->username() == $user ) ? Auth::STATUS_SUCCESS : Auth::STATUS_FAILED;; } } diff --git a/modules/cms/auth/SingleSignonAuth.class.php b/modules/cms/auth/SingleSignonAuth.class.php @@ -2,8 +2,6 @@ namespace cms\auth; -use cms\auth\Auth; - /** * Single-Signon-Authentifizierung. * @@ -13,6 +11,7 @@ class SingleSignonAuth implements Auth { public function username() { + return null; } @@ -21,7 +20,7 @@ class SingleSignonAuth implements Auth */ public function login($user, $password, $token) { - return false; + return Auth::STATUS_FAILED;; } }