diff --git a/install/sql/incremental/upd_dev_collection.sql b/install/sql/incremental/upd_dev_collection.sql
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c99596926b399b22291c617dc1479211de493ea7 100644
--- a/install/sql/incremental/upd_dev_collection.sql
+++ b/install/sql/incremental/upd_dev_collection.sql
@@ -0,0 +1 @@
+ALTER TABLE `sys_user` ADD `otp_type` SET('none', 'email') NOT NULL DEFAULT 'none' AFTER `lost_password_reqtime`, ADD `otp_data` VARCHAR(255) NULL AFTER `otp_type`, ADD `otp_recovery` VARCHAR(64) NULL AFTER `otp_data`, ADD `otp_attempts` TINYINT NOT NULL DEFAULT '0' AFTER `otp_recovery`;
diff --git a/install/sql/ispconfig3.sql b/install/sql/ispconfig3.sql
index ec6596bc779e4274d5612273e5f9fa96fd9a27cd..cf340bda0170cb77039941455303d76ee28066ca 100644
--- a/install/sql/ispconfig3.sql
+++ b/install/sql/ispconfig3.sql
@@ -1842,6 +1842,10 @@ CREATE TABLE `sys_user` (
   `lost_password_function` tinyint(1) NOT NULL default '1',
   `lost_password_hash` VARCHAR(50) NOT NULL default '',
   `lost_password_reqtime` DATETIME NULL default NULL,
+  `otp_type` set('none', 'email') NOT NULL DEFAULT 'none',
+  `otp_data` varchar(255) DEFAULT NULL,
+  `otp_recovery` varchar(64) DEFAULT NULL,
+  `otp_attempts` tinyint(4) NOT NULL DEFAULT 0,
   PRIMARY KEY  (`userid`)
 ) DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
 
diff --git a/interface/lib/app.inc.php b/interface/lib/app.inc.php
old mode 100755
new mode 100644
index 96e8a1ddef0892b1ca35186ea08d263af75b21be..6a8e563d80fade4e9ccb9482004017b8e6149c5e
--- a/interface/lib/app.inc.php
+++ b/interface/lib/app.inc.php
@@ -212,6 +212,12 @@ class app {
 		}
 	}
 
+	public function auth_log($msg) {
+		$authlog_handle = fopen($this->_conf['ispconfig_log_dir'].'/auth.log', 'a');
+		fwrite($authlog_handle, $msg . PHP_EOL);
+		fclose($authlog_handle);
+	}
+
 	/** Priority values are: 0 = DEBUG, 1 = WARNING,  2 = ERROR */
 	public function error($msg, $next_link = '', $stop = true, $priority = 1) {
 		//$this->uses("error");
diff --git a/interface/web/admin/form/users.tform.php b/interface/web/admin/form/users.tform.php
index 1aab0a42985bb8b28932393366e4de14d67f8aa2..bc77087e2852c71d10f416e559b23291ab86b243 100644
--- a/interface/web/admin/form/users.tform.php
+++ b/interface/web/admin/form/users.tform.php
@@ -94,6 +94,11 @@ while ($file = @readdir($handle)) {
 	}
 }
 
+$otp_method_list = array(
+	'none' => 'none',
+	'email' => 'email',
+);
+
 //* Load themes
 $themes_list = array();
 $handle = @opendir(ISPC_THEMES_PATH);
@@ -254,6 +259,25 @@ $form['tabs']['users'] = array (
 			'rows'  => '',
 			'cols'  => ''
 		),
