openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

commit 168c493b350b1f863f4e753267411fe6b1e6b44c
parent 0cf5f829c4b6801203698d5382f981941dd9ae46
Author: Jan Dankert <devnull@localhost>
Date:   Tue,  7 Nov 2017 23:22:45 +0100

Login mit TOTP-Token.

Diffstat:
action/LoginAction.class.php | 39++++++++++++++++++++++++++-------------
action/UserAction.class.php | 34+---------------------------------
auth/Auth.class.php | 10++++++++--
auth/CookieAuth.class.php | 3++-
auth/DatabaseAuth.class.php | 9+++++----
auth/GuestAuth.class.php | 2+-
auth/HttpAuth.class.php | 2+-
auth/IdentAuth.class.php | 4++--
auth/InternalAuth.class.php | 67++++++++++++++++++++++++++++++++++++++++++++++---------------------
auth/LdapAuth.class.php | 3++-
auth/LdapUserDNAuth.class.php | 2+-
auth/OpenIdAuth.class.php | 2+-
auth/PersonasAuth.class.php | 0
auth/RememberAuth.class.php | 2+-
auth/SSLAuth.class.php | 7+++++--
auth/SingleSignonAuth.class.php | 2+-
auth/include.inc.php | 1-
config/config-default.php | 7+++++--
language/de.ini.php | 3+++
model/User.class.php | 34++++++++++++++++++++++++++++++++++
20 files changed, 145 insertions(+), 88 deletions(-)

