diff --git a/html/secondfactor.php b/html/secondfactor.php
new file mode 100644
index 0000000000000000000000000000000000000000..5b6f833d6a59800fe4387e19cf0dd6d0244183dd
--- /dev/null
+++ b/html/secondfactor.php
@@ -0,0 +1,103 @@
+<?php
+/*
+  This code is part of FusionDirectory (http://www.fusiondirectory.org/)
+  Copyright (C) 2003-2010  Cajus Pollmeier
+  Copyright (C) 2011-2019  FusionDirectory
+
+  This program is free software; you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation; either version 2 of the License, or
+  (at your option) any later version.
+
+  This program is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program; if not, write to the Free Software
+  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
+*/
+
+/* Basic setup, remove eventually registered sessions */
+require_once("../include/php_setup.inc");
+require_once("functions.inc");
+require_once("variables.inc");
+
+/* Set headers */
+header('Content-type: text/html; charset=UTF-8');
+header('X-XSS-Protection: 1; mode=block');
+header('X-Content-Type-Options: nosniff');
+header('X-Frame-Options: deny');
+
+/* Set the text domain as 'fusiondirectory' */
+$domain = 'fusiondirectory';
+bindtextdomain($domain, LOCALE_DIR);
+textdomain($domain);
+
+/* Remember everything we did after the last click */
+session::start();
+session::set('DEBUGLEVEL', 0);
+reset_errors();
+
+/* Force SSL for second factor */
+if ($ssl != '') {
+  header("Location: $ssl");
+  exit;
+}
+
+CSRFProtection::check();
+
+/* Logged in? Redirect to FD */
+if (session::is_set('connected')) {
+  header('Location: main.php');
+  exit;
+}
+
+/* Missing data? Redirect to login */
+if (!session::is_set('ui') || !session::is_set('config')) {
+  header('Location: index.php');
+  exit;
+}
+
+$ui     = session::get('ui');
+$config = session::get('config');
+
+timezone::setDefaultTimezoneFromConfig();
+
+/* Check for invalid sessions */
+if (session::get('_LAST_PAGE_REQUEST') != '') {
+  /* check FusionDirectory.conf for defined session lifetime */
+  $max_life = $config->get_cfg_value('sessionLifetime', 60 * 60 * 2);
+
+  if ($max_life > 0) {
+    /* get time difference between last page reload */
+    $request_time = (time() - session::get('_LAST_PAGE_REQUEST'));
+
+    /* If page wasn't reloaded for more than max_life seconds
+     * kill session
+     */
+    if ($request_time > $max_life) {
+      session::destroy('main.php called with expired session');
+      header('Location: index.php?signout=1&message=expired');
+      exit;
+    }
+  }
+}
+session::set('_LAST_PAGE_REQUEST', time());
+
+foreach (LoginPost::$secondFactorMethods as $secondFactorMethod) {
+  if (!class_available($secondFactorMethod)) {
+    continue;
+  }
+  $secondFactorMethod::earlyProcess();
+}
+
+session::set('DEBUGLEVEL', $config->get_cfg_value('DEBUGLEVEL'));
+
+/* Set template compile directory */
+$smarty->compile_dir = $config->get_cfg_value('templateCompileDirectory', SPOOL_DIR);
+
+Language::init();
+
+LoginPost::displaySecondFactorPage();
diff --git a/ihtml/themes/breezy/secondfactor.tpl b/ihtml/themes/breezy/secondfactor.tpl
new file mode 100644
index 0000000000000000000000000000000000000000..8dac1d49fa3c3696d1a3706463a4cc136acc9ecf
--- /dev/null
+++ b/ihtml/themes/breezy/secondfactor.tpl
@@ -0,0 +1,48 @@
+<body>
+
+  {$php_errors}
+
+{* FusionDirectory login - smarty template *}
+
+<div id="window-container">
+
+<div id="window-div">
+<form action="index.php" method="post" id="loginform" name="loginform">
+
+{$msg_dialogs}
+  <div id="window-titlebar">
+    <img id="fd-logo" src="geticon.php?context=applications&amp;icon=fusiondirectory&amp;size=48" alt="FusionDirectory logo"/>
+    <p>
+      {t}Two factor authentication{/t}
+    </p>
+  </div>
+  <div id="window-content">
+    {foreach from=$methodOutputs key=method item=methodOutput}
+      <div class="secondfactormethod" id="{$method|escape}">
+        {$methodOutput}
+      </div>
+    {/foreach}
+  </div>
+  <div id="window-footer" class="plugbottom">
+    <div>
+      <!-- Display error message on demand -->
+      {$message}
+    </div>
+    <div>
+    </div>
+  </div>
+
+</form>
+</div>
+
+</div>
+
+{include file={filePath file="copynotice.tpl"}}
+
+<script type="text/javascript">
+<!--
+  next_msg_dialog();
+-->
+</script>
+</body>
+</html>
diff --git a/include/login/class_LoginMethod.inc b/include/login/class_LoginMethod.inc
index ac07f9d3ee61fb63b7f570e4795e3b8cfc7f9d66..9f4f83bdd1f4502dc01f7f0e01cd59d93e4d476d 100644
--- a/include/login/class_LoginMethod.inc
+++ b/include/login/class_LoginMethod.inc
@@ -145,15 +145,23 @@ class LoginMethod
     return TRUE;
   }
 
