commit 168c493b350b1f863f4e753267411fe6b1e6b44c
parent 0cf5f829c4b6801203698d5382f981941dd9ae46
Author: Jan Dankert <devnull@localhost>
Date: Tue, 7 Nov 2017 23:22:45 +0100
Login mit TOTP-Token.
Diffstat:
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