Commit 5dbb35cc authored by Côme Chilliet's avatar Côme Chilliet

feat(webauthn) Enable sign in with 2 factor auth

First working POC.

issue #6013
parent 156fae3d
......@@ -58,62 +58,59 @@ function webauthnNewRegistration()
* checks a FIDO2 registration
* @returns {undefined}
*/
function checkregistration()
function webauthnCheckRegistration()
{
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
window.alert('Browser not supported.');
return;
window.alert('Browser not supported.');
return;
}
// get default args
window.fetch('server.php?fn=getGetArgs' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) {
window.fetch(window.location.href + '?webauthn=getGetArgs', {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
// convert base64 to arraybuffer
}).then(function(json) {
// convert base64 to arraybuffer
if (json.success === false) {
// error handling
if (json.success === false) {
throw new Error(json.msg);
}
// replace binary base64 data with ArrayBuffer. a other way to do this
// is the reviver function of JSON.parse()
recursiveBase64StrToArrayBuffer(json);
return json;
throw new Error(json.msg);
}
// create credentials
// replace binary base64 data with ArrayBuffer. a other way to do this
// is the reviver function of JSON.parse()
recursiveBase64StrToArrayBuffer(json);
return json;
}).then(function(getCredentialArgs) {
return navigator.credentials.get(getCredentialArgs);
// create credentials
return navigator.credentials.get(getCredentialArgs);
// convert to base64
}).then(function(cred) {
return {
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
};
// transfer to server
// convert to base64
return {
id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null,
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null,
signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null
};
}).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
return window.fetch('server.php?fn=processGet' + getGetParams(), {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
// transfer to server
return window.fetch(window.location.href + '?webauthn=processGet', {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
// convert to json
}).then(function(response) {
return response.json();
// analyze response
// convert to json
return response.json();
}).then(function(json) {
if (json.success) {
window.alert(json.msg || 'login success');
} else {
throw new Error(json.msg);
}
// analyze response
if (json.success) {
document.location = 'main.php';
} else {
throw new Error(json.msg);
}
// catch errors
}).catch(function(err) {
window.alert(err.message || 'unknown error occured');
// catch errors
window.alert(err.message || 'unknown error occured');
});
}
......
<?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();
reset_errors();
CSRFProtection::check();
$ui = session::get('ui');
$config = session::get('config');
/* If SSL is forced, just forward to the SSL enabled site */
if (($config->get_cfg_value('forcessl') == 'TRUE') && ($ssl != '')) {
header("Location: $ssl");
exit;
}
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());
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::processWebAuthnJavascriptRequests();
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">
<!--
focus_field("{$focusfield}");
next_msg_dialog();
webauthnCheckRegistration();
-->
</script>
</body>
</html>
<?php
/*
This code is part of FusionDirectory (http://www.fusiondirectory.org/)
Copyright (C) 2018-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.
*/
require_once('WebAuthn/WebAuthn.php');
/*!
* \brief Login via POST + 2nd factor
*/
class LoginWebAuthnPost extends LoginPost
{
/*! \brief Displayed name */
static function getLabel ()
{
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();
  • Remove this commented out code. 📘

Please register or sign in to reply
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();
$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 ()
{
global $ui, $config, $plist, $message, $smarty;
$ldap = $config->get_ldap_link();
$ldap->cat($ui->dn, ['fdWebauthnRegistrations']);
$attrs = $ldap->fetch();
if (!$attrs) {
$message = _('Could not fetch user');
return FALSE;
}
unset($attrs['fdWebauthnRegistrations']['count']);
if (empty($attrs['fdWebauthnRegistrations'])) {
$message = _('2nd factor information missing');
return FALSE;
}
session::set('fdWebauthnRegistrations', $attrs['fdWebauthnRegistrations']);
return TRUE;
}
/*! \brief Display the login page and exit() */
static protected function redirectSecondFactorPage ()
{
session::un_set('connected');
header('Location: secondfactor.php');
exit;
}
/*! \brief Display the login 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', '');
}
$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 initWebAuthnObject ()
{
// Formats
$formats = [];
//~ if ($_GET['fmt_android-key']) {
  • Remove this commented out code. 📘

Please register or sign in to reply
$formats[] = 'android-key';
//~ }
  • Remove this commented out code. 📘

Please register or sign in to reply
//~ if ($_GET['fmt_android-safetynet']) {
$formats[] = 'android-safetynet';
//~ }
  • Remove this commented out code. 📘

Please register or sign in to reply
//~ if ($_GET['fmt_fido-u2f']) {
$formats[] = 'fido-u2f';
//~ }
  • Remove this commented out code. 📘

Please register or sign in to reply
//~ if ($_GET['fmt_none']) {
$formats[] = 'none';
//~ }
  • Remove this commented out code. 📘

Please register or sign in to reply
//~ if ($_GET['fmt_packed']) {
$formats[] = 'packed';
//~ }
  • Remove this commented out code. 📘

Please register or sign in to reply
//~ if ($_GET['fmt_tpm']) {
//~ $formats[] = 'tpm';
//~ }
// new Instance of the server library.
// make sure that $rpId is the domain name.
if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) {
$host = $_SERVER['HTTP_X_FORWARDED_HOST'];
} else {
$host = $_SERVER['SERVER_NAME'];
}
$WebAuthn = new \WebAuthn\WebAuthn('FusionDirectory', $host, $formats);
// add root certificates to validate new registrations
/*if ($_GET['solo']) {
  • Remove this commented out code. 📘

Please register or sign in to reply
$WebAuthn->addRootCertificates('rootCertificates/solo.pem');
}
if ($_GET['yubico']) {
$WebAuthn->addRootCertificates('rootCertificates/yubico.pem');
}
if ($_GET['hypersecu']) {
$WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem');
}
if ($_GET['google']) {
$WebAuthn->addRootCertificates('rootCertificates/globalSign.pem');
$WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem');
}*/
return $WebAuthn;
}
static protected function getGetArgs ()
{
global $ui;
$WebAuthn = static::initWebAuthnObject();
$ids = [];
$fdWebauthnRegistrations = session::get('fdWebauthnRegistrations');
foreach ($fdWebauthnRegistrations as $fdWebauthnRegistration) {
$reg = webauthnAccount::unserializeRegistration($fdWebauthnRegistration);
$ids[] = $reg['credentialId'];
}
$getArgs = $WebAuthn->getGetArgs($ids);
session::set('challenge', serialize($WebAuthn->getChallenge()));
return $getArgs;
}
static protected function processGet ()
{
global $config;
$WebAuthn = static::initWebAuthnObject();
$post = trim(file_get_contents('php://input'));
if ($post) {
$post = json_decode($post);
}
$clientDataJSON = base64_decode($post->clientDataJSON);
$authenticatorData = base64_decode($post->authenticatorData);
$signature = base64_decode($post->signature);
$id = base64_decode($post->id);
$challenge = unserialize(session::get('challenge'));
$credentialPublicKey = NULL;
$fdWebauthnRegistrations = session::get('fdWebauthnRegistrations');
foreach ($fdWebauthnRegistrations as $fdWebauthnRegistration) {
$reg = webauthnAccount::unserializeRegistration($fdWebauthnRegistration);
if ($reg['credentialId'] === $id) {
$credentialPublicKey = $reg['credentialPublicKey'];
break;
}
}
if ($credentialPublicKey === NULL) {
throw new Exception('Public Key for credential ID not found!');
  • Define and throw a dedicated exception instead of using a generic one. 📘

Please register or sign in to reply
}
// process the get request. throws WebAuthnException if it fails
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge);
session::un_set('challenge');
static::connect();
$return = new stdClass();
$return->success = TRUE;
return $return;
}
/* Same as redirect without redirection */
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', $ui->uid, [], 'Logged in successfully');
session::set('connected', 1);
session::set('DEBUGLEVEL', $config->get_cfg_value('DEBUGLEVEL'));
}
}
......@@ -66,7 +66,6 @@ class webauthnAccount extends simplePlugin
print(json_encode($this->getCreateArgs()));
break;
case 'processCreate':
//~ TODO : corriger Unused bytes after data item.
print(json_encode($this->processCreate()));
break;
default:
......@@ -75,7 +74,7 @@ class webauthnAccount extends simplePlugin
} catch (Throwable $ex) {
$return = new stdClass();
$return->success = FALSE;
$return->msg = "$ex";//->getMessage();
$return->msg = $ex->getMessage();
print(json_encode($return));
}
exit();
......@@ -164,6 +163,19 @@ class webauthnAccount extends simplePlugin
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge);
$this->attributesAccess['fdWebauthnRegistrations']->addRegistration(static::serializeRegistration($data));
$return = new stdClass();
$return->success = TRUE;
$return->msg = 'Registration Success! ' . count($this->fdWebauthnRegistrations) . ' registrations.';
session::un_set('challenge');
return $return;
}
static public function serializeRegistration (object $data): string
{
$safeData = [];
foreach (['rpId','credentialPublicKey','certificateChain','certificate','certificateIssuer','certificateSubject','signatureCounter'] as $stringField) {
$safeData[$stringField] = $data->$stringField;
......@@ -173,14 +185,16 @@ class webauthnAccount extends simplePlugin
$safeData[$binField] = base64_encode($data->$binField);
}
$this->attributesAccess['fdWebauthnRegistrations']->addRegistration(json_encode($safeData));
$return = new stdClass();
$return->success = TRUE;
$return->msg = 'Registration Success! ' . count($this->fdWebauthnRegistrations) . ' registrations.';
return json_encode($safeData);
}
session::un_set('challenge');
static public function unserializeRegistration (string $json): array
{
$data = json_decode($json, TRUE);
foreach (['credentialId','AAGUID'] as $binField) {
$data[$binField] = base64_decode($data[$binField]);
}
return $return;
return $data;
}
}
  • SonarQube analysis reported 18 issues

    • 18 major

    Watch the comments in this conversation to review them.

    9 extra issues

    Note: The following issues were found on lines that were not modified in the commit. Because these issues can't be reported as line comments, they are summarized here:

    1. Remove this commented out code. 📘
    2. Remove this commented out code. 📘
    3. Remove this commented out code. 📘
    4. Remove this commented out code. 📘
    5. Remove this commented out code. 📘
    6. Remove this commented out code. 📘
    7. Remove this commented out code. 📘
    8. Remove this commented out code. 📘
    9. Remove this commented out code. 📘
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