+		'otp_type' => array(
+				'datatype' => 'VARCHAR',
+				'formtype' => 'SELECT',
+				'validators' => array (  0 => array (    'type' => 'NOTEMPTY',
+						'errmsg'=> 'otp_auth_empty'),
+					1 => array (    'type' => 'REGEX',
+						'regex' => '/^[a-z0-9\_]{0,64}$/',
+						'errmsg'=> 'otp_auth_regex'),
+					),
+				'regex'  => '',
+				'errmsg' => '',
+				'default' => '',
+				'value'  => $otp_method_list,
+				'separator' => '',
+				'width'  => '30',
+				'maxlength' => '255',
+				'rows'  => '',
+				'cols'  => ''
+				),
 		'language' => array (
 			'datatype' => 'VARCHAR',
 			'formtype' => 'SELECT',
diff --git a/interface/web/admin/lib/lang/ar_users.lng b/interface/web/admin/lib/lang/ar_users.lng
index 369d56559510f445a96d40969035f4cbfde07ca5..d4acbf4e807d1a9ad740c14e6c5bfbdd12d1d594 100644
--- a/interface/web/admin/lib/lang/ar_users.lng
+++ b/interface/web/admin/lib/lang/ar_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/bg_users.lng b/interface/web/admin/lib/lang/bg_users.lng
index a7d669bd2284be54fffb0e9c9f7be1989db24aac..3bd134b40ba3fd2b52502329e1e409dac4f5f4fe 100644
--- a/interface/web/admin/lib/lang/bg_users.lng
+++ b/interface/web/admin/lib/lang/bg_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/br_users.lng b/interface/web/admin/lib/lang/br_users.lng
index 2e60a744c0c0ed605b0bfed34cd159495d57d490..022d723c97720740209c2897b0b80ab0854440be 100644
--- a/interface/web/admin/lib/lang/br_users.lng
+++ b/interface/web/admin/lib/lang/br_users.lng
@@ -38,3 +38,4 @@ $wb['startmodule_empty'] = 'O módulo inicial está vazio.';
 $wb['startmodule_regex'] = 'Caracteres inválidos no módulo inicial.';
 $wb['app_theme_empty'] = 'Tema está vazio.';
 $wb['app_theme_regex'] = 'Caracteres inválidos no tema.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
diff --git a/interface/web/admin/lib/lang/ca_users.lng b/interface/web/admin/lib/lang/ca_users.lng
index 369d56559510f445a96d40969035f4cbfde07ca5..d4acbf4e807d1a9ad740c14e6c5bfbdd12d1d594 100644
--- a/interface/web/admin/lib/lang/ca_users.lng
+++ b/interface/web/admin/lib/lang/ca_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/cz_users.lng b/interface/web/admin/lib/lang/cz_users.lng
index c973ea2f8044ee67504a22e3c6d8ca6422d32883..6ad957242a28ffa42c4929c4ada7b55f542949bd 100644
--- a/interface/web/admin/lib/lang/cz_users.lng
+++ b/interface/web/admin/lib/lang/cz_users.lng
@@ -38,3 +38,4 @@ $wb['startmodule_empty'] = 'Startmodule empty.';
 $wb['startmodule_regex'] = 'Invalid chars in Startmodule.';
 $wb['app_theme_empty'] = 'App theme empty.';
 $wb['app_theme_regex'] = 'Invalid chars in App theme.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
diff --git a/interface/web/admin/lib/lang/de_users.lng b/interface/web/admin/lib/lang/de_users.lng
index e52090dceaef804ea83f49f7deac564978dc7405..2747595da72aaf30b5f8beffa0bb233d8bb9bfbc 100644
--- a/interface/web/admin/lib/lang/de_users.lng
+++ b/interface/web/admin/lib/lang/de_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Der Benutzername darf nicht <b>web<b> oder <b
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Passwort vergessen Funktion steht zur Verfügung';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/dk_users.lng b/interface/web/admin/lib/lang/dk_users.lng
index fa595ed18415832f257a8bcab11a6bdd0bfae91c..351cd091afce3513693700c8d09a8a704b9d65fb 100644
--- a/interface/web/admin/lib/lang/dk_users.lng
+++ b/interface/web/admin/lib/lang/dk_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Brugernavn må ikke være web eller web plus
 $wb['client_not_admin_err'] = 'En bruger der hører til en klient kan ikke indstilles til typen: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/el_users.lng b/interface/web/admin/lib/lang/el_users.lng
index 6d07d6632457fc5dc1d854d7243669fffbadfeb6..0b46b3a52247913458c777f15ec09637d11db65b 100644
--- a/interface/web/admin/lib/lang/el_users.lng
+++ b/interface/web/admin/lib/lang/el_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/en_users.lng b/interface/web/admin/lib/lang/en_users.lng
index 88fa9430d3837b06fff7ee78535e1cd227e3f44f..855146d171063b86d81b3b2835090e266d9f5a1a 100644
--- a/interface/web/admin/lib/lang/en_users.lng
+++ b/interface/web/admin/lib/lang/en_users.lng
@@ -38,4 +38,5 @@ $wb['startmodule_empty'] = 'Startmodule empty.';
 $wb['startmodule_regex'] = 'Invalid chars in Startmodule.';
 $wb['app_theme_empty'] = 'App theme empty.';
 $wb['app_theme_regex'] = 'Invalid chars in App theme.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/es_users.lng b/interface/web/admin/lib/lang/es_users.lng
index 5a06c7ed3e3a3319245b7f031391bed4879c2fa5..44c67b1832cc45f978d257efaa64eb49c26eaf09 100644
--- a/interface/web/admin/lib/lang/es_users.lng
+++ b/interface/web/admin/lib/lang/es_users.lng
@@ -34,4 +34,5 @@ $wb['username_txt'] = 'Nombre de usuario';
 $wb['username_unique'] = 'Ya existe un usuario con ese nombre de usuario.';
 $wb['vorname_txt'] = 'Primer nombre';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/fi_users.lng b/interface/web/admin/lib/lang/fi_users.lng
index 1e5bb588559e0eb4577bc05a9eb65decd3a572ab..02bc863bb7f49e178a8645b39f53b2c5e4564e36 100644
--- a/interface/web/admin/lib/lang/fi_users.lng
+++ b/interface/web/admin/lib/lang/fi_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/fr_users.lng b/interface/web/admin/lib/lang/fr_users.lng
index 0f50d10d67c97698be1a92ad9b7b49854c1f3736..5013698f16b97b52e29cb3dc21e6ce8592149c64 100644
--- a/interface/web/admin/lib/lang/fr_users.lng
+++ b/interface/web/admin/lib/lang/fr_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Le nom d\'utilisateur ne peut pas commencer p
 $wb['client_not_admin_err'] = 'Un utilisateur affilié à un client ne peut pas être changé en type admin';
 $wb['lost_password_function_txt'] = 'La fonction mot de passe oublié est disponible';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/hr_users.lng b/interface/web/admin/lib/lang/hr_users.lng
index 3a7da2a422baea63b92c33b16d33c341b932117d..e317aa923a6fc456bc6380bb85bb9ae6e898e951 100644
--- a/interface/web/admin/lib/lang/hr_users.lng
+++ b/interface/web/admin/lib/lang/hr_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Korisničko ime ne može biti web ili web sa
 $wb['client_not_admin_err'] = 'Korisnik koji je u grupi klijenti ne može biti admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/hu_users.lng b/interface/web/admin/lib/lang/hu_users.lng
index 6f1ded0b70b1a36c0744badc8fc6af0643ca78f1..dc1089b76fdb295707d737c7c973cbf7aa1812dd 100644
--- a/interface/web/admin/lib/lang/hu_users.lng
+++ b/interface/web/admin/lib/lang/hu_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/id_users.lng b/interface/web/admin/lib/lang/id_users.lng
index bc546d4df4e050b30ee5aeec15926a76e288139e..032613d1e543049aad7ffedd6ac1d4ee61c1a4d3 100644
--- a/interface/web/admin/lib/lang/id_users.lng
+++ b/interface/web/admin/lib/lang/id_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/it_users.lng b/interface/web/admin/lib/lang/it_users.lng
index 2c5dbeda480b1c84f05caf7237c37e95edfda014..57a462394aa55abfe9f5c0e8c32c04a4b6b51d26 100644
--- a/interface/web/admin/lib/lang/it_users.lng
+++ b/interface/web/admin/lib/lang/it_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Il nome utente non può essere web o web+un n
 $wb['client_not_admin_err'] = 'Un utente che appartiene ad un cliente non può essere del tipo: admin';
 $wb['lost_password_function_txt'] = 'La funzione password dimenticata è disponibile';
 $wb['no_user_insert'] = 'CP-Utente di tipo -utente- viene aggiunto automaticamente quando aggiungi un cliente o un rivenditore.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/ja_users.lng b/interface/web/admin/lib/lang/ja_users.lng
index 45b1faec67c05eadb93286c6826aa7153608c4b0..9786f8b57647670f316bd266fbc365923d480bde 100644
--- a/interface/web/admin/lib/lang/ja_users.lng
+++ b/interface/web/admin/lib/lang/ja_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/nl_users.lng b/interface/web/admin/lib/lang/nl_users.lng
index e1e6609c27d5e998244b8db740709464d7c18ffe..9013af5901e530ef3654c209092a1086d343b780 100644
--- a/interface/web/admin/lib/lang/nl_users.lng
+++ b/interface/web/admin/lib/lang/nl_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = 'Twee-factor authenticatie';
 ?>
diff --git a/interface/web/admin/lib/lang/pl_users.lng b/interface/web/admin/lib/lang/pl_users.lng
index f5439f4f5f5fe86dee7e7e7161be928071d6b4de..b879f671e298d52c7eb0b12136a074d5293fe35f 100644
--- a/interface/web/admin/lib/lang/pl_users.lng
+++ b/interface/web/admin/lib/lang/pl_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Nazwa użytkownika nie może być web lub web
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/pt_users.lng b/interface/web/admin/lib/lang/pt_users.lng
index 7522fb0270d615bcbe2e3d6197ab16baffce1d08..f1cf858176002e2589ad2ead532bbe05a6574ea9 100644
--- a/interface/web/admin/lib/lang/pt_users.lng
+++ b/interface/web/admin/lib/lang/pt_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/ro_users.lng b/interface/web/admin/lib/lang/ro_users.lng
index 369d56559510f445a96d40969035f4cbfde07ca5..d4acbf4e807d1a9ad740c14e6c5bfbdd12d1d594 100644
--- a/interface/web/admin/lib/lang/ro_users.lng
+++ b/interface/web/admin/lib/lang/ro_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/ru_users.lng b/interface/web/admin/lib/lang/ru_users.lng
index d805b939116e50996d59f9ef73103b98476a69c0..5d67479d701ba0f83803f00a81a8e1a3c2bb0d7f 100644
--- a/interface/web/admin/lib/lang/ru_users.lng
+++ b/interface/web/admin/lib/lang/ru_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Имя пользователя не може
 $wb['client_not_admin_err'] = 'Пользователь, который принадлежит к клиенту не может быть установлен тип: admin';
 $wb['lost_password_function_txt'] = 'Функция восстановления пароля доступна';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/se_users.lng b/interface/web/admin/lib/lang/se_users.lng
index 60f3829adc9237e5453a4bbe9029e58f2bada62f..a8baa7ee79886fdcd1cb201adef121b9637e42c0 100644
--- a/interface/web/admin/lib/lang/se_users.lng
+++ b/interface/web/admin/lib/lang/se_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Användarnamnet får inte vara web  eller web
 $wb['client_not_admin_err'] = 'En användare som tillhör en kund kan inte sättas som admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/sk_users.lng b/interface/web/admin/lib/lang/sk_users.lng
index 9cc7f87e0d23dc6feb43ad2d26664c3b336e7918..f0c863d749ed2190a06956dd65b6c7f2c6a99c5b 100644
--- a/interface/web/admin/lib/lang/sk_users.lng
+++ b/interface/web/admin/lib/lang/sk_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'The username may not be web or web plus a num
 $wb['client_not_admin_err'] = 'A user that belongs to a client can not be set to type: admin';
 $wb['lost_password_function_txt'] = 'Forgot password function is available';
 $wb['no_user_insert'] = 'CP-Users of type -user- get added and updated automatically when you add a client or reseller.';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/lib/lang/tr_users.lng b/interface/web/admin/lib/lang/tr_users.lng
index 84826357b7a91726f0e2120f36c4489ac5b6d210..3483c48fb984f665d575428e3a766bb528753424 100644
--- a/interface/web/admin/lib/lang/tr_users.lng
+++ b/interface/web/admin/lib/lang/tr_users.lng
@@ -34,4 +34,5 @@ $wb['username_error_collision'] = 'Kullanıcı adı -web- ya da -web- sözcüğ
 $wb['client_not_admin_err'] = 'Bir müşteriye ait bir kullanıcının türü admin olarak atanamaz';
 $wb['lost_password_function_txt'] = 'Parolamı unuttum özelliği kullanılabilir';
 $wb['no_user_insert'] = 'Bir müşteri ya da bayi eklediğinizde -user- türündeki kontrol panosu kullanıcıları otomatik olarak eklenir ve güncellenir .';
+$wb['otp_auth_txt'] = '2-Factor Authentication';
 ?>
diff --git a/interface/web/admin/templates/users_user_edit.htm b/interface/web/admin/templates/users_user_edit.htm
index 234f40f25ef0a11cac4d20389061ad388bd71efa..4e8e3da496fb368a6c90017e669519ff92ccdc7b 100644
--- a/interface/web/admin/templates/users_user_edit.htm
+++ b/interface/web/admin/templates/users_user_edit.htm
@@ -28,6 +28,15 @@
 					<div id="confirmpasswordOK" style="display:none;" class="confirmpasswordok">{tmpl_var name='password_match_txt'}</div>
 				</div>
 			</div>
+            <div class="form-group">
+              <label for="otp_type" class="col-sm-3 control-label">{tmpl_var name='otp_auth_txt'}</label>
+              <div class="col-sm-9">
+                <select name="otp_type" id="otp_type" class="form-control">
+                  {tmpl_var name='otp_type'}
+                </select>
+              </div>
+            </div>
+
             <div class="form-group">
                 <label class="col-sm-3 control-label">{tmpl_var name='modules_txt'}</label>
                 <div class="col-sm-9">
diff --git a/interface/web/login/index.php b/interface/web/login/index.php
index d59d24efc8273dc6ab5781f117a5f640a667daa8..c4643f4ed2f9f7c076df06a15106b40c1ba93cb8 100644
--- a/interface/web/login/index.php
+++ b/interface/web/login/index.php
@@ -126,13 +126,6 @@ function process_login_request(app $app, &$error, $conf, $module)
 			}
 		}
 