-  /*! \brief Final step of successful login: redirect to main.php */
-  static function redirect ()
+  /*! \brief Connect user */
+  static function connect ()
   {
     global $config, $ui;
 
+    $ui = session::get('ui');
+
     /* Not account expired or password forced change go to main page */
-    logging::log('security', 'login', static::$username, [], 'Logged in successfully');
+    logging::log('security', 'login', $ui->uid, [], 'Logged in successfully');
     session::set('connected', 1);
     session::set('DEBUGLEVEL', $config->get_cfg_value('DEBUGLEVEL'));
+  }
+
+  /*! \brief Final step of successful login: redirect to main.php */
+  static function redirect ()
+  {
+    static::connect();
     header('Location: main.php');
     exit;
   }
diff --git a/include/login/class_LoginPost.inc b/include/login/class_LoginPost.inc
index 37465f574faef08b604326dedd732031686b1c43..c68f5ddbbcaea41ee99ec0b242824893a244f292 100644
--- a/include/login/class_LoginPost.inc
+++ b/include/login/class_LoginPost.inc
@@ -23,12 +23,28 @@
  */
 class LoginPost extends LoginMethod
 {
+  /*! \brief List of second factor methods, may be dynamic later */
+  static $secondFactorMethods = ['SecondFactorWebAuthn'];
+
   /*! \brief Displayed name */
   static function getLabel ()
   {
     return _('HTML form');
   }
 
+  static function init ()
+  {
+    parent::init();
+
+    /* Init second factor methods if needed */
+    foreach (static::$secondFactorMethods as $secondFactorMethod) {
+      if (!class_available($secondFactorMethod)) {
+        continue;
+      }
+      $secondFactorMethod::init();
+    }
+  }
+
   /*! \brief All login steps in the right order for standard POST login */
   static function loginProcess ()
   {
@@ -50,6 +66,16 @@ class LoginPost extends LoginMethod
         'runSchemaCheck',
       ]);
 
+      /* If needed redirect to second factor page */
+      foreach (static::$secondFactorMethods as $secondFactorMethod) {
+        if (!class_available($secondFactorMethod)) {
+          continue;
+        }
+        if ($secondFactorMethod::hasSecondFactor()) {
+          static::redirectSecondFactorPage();
+        }
+      }
+
       if ($success) {
         /* Everything went well, redirect to main.php */
         static::redirect();
@@ -62,6 +88,14 @@ class LoginPost extends LoginMethod
     static::displayLogin();
   }
 
+  /*! \brief Redirect to the second factor page */
+  static protected function redirectSecondFactorPage ()
+  {
+    session::un_set('connected');
+    header('Location: secondfactor.php');
+    exit;
+  }
+
   /*! \brief Display the login page and exit() */
   static protected function displayLogin ()
   {
@@ -134,4 +168,66 @@ class LoginPost extends LoginMethod
     $smarty->display(get_template_path('login.tpl'));
     exit();
   }
+
+  /*! \brief Display the second factor page and exit() */
+  static function displaySecondFactorPage ()
+  {
+    global $smarty,$message,$config,$ssl,$error_collector,$error_collector_mailto;
+
+    $lang = session::get('lang');
+
+    error_reporting(E_ALL | E_STRICT);
+
+    /* Fill template with required values */
+    $username = '';
+    if (isset($_POST['username'])) {
+      $username = trim($_POST['username']);
+    }
+    $smarty->assign('date',       gmdate('D, d M Y H:i:s'));
+    $smarty->assign('username',   $username);
+    $smarty->assign('revision',   FD_VERSION);
+    $smarty->assign('year',       date('Y'));
+    $smarty->append('css_files',  get_template_path('login.css'));
+    $smarty->assign('title',      _('Second factor'));
+
+    /* Some error to display? */
+    if (!isset($message)) {
+      $message = '';
+    }
+    $smarty->assign('message', $message);
+
+    /* show login screen */
+    $smarty->assign('PHPSESSID', session_id());
+    if ($error_collector != '') {
+      $smarty->assign('php_errors', preg_replace('/%BUGBODY%/', $error_collector_mailto, $error_collector).'</div>');
+    } else {
+      $smarty->assign('php_errors', '');
+    }
+    $smarty->assign('msg_dialogs',  msg_dialog::get_dialogs());
+    $smarty->assign('usePrototype', 'false');
+    $smarty->assign('date',         date('l, dS F Y H:i:s O'));
+    $smarty->assign('lang',         preg_replace('/_.*$/', '', $lang));
+    $smarty->assign('rtl',          Language::isRTL($lang));
+
+    $methodOutputs = [];
+
+    /* Run second factor methods */
+    foreach (static::$secondFactorMethods as $secondFactorMethod) {
+      if (!class_available($secondFactorMethod)) {
+        continue;
+      }
+      $methodOutput = $secondFactorMethod::execute();
+      if ($methodOutput !== NULL) {
+        $methodOutputs[$secondFactorMethod] = $methodOutput;
+      }
+    }
+
+    $smarty->assign('methodOutputs', $methodOutputs);
+
+    $smarty->display(get_template_path('headers.tpl'));
+    $smarty->assign('version', FD_VERSION);
+
+    $smarty->display(get_template_path('secondfactor.tpl'));
+    exit();
+  }
 }