diff --git a/action/LoginAction.class.php b/action/LoginAction.class.php @@ -334,20 +334,22 @@ class LoginAction extends Action // Den Benutzernamen aus dem Client-Zertifikat lesen und in die Loginmaske eintragen. - $ssl_user_var = $conf['security']['ssl']['user_var']; + $ssl_user_var = $conf['security']['ssl']['client_cert_dn_env']; if ( !empty($ssl_user_var) ) { - $username = getenv( $ssl_user_var ); + $username = getenv( $ssl_user_var ); if ( empty($username) ) { - echo lang('ERROR_LOGIN_BROKEN_SSL_CERT'); - Logger::warn( 'no username in SSL client certificate (var='.$ssl_user_var.').' ); - exit; + // Nothing to do. + // if user has no valid client cert he could not access this form. + } + else { + + // Benutzername ist in Eingabemaske unver�nderlich + $this->setTemplateVar('force_username',$username); } - // Benutzername ist in Eingabemaske unver�nderlich - $this->setTemplateVar('force_username',$username); } $this->setTemplateVar('objectid' ,$this->getRequestVar('objectid' ,OR_FILTER_NUMBER) ); @@ -838,7 +840,8 @@ class LoginAction extends Action $loginName = $this->getRequestVar('login_name' ,OR_FILTER_ALPHANUM); $loginPassword = $this->getRequestVar('login_password',OR_FILTER_ALPHANUM); $newPassword1 = $this->getRequestVar('password1' ,OR_FILTER_ALPHANUM); - $newPassword2 = $this->getRequestVar('password2' ,OR_FILTER_ALPHANUM); + $newPassword2 = $this->getRequestVar('password2' ,OR_FILTER_ALPHANUM); + $token = $this->getRequestVar('user_token' ,OR_FILTER_ALPHANUM); // Der Benutzer hat zwar ein richtiges Kennwort eingegeben, aber dieses ist abgelaufen. // Wir versuchen hier, das neue zu setzen (sofern eingegeben). @@ -895,6 +898,7 @@ class LoginAction extends Action $loginOk = false; $mustChangePassword = false; + $tokenFailed = false; $groups = null; $lastModule = null; @@ -904,10 +908,13 @@ class LoginAction extends Action $moduleClass = $module.'Auth'; $auth = new $moduleClass; Logger::info('Trying to login with module '.$moduleClass); - $loginOk = $auth->login( $loginName,$loginPassword ); + $loginStatus = $auth->login( $loginName,$loginPassword, $token ); + $loginOk = $loginStatus === true || $loginStatus === OR_AUTH_STATUS_SUCCESS; - if ( @$auth->mustChangePassword ) - $mustChangePassword = true; + if ( $loginStatus === OR_AUTH_STATUS_PW_EXPIRED ) + $mustChangePassword = true; + if ( $loginStatus === OR_AUTH_STATUS_TOKEN_NEEDED ) + $tokenFailed = true; if ( $loginOk ) { @@ -967,7 +974,7 @@ class LoginAction extends Action } } - usleep(hexdec(Password::randomHexString(1))); // delay: 0-255 ms + Password::delay(); $ip = getenv("REMOTE_ADDR"); @@ -977,7 +984,13 @@ class LoginAction extends Action Logger::debug("Login failed for user '$loginName' from IP $ip"); - if ( $mustChangePassword ) + if ( $tokenFailed ) + { + // Token falsch. + $this->addNotice('user',$loginName,'LOGIN_FAILED_TOKEN_FAILED','error' ); + $this->addValidationError('user_token',''); + } + elseif ( $mustChangePassword ) { // Anmeldung gescheitert, Benutzer muss Kennwort ?ndern. $this->addNotice('user',$loginName,'LOGIN_FAILED_MUSTCHANGEPASSWORD','error' ); diff --git a/action/UserAction.class.php b/action/UserAction.class.php @@ -226,7 +226,7 @@ class UserAction extends Action array('totpSecretUrl' => "otpauth://totp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}", 'hotpSecretUrl' => "otpauth://hotp/{$issuer}:{$account}?secret={$secret}&issuer={$issuer}&counter={$counter}" ) - + array('totpToken'=>$this->getCode()) + + array('totpToken'=>$this->user->getCode()) ); $this->setTemplateVar( 'allstyles',$this->user->getAvailableStyles() ); @@ -247,38 +247,6 @@ class UserAction extends Action /** - * Calculate the code, with given secret and point in time. - * - * @param string $secret - * @param int|null $timeSlice - * - * @return string - */ - private function getCode() - { - $codeLength = 6; - $timeSlice = floor(time() / 30); - $secretkey = hex2bin($this->user->otpSecret); - // Pack time into binary string - $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); - // Hash it with users secret key - $hm = hash_hmac('SHA1', $time, $secretkey, true); - // Use last nipple of result as index/offset - $offset = ord(substr($hm, -1)) & 0x0F; - // grab 4 bytes of the result - $hashpart = substr($hm, $offset, 4); - // Unpak binary value - $value = unpack('N', $hashpart); - $value = $value[1]; - // Only 32 bits - $value = $value & 0x7FFFFFFF; - $modulo = pow(10, $codeLength); - return str_pad($value % $modulo, $codeLength, '0', STR_PAD_LEFT); - } - - - - /** * Eigenschaften des Benutzers anzeigen */ function infoView() diff --git a/auth/Auth.class.php b/auth/Auth.class.php @@ -1,5 +1,11 @@ <?php + +DEFINE('OR_AUTH_STATUS_SUCCESS',1); +DEFINE('OR_AUTH_STATUS_FAILED',2); +DEFINE('OR_AUTH_STATUS_PW_EXPIRED',3); +DEFINE('OR_AUTH_STATUS_TOKEN_NEEDED',4); + interface Auth { /** @@ -9,12 +15,12 @@ interface Auth * @param Benutzername * @param Kennwort */ - function login( $username, $password ); - + function login( $username, $password, $token ); /** * Ermittelt den Benutzernamen. + * Der Benutzername wird verwendet, um die Loginmaske vorauszufüllen. */ function username(); } diff --git a/auth/CookieAuth.class.php b/auth/CookieAuth.class.php @@ -21,10 +21,11 @@ class CookieAuth implements Auth /** * Ueberpruefen des Kennwortes ist über Ident nicht möglich. */ - public function login( $user, $password ) + public function login( $user, $password, $token ) { return false; } + } ?> \ No newline at end of file diff --git a/auth/DatabaseAuth.class.php b/auth/DatabaseAuth.class.php @@ -11,7 +11,7 @@ class DatabaseAuth implements Auth /** * Login. */ - public function login( $user, $password ) + public function login( $user, $password, $token ) { global $conf; @@ -22,15 +22,16 @@ class DatabaseAuth implements Auth $authdb = new DB( $authDbConf ); - $sql = $authdb->sql( $conf['security']['authdb']['sql'] ); + $sql = $authdb->sql( $conf['security']['authdb']['sql'] ); + $algo = $authdb->sql( $conf['security']['authdb']['hash_algo'] ); $sql->setString('username',$user ); - $sql->setString('password',$password); + $sql->setString('password',hash($algo,$password)); $row = $sql->getRow(); $ok = !empty($row); // noch nicht implementiert: $authdb->close(); - return $ok; + return $ok?OR_AUTH_STATUS_SUCCESS:OR_AUTH_STATUS_FAILED; } public function username() diff --git a/auth/GuestAuth.class.php b/auth/GuestAuth.class.php @@ -24,7 +24,7 @@ class GuestAuth implements Auth /** * Ueberpruefen des Kennwortes ist über Ident nicht möglich. */ - public function login( $user, $password ) + public function login( $user, $password, $token ) { return false; } diff --git a/auth/HttpAuth.class.php b/auth/HttpAuth.class.php @@ -25,7 +25,7 @@ class HttpAuth implements Auth * * Das Kennwort wird gegen einen HTTP-Server geprüft. */ - public function login( $user, $password ) + public function login( $user, $password, $token ) { global $conf; diff --git a/auth/IdentAuth.class.php b/auth/IdentAuth.class.php @@ -48,9 +48,9 @@ class IdentAuth implements Auth /** * Ueberpruefen des Kennwortes ist über Ident nicht möglich. */ - public function login( $user, $password ) + public function login( $user, $password, $token ) { - return false; + return OR_AUTH_STATUS_FAILED; } } diff --git a/auth/InternalAuth.class.php b/auth/InternalAuth.class.php @@ -14,10 +14,8 @@ class InternalAuth implements Auth * Ueberpruefen des Kennwortes * ueber die Benutzertabelle in der Datenbank. */ - function login( $username, $password ) + function login( $username, $password,$token ) { - global $conf; - $db = db_connection(); // Lesen des Benutzers aus der DB-Tabelle @@ -30,31 +28,58 @@ SQL $row_user = $sql->getRow( $sql ); - if ( empty($row_user) ) - // Benutzer ist nicht vorhanden - return false; + if ( empty($row_user) ) { + + // Benutzer ist nicht vorhanden. + // Trotzdem das Kennwort hashen, um Timingattacken zu verhindern. + $unusedHash = Password::hash(User::pepperPassword($password),Password::bestAlgoAvailable() ); + return false; + } + // Pruefen ob Kennwort mit Datenbank uebereinstimmt. - elseif ( Password::check(User::pepperPassword($password),$row_user['password_hash'],$row_user['password_algo']) && $row_user['password_algo'] == OR_PASSWORD_ALGO_PLAIN ) + if ( ! Password::check(User::pepperPassword($password),$row_user['password_hash'],$row_user['password_algo']) ) + { + return false; + } + + // Behandeln von Klartext-Kennwoertern (Igittigitt). + if ( $row_user['password_algo'] == OR_PASSWORD_ALGO_PLAIN ) { - // Kennwort stimmt mit Datenbank �berein, aber nur im Klartext. - // Das Kennwort muss ge�ndert werden - $this->mustChangePassword = true; - - // Login nicht erfolgreich - return false; + if ( config('security','password','force_change_if_cleartext') ) + // Kennwort steht in der Datenbank im Klartext. + // Das Kennwort muss geaendert werden + return OR_AUTH_STATUS_PW_EXPIRED; + + // Anderenfalls ist das Login zwar moeglich, aber das Kennwort wird automatisch neu gehasht, weil der beste Algo erzwungen wird. + // Das Klartextkennwort waere danach ueberschrieben. } - // Pruefen ob Kennwort mit Datenbank uebereinstimmt - elseif ( Password::check(User::pepperPassword($password),$row_user['password_hash'],$row_user['password_algo']) ) + + if ( $row_user['password_expires'] != null && $row_user['password_expires'] < time() ) + { + // Kennwort ist abgelaufen. + if ( config('security','password','deny_if_expired') ) + return false; // Abgelaufenes Kennwort wird nicht mehr akzeptiert. + else + return OR_AUTH_STATUS_PW_EXPIRED; + } + + if ( $row_user['totp'] == 1 ) { - // Die Kennwort-Pruefsumme stimmt mit dem aus der Datenbank �berein. - // Juchuu, Login ist erfolgreich. - return true; + $user = new User($row_user['id']); + $user->load(); + if ( $user->getTOTPCode() == $token ) + return true; + else + return OR_AUTH_STATUS_TOKEN_NEEDED; } - else + + if ( $row_user['hotp'] == 1 ) { - // Kennwort stimmt garnicht ueberein. - return false; + // HOTP not yet implemented. } + + // Benutzer wurde erfolgreich authentifiziert. + return true; } public function username() diff --git a/auth/LdapAuth.class.php b/auth/LdapAuth.class.php @@ -3,8 +3,9 @@ class LdapAuth implements Auth { - public function login($username, $password) + public function login($username, $password, $token) { + global $conf; $db = db_connection(); $this->mustChangePassword = false; diff --git a/auth/LdapUserDNAuth.class.php b/auth/LdapUserDNAuth.class.php @@ -11,7 +11,7 @@ class LdapUserDNAuth implements Auth /** * @see Auth::login() */ - public function login($username, $password) + public function login($username, $password, $token) { $db = db_connection(); $this->mustChangePassword = false; diff --git a/auth/OpenIdAuth.class.php b/auth/OpenIdAuth.class.php @@ -13,7 +13,7 @@ class OpenIdAuth implements Auth } - function login( $username, $password ) + function login( $username, $password, $token ) { return false; } diff --git a/auth/PersonasAuth.class.php b/auth/PersonasAuth.class.php diff --git a/auth/RememberAuth.class.php b/auth/RememberAuth.class.php @@ -47,7 +47,7 @@ class RememberAuth implements Auth /** * Ueberpruefen des Kennwortes ist über den Cookie nicht möglich. */ - public function login( $user, $password ) + public function login( $user, $password, $token ) { return false; } diff --git a/auth/SSLAuth.class.php b/auth/SSLAuth.class.php @@ -9,13 +9,16 @@ class SSLAuth implements Auth { public function username() { + $conf = config('security','ssl'); + if ( isset($_SERVER[config('security','ssl','client_cert_dn_env')])) + return $_SERVER[config('security','ssl','client_cert_dn_env')]; } /** - * Ueberpruefen des Kennwortes ist über Ident nicht möglich. + * Ueberpruefen des Kennwortes ist nicht möglich. */ - public function login( $user, $password ) + public function login( $user, $password, $token ) { return false; } diff --git a/auth/SingleSignonAuth.class.php b/auth/SingleSignonAuth.class.php @@ -15,7 +15,7 @@ class SingleSignonAuth implements Auth /** * Ueberpruefen des Kennwortes ist über Ident nicht möglich. */ - public function login( $user, $password ) + public function login( $user, $password, $token ) { return false; } diff --git a/auth/include.inc.php b/auth/include.inc.php @@ -9,7 +9,6 @@ require_once( OR_AUTHCLASSES_DIR."InternalAuth.class.".PHP_EXT ); require_once( OR_AUTHCLASSES_DIR."LdapAuth.class.".PHP_EXT ); require_once( OR_AUTHCLASSES_DIR."LdapUserDNAuth.class.".PHP_EXT ); require_once( OR_AUTHCLASSES_DIR."OpenIdAuth.class.".PHP_EXT ); -require_once( OR_AUTHCLASSES_DIR."PersonasAuth.class.".PHP_EXT ); require_once( OR_AUTHCLASSES_DIR."RememberAuth.class.".PHP_EXT ); require_once( OR_AUTHCLASSES_DIR."SingleSignonAuth.class.".PHP_EXT ); require_once( OR_AUTHCLASSES_DIR."SSLAuth.class.".PHP_EXT ); diff --git a/config/config-default.php b/config/config-default.php @@ -745,6 +745,8 @@ $conf['security']['password'] = array(); $conf['security']['password']['random_length']=10; $conf['security']['password']['min_length']=6; $conf['security']['password']['pepper']= ''; +$conf['security']['password']['deny_if_expired'] = false; +$conf['security']['password']['force_change_if_cleartext']= true; $conf['security']['http'] = array(); $conf['security']['http']['url']= "http://example.net/restricted-area"; $conf['security']['authdb'] = array(); @@ -756,11 +758,12 @@ $conf['security']['authdb']['host']= '127.0.0.1'; $conf['security']['authdb']['database']='dbname'; $conf['security']['authdb']['persistent']=false; $conf['security']['authdb']['prepare']=false; -$conf['security']['authdb']['sql']= "select 1 from table where user={username} and password=md5({password})"; +$conf['security']['authdb']['sql']= "select 1 from table where user={username} and password={password}"; +$conf['security']['authdb']['hash_algo']='md5'; $conf['security']['authdb']['add']=true; $conf['security']['ssl'] = array(); -$conf['security']['ssl']['user_var']=''; $conf['security']['ssl']['trust']=false; +$conf['security']['ssl']['client_cert_dn_env'] = 'SSL_CLIENT_S_DN_CN'; $conf['security']['openid'] = array(); $conf['security']['openid']['enable']=false; $conf['security']['openid']['add']=false; diff --git a/language/de.ini.php b/language/de.ini.php @@ -1198,3 +1198,5 @@ ERROR_IN_ELEMENT="Dieses Seitenelement konnte nicht erzeugt werden" USER_PASSWORD_EXPIRES=Kennwort läuft ab USER_HOTP=Zählerbasiertes Token als Zweifaktorauthentifizierung USER_TOTP=Zeitbasieres Token als Zweifaktorauthentifizierung +NOTICE_LOGIN_FAILED_TOKEN_FAILED=Bitte geben Sie ein gültiges Token ein +USER_TOKEN=Token+ \ No newline at end of file diff --git a/model/User.class.php b/model/User.class.php @@ -942,6 +942,40 @@ SQL } + + /** + * Calculate the code, with given secret and point in time. + * + * @param string $secret + * @param int|null $timeSlice + * + * @return string + */ + public function getTOTPCode() + { + $codeLength = 6; + $timeSlice = floor(time() / 30); + $secretkey = hex2bin($this->otpSecret); + // Pack time into binary string + $time = chr(0).chr(0).chr(0).chr(0).pack('N*', $timeSlice); + // Hash it with users secret key + $hm = hash_hmac('SHA1', $time, $secretkey, true); + // Use last nipple of result as index/offset + $offset = ord(substr($hm, -1)) & 0x0F; + // grab 4 bytes of the result + $hashpart = substr($hm, $offset, 4); + // Unpak binary value + $value = unpack('N', $hashpart); + $value = $value[1]; + // Only 32 bits + $value = $value & 0x7FFFFFFF; + $modulo = pow(10, $codeLength); + return str_pad($value % $modulo, $codeLength, '0', STR_PAD_LEFT); + } + + + + } ?> \ No newline at end of file