Commit 1331e7c0 authored by Côme Chilliet's avatar Côme Chilliet

feat(webauthn) Adapted to core changes to make second factor generic

issue #6013
parent d8dbb849
<?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());
LoginWebAuthnPost::processWebAuthnJavascriptRequests();
session::set('DEBUGLEVEL', $config->get_cfg_value('DEBUGLEVEL'));
/* Set template compile directory */
$smarty->compile_dir = $config->get_cfg_value('templateCompileDirectory', SPOOL_DIR);
Language::init();
LoginWebAuthnPost::displaySecondFactorPage();
<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">
<div class="optional"><br />
{if $ssl}<span class="warning">{$ssl}</span>{/if}
</div>
<div>
{t}Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.{/t}
</div>
</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();
webauthnCheckRegistration();
-->
</script>
</body>
</html>
......@@ -20,95 +20,28 @@
require_once('WebAuthn/WebAuthn.php');
/*!
* \brief Login via POST + 2nd factor
*/
class LoginWebAuthnPost extends LoginPost
class SecondFactorWebAuthn
{
/*! \brief Displayed name */
static function getLabel ()
static function init ()
{
return _('HTML form and 2nd factor');
}
static function processWebAuthnJavascriptRequests ()
{
if (isset($_GET['webauthn'])) {
try {
switch ($_GET['webauthn']) {
case 'getGetArgs':
print(json_encode(static::getGetArgs()));
break;
case 'processGet':
print(json_encode(static::processGet()));
break;
default:
throw new FusionDirectoryException('Unknown operation '.$_GET['webauthn']);
}
} catch (Throwable $ex) {
session::un_set('challenge');
$return = new stdClass();
$return->success = FALSE;
$return->msg = $ex->getMessage();
print(json_encode($return));
}
exit();
}
}
/*! \brief All login steps in the right order for standard POST login */
static function loginProcess ()
{
global $smarty, $config, $message;
static::processWebAuthnJavascriptRequests();
static::init();
session::un_set('WebauthnChallenge');
session::un_set('fdWebauthnRegistrations');
$smarty->assign('focusfield', 'username');
if (($_SERVER['REQUEST_METHOD'] == 'POST') && isset($_POST['login']) && isset($_POST['username']) && isset($_POST['password'])) {
static::$username = $_POST['username'];
static::$password = $_POST['password'];
$success = static::runSteps([
'validateUserInput',
'checkForLockingBranch',
'ldapLoginUser',
'loginAndCheckExpired',
'runSchemaCheck',
'secondFactorAuth',
]);
if ($success) {
static::redirectSecondFactorPage();
}
}
/* Translation of cookie-warning. Whether to display it, is determined by JavaScript */
$smarty->assign('cookies', '<b>'._('Warning').':</b> '._('Your browser has cookies disabled. Please enable cookies and reload this page before logging in!'));
static::displayLogin();
}
/*! \brief Called after successful login, return FALSE if account is expired */
static function secondFactorAuth ()
/*! \brief Checks if connected user has second factor active */
static function hasSecondFactor (): bool
{
global $ui, $config, $plist, $message, $smarty;
global $ui, $config;
$ldap = $config->get_ldap_link();
$ldap->cat($ui->dn, ['fdWebauthnRegistrations']);
$attrs = $ldap->fetch();
if (!$attrs) {
$message = _('Could not fetch user');
return FALSE;
throw new FusionDirectoryException(_('Could not fetch user'));
}
unset($attrs['fdWebauthnRegistrations']['count']);
if (empty($attrs['fdWebauthnRegistrations'])) {
$message = _('2nd factor information missing');
return FALSE;
}
......@@ -117,68 +50,30 @@ class LoginWebAuthnPost extends LoginPost
return TRUE;
}
/*! \brief Redirect to the second factor page */
static protected function redirectSecondFactorPage ()
/*! \brief Process WebAuthn Javascript Requests */
static function earlyProcess ()
{
session::un_set('connected');
header('Location: secondfactor.php');
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);
/* Display SSL mode warning? */
if (($ssl != '') && ($config->get_cfg_value('warnSSL') == 'TRUE')) {
$smarty->assign('ssl', sprintf(_('Warning: <a href="%s">Session is not encrypted!</a>'), $ssl));
} else {
$smarty->assign('ssl', '');
}
/* 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', '');
if (isset($_GET['webauthn'])) {
try {
switch ($_GET['webauthn']) {
case 'getGetArgs':
print(json_encode(static::getGetArgs()));
break;
case 'processGet':
print(json_encode(static::processGet()));
break;
default:
throw new FusionDirectoryException('Unknown operation '.$_GET['webauthn']);
}
} catch (Throwable $ex) {
session::un_set('WebauthnChallenge');
$return = new stdClass();
$return->success = FALSE;
$return->msg = $ex->getMessage();
print(json_encode($return));
}
exit();
}
$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));
$smarty->append('js_files', 'include/webauthn.js');
$smarty->display(get_template_path('headers.tpl'));
$smarty->assign('version', FD_VERSION);
$smarty->display(get_template_path('secondfactor.tpl'));
exit();
}
static protected function getGetArgs ()
......@@ -197,7 +92,7 @@ class LoginWebAuthnPost extends LoginPost
$getArgs = $WebAuthn->getGetArgs($ids);
/* Serializing avoids failed autoload of WebAuthn classes before our require_once (at session start) */
session::set('challenge', serialize($WebAuthn->getChallenge()));
session::set('WebauthnChallenge', serialize($WebAuthn->getChallenge()));
return $getArgs;
}
......@@ -216,7 +111,7 @@ class LoginWebAuthnPost extends LoginPost
$authenticatorData = base64_decode($post->authenticatorData);
$signature = base64_decode($post->signature);
$id = base64_decode($post->id);
$challenge = unserialize(session::get('challenge'));
$challenge = unserialize(session::get('WebauthnChallenge'));
$credentialPublicKey = NULL;
$fdWebauthnRegistrations = session::get('fdWebauthnRegistrations');
......@@ -236,7 +131,7 @@ class LoginWebAuthnPost extends LoginPost
// process the get request. throws WebAuthnException if it fails
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge);
session::un_set('challenge');
session::un_set('WebauthnChallenge');
static::connect();
......@@ -245,17 +140,32 @@ class LoginWebAuthnPost extends LoginPost
return $return;
}
/* Same as redirect without redirection */
static function connect ()
static public function execute ()
{
global $config, $ui;
global $smarty;
if (!session::is_set('fdWebauthnRegistrations')) {
return NULL;
}
$smarty->append('js_files', 'include/webauthn.js');
$ui = session::get('ui');
$message = _('Trying to communicate with your device. Plug it in (if you haven\'t already) and press the button on the device now.');
logging::log('security', 'login', $ui->uid, [], 'Logged in successfully');
session::set('connected', 1);
session::set('DEBUGLEVEL', $config->get_cfg_value('DEBUGLEVEL'));
session::un_set('challenge');
return htmlentities($message, ENT_COMPAT, 'UTF-8').
'<script type="text/javascript">'."\n".
'<!-- '."\n".
'webauthnCheckRegistration();'."\n".
'-->'."\n".
'</script>'."\n".
'<noscript>'._('Javascript is needed for WebAuthn second factor, please enable it for this page.').'</noscript>';
}
/* Same as redirect without redirection */
static function connect ()
{
LoginMethod::connect();
session::un_set('WebauthnChallenge');
session::un_set('fdWebauthnRegistrations');
}
}
......@@ -72,7 +72,7 @@ class webauthnAccount extends simplePlugin
throw new FusionDirectoryException('Unknown operation '.$_GET['webauthn']);
}
} catch (Throwable $ex) {
session::un_set('challenge');
session::un_set('WebauthnChallenge');
$return = new stdClass();
$return->success = FALSE;
$return->msg = $ex->getMessage();
......@@ -113,7 +113,7 @@ class webauthnAccount extends simplePlugin
$createArgs = $WebAuthn->getCreateArgs($ui->dn, $ui->uid, $ui->cn, 20, FALSE);
session::set('challenge', $WebAuthn->getChallenge());
session::set('WebauthnChallenge', $WebAuthn->getChallenge());
return $createArgs;
}
......@@ -128,7 +128,7 @@ class webauthnAccount extends simplePlugin
}
$clientDataJSON = base64_decode($post->clientDataJSON);
$attestationObject = base64_decode($post->attestationObject);
$challenge = session::get('challenge');
$challenge = session::get('WebauthnChallenge');
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge);
......@@ -138,7 +138,7 @@ class webauthnAccount extends simplePlugin
$return->success = TRUE;
$return->msg = 'Registration Success! ' . count($this->fdWebauthnRegistrations) . ' registrations.';
session::un_set('challenge');
session::un_set('WebauthnChallenge');
return $return;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment