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:
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;;
}
}