-		$app->plugin->raiseEvent('login', $username);
-
-		//* Save successful login message to var
-		$authlog = 'Successful login for user \''.$username.'\' from '.$_SERVER['REMOTE_ADDR'].' at '.date('Y-m-d H:i:s').' with session ID '.session_id();
-		$authlog_handle = fopen($conf['ispconfig_log_dir'].'/auth.log', 'a');
-		fwrite($authlog_handle, $authlog."\n");
-		fclose($authlog_handle);
 
 		/*
 		* We need LOGIN_REDIRECT instead of HEADER_REDIRECT to load the
@@ -141,10 +134,33 @@ function process_login_request(app $app, &$error, $conf, $module)
 
 		if ($loginAs) {
 			echo 'LOGIN_REDIRECT:'.$_SESSION['s']['module']['startpage'];
+			$app->plugin->raiseEvent('login', $username);
+			$app->auth_log('Successful login for user \''. $username .'\' ' . $msg . ' from '. $_SERVER['REMOTE_ADDR'] .' at '. date('Y-m-d H:i:s') . ' with session ID ' .session_id());
 			exit;
 		} else {
-			header('Location: ../index.php');
-			die();
+
+			//* Do 2FA authentication
+			if($user['otp_type'] != 'none') {
+
+				//* Save session in pending state and destroy original session
+				$_SESSION['s_pending'] = $_SESSION['s'];
+				unset($_SESSION['s']);
+
+				//* Create OTP session
+				$_SESSION['otp']['session_attempts'] = 0;
+				$_SESSION['otp']['type'] = $user['otp_type'];
+				$_SESSION['otp']['data'] = $user['otp_data'];
+				//$_SESSION['otp']['recovery_debug'] = $user['otp_recovery']; // For DEBUG only.
+
+				//* Redirect to otp script
+				header('Location: otp.php');
+				die();
+			} else {
+				$app->plugin->raiseEvent('login', $username);
+				$app->auth_log('Successful login for user \''. $username .'\' ' . $msg . ' from '. $_SERVER['REMOTE_ADDR'] .' at '. date('Y-m-d H:i:s') . ' with session ID ' .session_id());
+				header('Location: ../index.php');
+				die();
+			}
 		}
 	} else {
 		if (!$alreadyfailed['times']) {
@@ -161,11 +177,7 @@ function process_login_request(app $app, &$error, $conf, $module)
 		if ($app->db->errorMessage != '') $error .= '<br />'.$app->db->errorMessage != '';
 
 		$app->plugin->raiseEvent('login_failed', $username);
-		//* Save failed login message to var
-		$authlog = 'Failed login for user \''.$username.'\' from '.$_SERVER['REMOTE_ADDR'].' at '.date('Y-m-d H:i:s');
-		$authlog_handle = fopen($conf['ispconfig_log_dir'].'/auth.log', 'a');
-		fwrite($authlog_handle, $authlog."\n");
-		fclose($authlog_handle);
+		$app->auth_log('Failed login for user \''. $username .'\ from '. $_SERVER['REMOTE_ADDR'] .' at '. date('Y-m-d H:i:s'));
 	}
 }
 
diff --git a/interface/web/login/lib/lang/ar.lng b/interface/web/login/lib/lang/ar.lng
index a50df19db41051fb421ffe9dc5673ba273bda9c9..79ff8def0bfad10129e96da146ec9a7629d29b32 100644
--- a/interface/web/login/lib/lang/ar.lng
+++ b/interface/web/login/lib/lang/ar.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/bg.lng b/interface/web/login/lib/lang/bg.lng
index 8081ec8097f2efe56bed1d001c7365be9b46e29a..18f46d6df8f5be5e44473db554981d8c28b95807 100644
--- a/interface/web/login/lib/lang/bg.lng
+++ b/interface/web/login/lib/lang/bg.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/br.lng b/interface/web/login/lib/lang/br.lng
index 382bd1b8ac14962c3ffd4104fed43758ffc4612e..1f5b41c369389027fae7cb32be3a66c392895a39 100644
--- a/interface/web/login/lib/lang/br.lng
+++ b/interface/web/login/lib/lang/br.lng
@@ -32,3 +32,12 @@ $wb['pw_reset_act_mail_msg'] = 'Por favor, confirme se você deseja reiniciar su
 $wb['lost_password_function_wait_txt'] = 'Você não pode requisitar uma nova senha ainda. Por favor, aguarde alguns minutos.';
 $wb['lost_password_function_expired_txt'] = 'Este link de ativação expirou. Por favor, faça uma nova requisição.';
 $wb['lost_password_function_denied_txt'] = 'Este link de ativação não é válido.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
diff --git a/interface/web/login/lib/lang/ca.lng b/interface/web/login/lib/lang/ca.lng
index 8c672b59c5caac8bbef310f796b5e283799f5f40..f4aa11776a941d8b506392f45007d051efa6700f 100644
--- a/interface/web/login/lib/lang/ca.lng
+++ b/interface/web/login/lib/lang/ca.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/cz.lng b/interface/web/login/lib/lang/cz.lng
index 9b22d7d3c776076375eef06dcfc64f313d871c32..a57ff3f7044885f0acae16a68495376c7e63e0bf 100644
--- a/interface/web/login/lib/lang/cz.lng
+++ b/interface/web/login/lib/lang/cz.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/de.lng b/interface/web/login/lib/lang/de.lng
index 446f04edc87eb44b17263d5b4754f1966679f848..dcb94af7168cafcdd4af58137fd246fe8933bb1f 100644
--- a/interface/web/login/lib/lang/de.lng
+++ b/interface/web/login/lib/lang/de.lng
@@ -32,4 +32,13 @@ $wb['lost_password_function_disabled_txt'] = 'Die Passwort vergessen Funktion st
 $wb['lost_password_function_wait_txt'] = 'Sie können im Moment kein neues Passwort anfordern. Bitte warten Sie einige Minuten.';
 $wb['lost_password_function_expired_txt'] = 'Der Passwortlink ist abgelaufen. Bitte fordern Sie einen neuen an.';
 $wb['lost_password_function_denied_txt'] = 'Dieser Passwortlink ist ungültig.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/dk.lng b/interface/web/login/lib/lang/dk.lng
index 8a104c45be9fdd0a364f519870d20134cd926b2e..37d41ddea1581710d1a24d512d35803ab79e27cc 100644
--- a/interface/web/login/lib/lang/dk.lng
+++ b/interface/web/login/lib/lang/dk.lng
@@ -32,4 +32,13 @@ $wb['lost_password_function_disabled_txt'] = 'The lost password function is not
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/el.lng b/interface/web/login/lib/lang/el.lng
index 8a6ed3387f8bb72829facb4051345e2197db9aa2..64a03b36f63e3bc1ec1856d7ac6aa87ce6397788 100644
--- a/interface/web/login/lib/lang/el.lng
+++ b/interface/web/login/lib/lang/el.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/en.lng b/interface/web/login/lib/lang/en.lng
index a99d0d3d092c47d3aa334bdef4de4c0d96cdf184..8dd94c6294f3c996fac2b331b7431323b0d7d1c3 100644
--- a/interface/web/login/lib/lang/en.lng
+++ b/interface/web/login/lib/lang/en.lng
@@ -32,4 +32,13 @@ $wb['lost_password_function_disabled_txt'] = 'The lost password function is not
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/es.lng b/interface/web/login/lib/lang/es.lng
index 273f69aabea71918947c92a351114595f2f3ae2c..dd8bd12bc4910604569c1d6576d2bf4aa54234ea 100644
--- a/interface/web/login/lib/lang/es.lng
+++ b/interface/web/login/lib/lang/es.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Por favor confirme que desea restablecer la cont
 $wb['lost_password_function_wait_txt'] = 'No puede solicitar una nueva contraseña todavía. Por favor, espere unos minutos.';
 $wb['lost_password_function_expired_txt'] = 'Este enlace de activación ha caducado. Por favor, solicite uno nuevo.';
 $wb['lost_password_function_denied_txt'] = 'Este enlace de activación no es válido.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/fi.lng b/interface/web/login/lib/lang/fi.lng
index 8198dd4822ae84dd7a2202b9e0b9ec726316148f..9bbcbdddabdd7cf09d666de995cf91194d15abe0 100644
--- a/interface/web/login/lib/lang/fi.lng
+++ b/interface/web/login/lib/lang/fi.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/fr.lng b/interface/web/login/lib/lang/fr.lng
index f067751aa9bc1499665c7126c494f4e78c51f48e..dfc04fd184601ab61b2bf3abe8648b5819bd07ea 100644
--- a/interface/web/login/lib/lang/fr.lng
+++ b/interface/web/login/lib/lang/fr.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/hr.lng b/interface/web/login/lib/lang/hr.lng
index 193123557a4b6931d20ef32de6c01ee782f3596b..1980c4292dcda9cbe5eef922e48e1c540b1e29d3 100644
--- a/interface/web/login/lib/lang/hr.lng
+++ b/interface/web/login/lib/lang/hr.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/hu.lng b/interface/web/login/lib/lang/hu.lng
index b6adb6b19d2dd3fb3ba25d24508061c972954ca1..99e2ca769bcc07b6de8534090e36af5feac6003b 100644
--- a/interface/web/login/lib/lang/hu.lng
+++ b/interface/web/login/lib/lang/hu.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/id.lng b/interface/web/login/lib/lang/id.lng
index 35e3675a9d9f630a024d13ca03b6e097b70b9331..615357f654f6b47fe4dc61eda6c32c2c0f88d1db 100644
--- a/interface/web/login/lib/lang/id.lng
+++ b/interface/web/login/lib/lang/id.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/it.lng b/interface/web/login/lib/lang/it.lng
index 7d83e22108a67d4d0a812dc76c3a551ddb5b7604..df498e978acc76c0f12db9149b0ab5891925a867 100644
--- a/interface/web/login/lib/lang/it.lng
+++ b/interface/web/login/lib/lang/it.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Per confermare il reset della password si prega
 $wb['lost_password_function_wait_txt'] = 'Non puoi richiedere una nuova password in questo momento. Riprova tra qualche minuto.';
 $wb['lost_password_function_expired_txt'] = 'Link di attivazione scaduto. Si prega di richiedere un nuovo link di attivazione.';
 $wb['lost_password_function_denied_txt'] = 'Link di attivazione non valido.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/ja.lng b/interface/web/login/lib/lang/ja.lng
index 4b9e2c62363720530033eee88a65e4639edd08be..bff26e9d83826f74edf744190f3a2737c0a72885 100644
--- a/interface/web/login/lib/lang/ja.lng
+++ b/interface/web/login/lib/lang/ja.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/nl.lng b/interface/web/login/lib/lang/nl.lng
index cd5f3b713df85fc5354847b9ec6a19d5a7d0bbfe..453aae6fac9bdc7ef6646a2fcd9e6db23c2f67f7 100644
--- a/interface/web/login/lib/lang/nl.lng
+++ b/interface/web/login/lib/lang/nl.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Twee-factor authenticatie';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'Een email is verstuurd aan';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authenticatie';
+$wb['otp_code_email_template_txt'] = 'Uw eenmalige login code is %s' . PHP_EOL . 'Deze code is geldig voor 10 minuten.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Aanvragen nieuwe code';
+$wb['otp_code_email_sent_failed_txt'] = 'Verzenden van email naar %s is mislukt.';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/pl.lng b/interface/web/login/lib/lang/pl.lng
index 9b2359b3eb7610481b194ff4aef087a835393804..f6937205425592ecc92a2903e5ca38fffc674404 100644
--- a/interface/web/login/lib/lang/pl.lng
+++ b/interface/web/login/lib/lang/pl.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'Nie możesz jeszcze zażądać nowego hasła. Poczekaj kilka minut.';
 $wb['lost_password_function_expired_txt'] = 'Link aktywacyjny wygasł. Poproś o nowy.';
 $wb['lost_password_function_denied_txt'] = 'Ten link aktywacyjny jest nieprawidłowy.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/pt.lng b/interface/web/login/lib/lang/pt.lng
index 4be018a50c72b54a1f4007b73e1a60b80b1a054a..fd453844ea22bee3cf366331f3135564623bbc38 100644
--- a/interface/web/login/lib/lang/pt.lng
+++ b/interface/web/login/lib/lang/pt.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/ro.lng b/interface/web/login/lib/lang/ro.lng
index 7676d928d57fbe6a18e2eb7182d9a80e7f6c714d..94950d6c069ab7652e9f75d7529d0d0bfa74cb33 100644
--- a/interface/web/login/lib/lang/ro.lng
+++ b/interface/web/login/lib/lang/ro.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/ru.lng b/interface/web/login/lib/lang/ru.lng
index 4f1b4e86fc8d4cd099161dd6d8f5fc5d7d31ad47..999723c2d6b378f775a7435d19fb2f1a7616f4a8 100644
--- a/interface/web/login/lib/lang/ru.lng
+++ b/interface/web/login/lib/lang/ru.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Для подтверждения сброса 
 $wb['lost_password_function_wait_txt'] = 'Вы не можете запросить новый пароль, пока нет. Пожалуйста, подождите несколько минут.';
 $wb['lost_password_function_expired_txt'] = 'Срок активации этой ссылки истек. Пожалуйста, запросите новую.';
 $wb['lost_password_function_denied_txt'] = 'Эта ссылка активации недействительна.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/se.lng b/interface/web/login/lib/lang/se.lng
index 4f8a8a44431c6787484fd280173fdd4b6e2c3b7b..db0aeba05ecba8c39d72a3858dd2981a5a0489e7 100644
--- a/interface/web/login/lib/lang/se.lng
+++ b/interface/web/login/lib/lang/se.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/sk.lng b/interface/web/login/lib/lang/sk.lng
index 8c211b19beab44a010faa95f82f731c0988b04ef..20ed8f74a79446c9e33e8f834276685dabbd3e09 100644
--- a/interface/web/login/lib/lang/sk.lng
+++ b/interface/web/login/lib/lang/sk.lng
@@ -32,4 +32,13 @@ $wb['pw_reset_act_mail_msg'] = 'Please confirm that your want to reset your ISPC
 $wb['lost_password_function_wait_txt'] = 'You cannot request a new password, yet. Please wait a few minutes.';
 $wb['lost_password_function_expired_txt'] = 'This activation link has expired. Please request a new one.';
 $wb['lost_password_function_denied_txt'] = 'This activation link is not valid.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/lib/lang/tr.lng b/interface/web/login/lib/lang/tr.lng
index 7bcc75f6942b52238d1c80546c0397d3c29d908d..f30f1fb35440cb52be0ab93803ccd10239acb32e 100644
--- a/interface/web/login/lib/lang/tr.lng
+++ b/interface/web/login/lib/lang/tr.lng
@@ -32,4 +32,13 @@ $wb['lost_password_function_disabled_txt'] = 'Bu kullanıcı parolamı unuttum 
 $wb['lost_password_function_wait_txt'] = 'Henüz yeni parola isteğinde bulunamazsınız. Lütfen bir kaç dakika bekleyin.';
 $wb['lost_password_function_expired_txt'] = 'Bu etkinleştirme bağlantısının süresi geçmiş. Lütfen yeni bir parola sıfırlama isteğinde bulunun.';
 $wb['lost_password_function_denied_txt'] = 'Bu etkinleştirme bağlantısı geçersiz.';
+$wb['otp_code_txt'] = 'Two Factor Authentication';
+$wb['otp_code_desc_txt'] = 'Enter the code you got from your authenticator app or via email.';
+$wb['otp_code_placeholder_txt'] = 'OTP code';
+$wb['otp_code_email_sent_txt'] = 'An email was sent to';
+$wb['otp_code_email_subject_txt'] = 'ISPConfig Login authentication';
+$wb['otp_code_email_template_txt'] = 'Your One time login code is %s' . PHP_EOL . 'This code is valid for 10 minutes.' . PHP_EOL;
+$wb['otp_code_resend_txt'] = 'Request new code';
+$wb['otp_code_email_sent_failed_txt'] = 'Failed sending an email to %s';
+$wb['otp_code_email_sent_wait_txt'] = 'Please wait, re-sending the code is only possible after %s seconds.';
 ?>
diff --git a/interface/web/login/otp.php b/interface/web/login/otp.php
new file mode 100644
index 0000000000000000000000000000000000000000..8b80691da6f20a626c518db9a89d12c8cc335d9e
--- /dev/null
+++ b/interface/web/login/otp.php
@@ -0,0 +1,231 @@
+<?php
+
+/*
+Copyright (c) 2021, Till Brehm, ISPConfig UG
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice,
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright notice,
+      this list of conditions and the following disclaimer in the documentation
+      and/or other materials provided with the distribution.
+    * Neither the name of ISPConfig nor the names of its contributors
+      may be used to endorse or promote products derived from this software without
+      specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+require_once '../../lib/config.inc.php';
+require_once '../../lib/app.inc.php';
+
+// Check if we have an active users ession.
+if($_SESSION['s']['user']['active'] == 1) {
+	header('Location: /index.php');
+	die();
+}
+
+// If we don't have a 2fa session go back to login page.
+if(!isset($_SESSION['otp'])) {
+	header('Location: index.php');
+	die();
+}
+
+// Variables and settings.
+$error = '';
+$msg = '';
+$max_session_code_retry = 3;
+$max_global_code_retry = 10;
+$otp_recovery_code_length = 32;
+
+// CSRF Check if we got POST data.
+if(count($_POST) >= 1) {
+	$app->auth->csrf_token_check();
+}
+
+require ISPC_ROOT_PATH.'/web/login/lib/lang/'.$app->functions->check_language($conf['language']).'.lng';
+
+function finish_2fa_success($msg = '') {
+	global $app;
+	$_SESSION['s'] = $_SESSION['s_pending'];
+	unset($_SESSION['s_pending']);
+	unset($_SESSION['otp']);
+	$username = $_SESSION['s']['user']['username'];
+	if (!empty($msg)) {
+		$msg = ' ' . $msg;
+	}
+	$app->auth_log('Successful login for user \''. $username .'\'' . $msg . ' from '. $_SERVER['REMOTE_ADDR'] .' at '. date('Y-m-d H:i:s') . ' with session ID ' .session_id());
+	$app->db->query('UPDATE `sys_user` SET otp_attempts=0 WHERE userid = ?', $_SESSION['s']['user']['userid']);
+	session_write_close();
+	header('Location: ../index.php');
+	die();
+}
+
+// Handle recovery code
+if(isset($_POST['code']) && strlen($_POST['code']) == $otp_recovery_code_length) {
+	//* TODO Recovery code handling
+
+	$user = $app->db->queryOneRecord('SELECT otp_attempts, otp_recovery FROM sys_user WHERE userid = ?', $_SESSION['s_pending']['user']['userid']);
+
+	//* We allow one more try to enter recovery code
+	if($user['otp_attempts'] > $max_global_code_retry + 1) {
+		die("Sorry, contact your administrator.");
+	}
+
+	if (password_verify($_POST['code'], $user['otp_recovery'])) {
+		finish_2fa_success('via 2fa recovery code');
+	}
+	else {
+		$app->db->query('UPDATE `sys_user` SET otp_attempts=otp_attempts + 1 WHERE userid = ?', $_SESSION['s_pending']['user']['userid']);
+	}
+}
+
+
+// Begin 2fa via Email.
+if($_SESSION['otp']['type'] == 'email') {
+
+	//* Email 2fa handler settings
+	$max_code_resend = 3;
+	$max_time = 600; // time in seconds until the code gets invalidated
+	$code_length = 6;
+
+	if(isset($_POST['code']) && strlen($_POST['code']) == $code_length && isset($_SESSION['otp']['code_hash'])) {
+
+		$user = $app->db->queryOneRecord('SELECT otp_attempts FROM sys_user WHERE userid = ?', $_SESSION['s_pending']['user']['userid']);
+
+		//* Check if we reached limits
+		if($_SESSION['otp']['sent'] > $max_code_resend
+			|| $_SESSION['otp']['session_attempts'] > $max_session_code_retry
+			|| $user['otp_attempts'] > $max_global_code_retry
+			|| time() > $_SESSION['otp']['starttime'] + $max_time
+			) {
+			unset($_SESSION['otp']);
+			unset($_SESSION['s_pending']);
+			$app->error('2FA failed','index.php');
+		}
+
+		//* 2fa success
+		if(password_verify($_POST['code'], $_SESSION['otp']['code_hash'])) {
+			finish_2fa_success('with 2fa');
+		} else {
+			//* 2fa wrong code
+			$_SESSION['otp']['session_attempts']++;
+			$app->db->query('UPDATE `sys_user` SET otp_attempts=otp_attempts + 1 WHERE userid = ?', $_SESSION['s_pending']['user']['userid']);
+		}
+	}
+
+	// Send code via email.
+	if (!isset($_SESSION['otp']['sent']) || $_GET['action'] == 'resend') {
+
+		$mail_otp_code_retry_timeout = 30;
+		if (isset($_SESSION['otp']['starttime']) && $_SESSION['otp']['starttime'] > time() - $mail_otp_code_retry_timeout) {
+			$token_sent_message = sprintf($wb['otp_code_email_sent_wait_txt'], $mail_otp_code_retry_timeout);
+		}
+		else {
+
+			// Generate new code
+			$new_otp_code = random_int(100000, 999999);
+			$_SESSION['otp']['code_hash'] = password_hash($new_otp_code, PASSWORD_DEFAULT);
+			//$_SESSION['otp']['code_debug'] = $new_otp_code; # for DEBUG only.
+			$_SESSION['otp']['starttime'] = time();
+
+			// Ensure that code is not sent too often
+			if(isset($_SESSION['otp']['sent']) && $_SESSION['otp']['sent'] > $max_code_resend) {
+				$app->error('Code resend limit reached', 'index.php');
+			}
+
+			$app->uses('functions');
+			$app->uses('getconf');
+			$server_config_array = $app->getconf->get_global_config();
+
+			$app->uses('getconf,ispcmail');
+			$mail_config = $server_config_array['mail'];
+			if($mail_config['smtp_enabled'] == 'y') {
+				$mail_config['use_smtp'] = true;
+				$app->ispcmail->setOptions($mail_config);
+			}
+
+			$clientuser = $app->db->queryOneRecord('SELECT email FROM sys_user u LEFT JOIN client c ON (u.client_id=c.client_id) WHERE u.userid = ?', $_SESSION['s_pending']['user']['userid']);
+			if (!empty($clientuser['email'])) {
+				$email_to = $clientuser['email'];
+			}
+			else {
+				// Admin users are not related to a client, thus use the globally configured email address.
+				$email_to = $mail_config['admin_mail'];
+			}
+
+			$app->ispcmail->setSender($mail_config['admin_mail'], $mail_config['admin_name']);
+			$app->ispcmail->setSubject($wb['otp_code_email_subject_txt']);
+			$app->ispcmail->setMailText(sprintf($wb['otp_code_email_template_txt'], $new_otp_code));
+			$send_result = $app->ispcmail->send($email_to);
+			$app->ispcmail->finish();
+
+			if ($send_result) {
+
+				// Increase sent counter.
+				if(!isset($_SESSION['otp']['sent'])) {
+					$_SESSION['otp']['sent'] = 1;
+				} else {
+					$_SESSION['otp']['sent']++;
+				}
+
+				$token_sent_message = $wb['otp_code_email_sent_txt'] . ' ' . $email_to;
+			}
+			else {
+				$token_sent_message = sprintf($wb['otp_code_email_sent_failed_txt'], $email_to);
+			}
+		}
+	}
+
+	// Show form to enter email code
+	// ... below
+
+} else {
+	$app->error('Otp method unknown', 'index.php');
+}
+
+
+$logo = $app->db->queryOneRecord("SELECT * FROM sys_ini WHERE sysini_id = 1");
+if($logo['custom_logo'] != ''){
+    $base64_logo_txt = $logo['custom_logo'];
+} else {
+    $base64_logo_txt = $logo['default_logo'];
+}
+$app->tpl->setVar('base64_logo_txt', $base64_logo_txt);
+
+$app->tpl->setVar('current_theme', isset($_SESSION['s']['theme']) ? $_SESSION['s']['theme'] : 'default', true);
+if (!empty($token_sent_message)) {
+  $app->tpl->setVar('token_sent_message', $token_sent_message);
+}
+
+// Load templating system and lang file.
+$app->uses('tpl');
+$app->tpl->newTemplate('main_login.tpl.htm');
+$app->tpl->setInclude('content_tpl', 'templates/otp.htm');
+
+
+// SET csrf token.
+$csrf_token = $app->auth->csrf_token_get('otp');
+$app->tpl->setVar('_csrf_id',$csrf_token['csrf_id']);
+$app->tpl->setVar('_csrf_key',$csrf_token['csrf_key']);
+//$app->tpl->setVar('msg', print_r($_SESSION['otp'], 1)); // For DEBUG only.
+
+$app->tpl->setVar($wb);
+
+$app->tpl_defaults();
+$app->tpl->pparse();
+
+
+?>
diff --git a/interface/web/login/templates/otp.htm b/interface/web/login/templates/otp.htm
new file mode 100644
index 0000000000000000000000000000000000000000..cf49ba8108d6c7157c0458104d79c5098a40868d
--- /dev/null
+++ b/interface/web/login/templates/otp.htm
@@ -0,0 +1,25 @@
+<tmpl_if name="msg">
+<div class="alert alert-success" role="alert"><tmpl_var name="msg"></div>
+</tmpl_if>
+<tmpl_if name="error">
+<div class="alert alert-danger" role="alert"><tmpl_var name="error"></div>
+</tmpl_if>
+<h2><tmpl_var name="otp_code_txt"></h2>
+<p><tmpl_var name="otp_code_desc_txt"></p>
+<form accept-charset="UTF-8" role="form" method="post" action="otp.php">
+<fieldset>
+	<div class="form-group">
+		<input class="form-control" placeholder="{tmpl_var name='otp_code_placeholder_txt'}" name="code" id="code" type="text" autofocus>
+	</div>
+	<div class="right">
+			<input class="btn btn-default formbutton-default" type="submit" value="{tmpl_var name='login_button_txt'}">
+	</div>
+	<tmpl_if name="token_sent_message">{tmpl_var name='token_sent_message'}<br /></tmpl_if>
+	<a href="otp.php?action=resend">{tmpl_var name='otp_code_resend_txt'}</a>
+
+
+	<input type="hidden" name="_csrf_id" value="{tmpl_var name='_csrf_id'}" />
+	<input type="hidden" name="_csrf_key" value="{tmpl_var name='_csrf_key'}" />
+
+</fieldset>
+</form>
diff --git a/interface/web/tools/form/user_settings.tform.php b/interface/web/tools/form/user_settings.tform.php
index f063634b0ccf96ed6222acb33274c81b7dbd9ea2..85cdda08471d49d45f34e096cdb4119d3227f5ff 100644
--- a/interface/web/tools/form/user_settings.tform.php
+++ b/interface/web/tools/form/user_settings.tform.php
@@ -119,6 +119,10 @@ if($_SESSION["s"]["user"]["typ"] == 'admin') {
 	}
 }
 
+$otp_method_list = array(
+	'none' => 'none',
+	'email' => 'email',
+);
 //* Load themes
 $themes_list = array();
 $handle = @opendir(ISPC_THEMES_PATH);
@@ -163,6 +167,25 @@ $form['tabs']['users'] = array (
 			'rows'  => '',
 			'cols'  => ''
 		),
+		'otp_type' => array(
+			'datatype' => 'VARCHAR',
+			'formtype' => 'SELECT',
+			'validators' => array (  0 => array (    'type' => 'NOTEMPTY',
+			'errmsg'=> 'otp_auth_empty'),
+			1 => array (    'type' => 'REGEX',
+			'regex' => '/^[a-z0-9\_]{0,64}$/',
+			'errmsg'=> 'otp_auth_regex'),
+			),
+			'regex'  => '',
+			'errmsg' => '',
+			'default' => '',
+			'value'  => $otp_method_list,
+			'separator' => '',
+			'width'  => '30',
+			'maxlength' => '255',
+			'rows'  => '',
+			'cols'  => ''
+		),
 		'language' => array (
 			'datatype' => 'VARCHAR',
 			'formtype' => 'SELECT',
diff --git a/interface/web/tools/lib/lang/ar_usersettings.lng b/interface/web/tools/lib/lang/ar_usersettings.lng
index c05b94e76e77b6f2c2548b8f3dc9607ba91d228f..ac3339f448b0f861f7701f03aeb01af081ec6a2a 100644
--- a/interface/web/tools/lib/lang/ar_usersettings.lng
+++ b/interface/web/tools/lib/lang/ar_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/bg_usersettings.lng b/interface/web/tools/lib/lang/bg_usersettings.lng
index c30dcf37019f205569a250bd4addd21cf6961e6a..93cbf9441728149843566af79f2053dc0f7f507c 100644
--- a/interface/web/tools/lib/lang/bg_usersettings.lng
+++ b/interface/web/tools/lib/lang/bg_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/br_usersettings.lng b/interface/web/tools/lib/lang/br_usersettings.lng
index 8e6829a8c027b923b5d700d4f5144b039bbcf5af..a8bb10a34372a2a919e9cfb9e5bbf8b8ae541bd6 100644
--- a/interface/web/tools/lib/lang/br_usersettings.lng
+++ b/interface/web/tools/lib/lang/br_usersettings.lng
@@ -15,3 +15,4 @@ $wb['startmodule_empty'] = 'Módulo inicial está vazio.';
 $wb['startmodule_regex'] = 'Caracteres inválidos no módulo inicial.';
 $wb['app_theme_empty'] = 'Tema está vazio.';
 $wb['app_theme_regex'] = 'Caracteres inválidos no tema.';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
diff --git a/interface/web/tools/lib/lang/ca_usersettings.lng b/interface/web/tools/lib/lang/ca_usersettings.lng
index 4705660b9e1489cdd8fdcdaadd22561a4892d410..74dfb3281d8b0446ed32b29d3fc0da2091f8cb32 100644
--- a/interface/web/tools/lib/lang/ca_usersettings.lng
+++ b/interface/web/tools/lib/lang/ca_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Les mots de passe correspondent.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Page d\'accueil';
 $wb['app_theme_txt'] = 'Interface';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/cz_usersettings.lng b/interface/web/tools/lib/lang/cz_usersettings.lng
index 3805d25b3a6a144835d8a7d499717729bf8ab0ba..c3ded6184d09917be0a99248b45bf722652595e2 100644
--- a/interface/web/tools/lib/lang/cz_usersettings.lng
+++ b/interface/web/tools/lib/lang/cz_usersettings.lng
@@ -15,3 +15,4 @@ $wb['startmodule_empty'] = 'Startmodule empty.';
 $wb['startmodule_regex'] = 'Invalid chars in Startmodule.';
 $wb['app_theme_empty'] = 'App theme empty.';
 $wb['app_theme_regex'] = 'Invalid chars in App theme.';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
diff --git a/interface/web/tools/lib/lang/de_usersettings.lng b/interface/web/tools/lib/lang/de_usersettings.lng
index dd5fefffae16a756fc596ae7566cedefe27d495f..e66f2db650f617d2eabd537f70bc02afd8b32e65 100644
--- a/interface/web/tools/lib/lang/de_usersettings.lng
+++ b/interface/web/tools/lib/lang/de_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Die Passwörter stimmen überein.';
 $wb['language_txt'] = 'Sprache';
 $wb['startmodule_txt'] = 'Startmodul';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/dk_usersettings.lng b/interface/web/tools/lib/lang/dk_usersettings.lng
index 341d9db757b08e847b3d0aa5b7ece8781959764a..e413c61f1b360a3e2efe9757ddc0d172792edf89 100644
--- a/interface/web/tools/lib/lang/dk_usersettings.lng
+++ b/interface/web/tools/lib/lang/dk_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_txt'] = 'Password';
 $wb['language_txt'] = 'Sprog';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/el_usersettings.lng b/interface/web/tools/lib/lang/el_usersettings.lng
index 68ddcd657c44dac297d32edcc570ceb6bd06b10c..4158c9fc0ee1b6fe7f66eb8d85032265a302ffcd 100644
--- a/interface/web/tools/lib/lang/el_usersettings.lng
+++ b/interface/web/tools/lib/lang/el_usersettings.lng
@@ -11,4 +11,5 @@ $wb['password_mismatch_txt'] = 'The passwords do not match.';
 $wb['password_match_txt'] = 'The passwords do match.';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/en.lng b/interface/web/tools/lib/lang/en.lng
index 7794543497984f911d29364e2b6497c39ab5940d..eff8a133231bfd8974da242398f0470b45fc5dbf 100644
--- a/interface/web/tools/lib/lang/en.lng
+++ b/interface/web/tools/lib/lang/en.lng
@@ -10,4 +10,4 @@ $wb['Resync'] = 'Resync';
 $wb['Import'] = 'Import';
 $wb['ISPConfig 3 mail'] = 'ISPConfig 3 mail';
 $wb['PDNS Tupa'] = 'PowerDNS Tupa';
-?>
\ No newline at end of file
+?>
diff --git a/interface/web/tools/lib/lang/en_usersettings.lng b/interface/web/tools/lib/lang/en_usersettings.lng
index 421805e7896ebe54e961752415dd7cfa46244179..458eef7606042fe4019e35292cdfbb75d056a529 100644
--- a/interface/web/tools/lib/lang/en_usersettings.lng
+++ b/interface/web/tools/lib/lang/en_usersettings.lng
@@ -16,4 +16,5 @@ $wb['startmodule_empty'] = 'Startmodule empty.';
 $wb['startmodule_regex'] = 'Invalid chars in Startmodule.';
 $wb['app_theme_empty'] = 'App theme empty.';
 $wb['app_theme_regex'] = 'Invalid chars in App theme.';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/es_usersettings.lng b/interface/web/tools/lib/lang/es_usersettings.lng
index 43a181a11c7337042c0faada5357b30b53aae228..b3a6390948a432c9d6023b85f9f75854e4f31d80 100644
--- a/interface/web/tools/lib/lang/es_usersettings.lng
+++ b/interface/web/tools/lib/lang/es_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Las contraseñas coinciden.';
 $wb['language_txt'] = 'Idioma';
 $wb['startmodule_txt'] = 'Módulo de inicio';
 $wb['app_theme_txt'] = 'Diseño';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/fi_usersettings.lng b/interface/web/tools/lib/lang/fi_usersettings.lng
index 91d71739537e6d663f4a6e9fc043ab5f73af5fed..888b1c7eb64d46a4dc3e961ee38378bda61eda77 100644
--- a/interface/web/tools/lib/lang/fi_usersettings.lng
+++ b/interface/web/tools/lib/lang/fi_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/fr_usersettings.lng b/interface/web/tools/lib/lang/fr_usersettings.lng
index b398e76e352ccb6b168473b29f74bcbf6c5b932f..57f47dc9fbb82bc1c211dc4fb92cf16c1b670d19 100644
--- a/interface/web/tools/lib/lang/fr_usersettings.lng
+++ b/interface/web/tools/lib/lang/fr_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Les mots de passe correspondent.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/hr_usersettings.lng b/interface/web/tools/lib/lang/hr_usersettings.lng
index b707cce4da55bf35f0c771408c92f95fd0464457..cf206223395f8b6c4016cbc14a8e1101b0efcec2 100644
--- a/interface/web/tools/lib/lang/hr_usersettings.lng
+++ b/interface/web/tools/lib/lang/hr_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Šifre su identične.';
 $wb['language_txt'] = 'Jezik';
 $wb['startmodule_txt'] = 'Početna stranica';
 $wb['app_theme_txt'] = 'Tema';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/hu_usersettings.lng b/interface/web/tools/lib/lang/hu_usersettings.lng
index 2a89acf5b5f926929859300cc3e573566b55fc1c..35bb4a60dee5ebb226d8b75c1e02d341119e66be 100644
--- a/interface/web/tools/lib/lang/hu_usersettings.lng
+++ b/interface/web/tools/lib/lang/hu_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/id_usersettings.lng b/interface/web/tools/lib/lang/id_usersettings.lng
index 760cfe75f244d893433fc863f683093816a11c00..1a63a8b41c399aefdd28cc5c193df9a711a34278 100644
--- a/interface/web/tools/lib/lang/id_usersettings.lng
+++ b/interface/web/tools/lib/lang/id_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/it_usersettings.lng b/interface/web/tools/lib/lang/it_usersettings.lng
index 0f1cefe1bf3411eeb539324b6927dd3ac96dfe26..e291041b1c61cbaea57ccf2c7f049f7dc6fef266 100644
--- a/interface/web/tools/lib/lang/it_usersettings.lng
+++ b/interface/web/tools/lib/lang/it_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Le password coincidono.';
 $wb['language_txt'] = 'Lingua pannello';
 $wb['startmodule_txt'] = 'Modulo di avvio';
 $wb['app_theme_txt'] = 'Apparenza';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/ja_usersettings.lng b/interface/web/tools/lib/lang/ja_usersettings.lng
index 291aa94537208f694c4b6e49eabe059d7ac8f085..d5ba174fde29f7690d6e6c3a8c842822d5bc07fa 100644
--- a/interface/web/tools/lib/lang/ja_usersettings.lng
+++ b/interface/web/tools/lib/lang/ja_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/nl_usersettings.lng b/interface/web/tools/lib/lang/nl_usersettings.lng
index 909df9dbecf4bcadca6266477ec37b276581f1ee..cb31a68d209977df2ee0ab01c6ae354562738ebe 100644
--- a/interface/web/tools/lib/lang/nl_usersettings.lng
+++ b/interface/web/tools/lib/lang/nl_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Taal';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Twee-factor Authenticatie';
 ?>
diff --git a/interface/web/tools/lib/lang/pl_usersettings.lng b/interface/web/tools/lib/lang/pl_usersettings.lng
index 64396df2409dbb08ea741cd5d7ea7551ce5769c1..1bb9a69ae7525a61a7bea81e02c91331cc0551dc 100644
--- a/interface/web/tools/lib/lang/pl_usersettings.lng
+++ b/interface/web/tools/lib/lang/pl_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Hasła się zgadzają';
 $wb['language_txt'] = 'Język';
 $wb['startmodule_txt'] = 'Moduł startowy';
 $wb['app_theme_txt'] = 'Temat';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/pt_usersettings.lng b/interface/web/tools/lib/lang/pt_usersettings.lng
index 4925a3f7286da3e7a1fc9c74205d7dcd4217ccfe..b5ca31359e3cfc3e9b6443be984572b68973855e 100644
--- a/interface/web/tools/lib/lang/pt_usersettings.lng
+++ b/interface/web/tools/lib/lang/pt_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/ro_usersettings.lng b/interface/web/tools/lib/lang/ro_usersettings.lng
index 8c43550f33cc56729cd5902089e60d21b5732e25..7c826d923b62e6958ee82b9cb178561dfcff642f 100644
--- a/interface/web/tools/lib/lang/ro_usersettings.lng
+++ b/interface/web/tools/lib/lang/ro_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/ru_usersettings.lng b/interface/web/tools/lib/lang/ru_usersettings.lng
index ffa3f3bf03116cf1d8b3cd4b97c38114e4762b28..d8e091e55364e47b9e2528a231f34855f41d8bcb 100644
--- a/interface/web/tools/lib/lang/ru_usersettings.lng
+++ b/interface/web/tools/lib/lang/ru_usersettings.lng
@@ -14,4 +14,5 @@ $wb['interface_desc_txt'] = 'Измените свой интерфейс';
 $wb['language_txt'] = 'Язык';
 $wb['startmodule_txt'] = 'Стартовый модуль';
 $wb['app_theme_txt'] = 'Тема';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/se_usersettings.lng b/interface/web/tools/lib/lang/se_usersettings.lng
index f6de2dc4eebe227af1be4cfe0e5de3bd5ee7eff1..9680936d7bee1af79c3f846b9d90b66a7156e368 100644
--- a/interface/web/tools/lib/lang/se_usersettings.lng
+++ b/interface/web/tools/lib/lang/se_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Lösenorden matchar';
 $wb['language_txt'] = 'Språk';
 $wb['startmodule_txt'] = 'Startmodul';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/sk_usersettings.lng b/interface/web/tools/lib/lang/sk_usersettings.lng
index e00f5e664b1bb4bcb71a6614446dec2530c1aa21..ee1ff41888281bcecd049c124a68679526cced82 100644
--- a/interface/web/tools/lib/lang/sk_usersettings.lng
+++ b/interface/web/tools/lib/lang/sk_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'The passwords do match.';
 $wb['language_txt'] = 'Language';
 $wb['startmodule_txt'] = 'Startmodule';
 $wb['app_theme_txt'] = 'Design';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/lib/lang/tr_usersettings.lng b/interface/web/tools/lib/lang/tr_usersettings.lng
index a1fd7c56d4cddc2fd47065819f46c37917369bfb..81ea3ed5f6ed1a851a34b1449c395a3fa96a3a25 100644
--- a/interface/web/tools/lib/lang/tr_usersettings.lng
+++ b/interface/web/tools/lib/lang/tr_usersettings.lng
@@ -12,4 +12,5 @@ $wb['password_match_txt'] = 'Parola ile onayı aynı.';
 $wb['language_txt'] = 'Dil';
 $wb['startmodule_txt'] = 'Başlangıç modülü';
 $wb['app_theme_txt'] = 'Tasarım';
+$wb['otp_auth_txt'] = 'Two Factor Authentication';
 ?>
diff --git a/interface/web/tools/templates/user_settings.htm b/interface/web/tools/templates/user_settings.htm
index a620f419c54d58ed5ad4e3b044c8410b10d46160..f430b64195d29cb56cb3a52e74355101b0db64c5 100644
--- a/interface/web/tools/templates/user_settings.htm
+++ b/interface/web/tools/templates/user_settings.htm
@@ -26,6 +26,14 @@
     <div id="confirmpasswordOK" style="display:none;" class="confirmpasswordok">{tmpl_var name='password_match_txt'}</div>
   </div>
 </div>
+<div class="form-group">
+  <label for="2fa" class="col-sm-3 control-label">{tmpl_var name='otp_auth_txt'}</label>
+  <div class="col-sm-9">
+    <select name="otp_type" id="otp_type" class="form-control">
+      {tmpl_var name='otp_type'}
+    </select>
+  </div>
+</div>
 <div class="form-group">
   <label for="language" class="col-sm-3 control-label">{tmpl_var name='language_txt'}</label>
   <div class="col-sm-9"><select name="language" id="language" class="form-control flags">