Verified Commit f20012b2 authored by Côme Chilliet's avatar Côme Chilliet
Browse files

:sparkles: feat(webauthn) Add basic recovery code feature

issue #6019
parent 1e279d8f
No related merge requests found
Showing with 125 additions and 12 deletions
+125 -12
......@@ -15,7 +15,13 @@ attributetype ( 1.3.6.1.4.1.38414.73.1.2 NAME 'fdTOTPTokens'
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26)
attributetype ( 1.3.6.1.4.1.38414.73.1.3 NAME 'fdWebauthnRecoveryCode'
DESC 'FusionDirectory - Second factor recovery code'
EQUALITY caseExactIA5Match
SUBSTR caseIgnoreSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.26)
# Objectclasses
objectclass (1.3.6.1.4.1.38414.73.2.1 NAME 'fdWebauthnAccount' SUP top AUXILIARY
DESC 'FusionDirectory - User WebAuthn tab'
MAY ( fdWebauthnRegistrations $ fdTOTPTokens ) )
MAY ( fdWebauthnRegistrations $ fdTOTPTokens $ fdWebauthnRecoveryCode ) )
<?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.
*/
class SecondFactorRecoveryCode
{
static function init ()
{
session::un_set('fdWebauthnRecoveryCode');
}
/*! \brief Checks if connected user has second factor active */
static function hasSecondFactor (): bool
{
global $ui, $config;
$ldap = $config->get_ldap_link();
$ldap->cat($ui->dn, ['fdWebauthnRecoveryCode']);
$attrs = $ldap->fetch();
if (!$attrs) {
throw new FusionDirectoryException(_('Could not fetch user'));
}
unset($attrs['fdWebauthnRecoveryCode']['fdWebauthnRecoveryCode']);
if (empty($attrs['fdWebauthnRecoveryCode'])) {
return FALSE;
}
session::set('fdWebauthnRecoveryCode', $attrs['fdWebauthnRecoveryCode']);
return TRUE;
}
/*! \brief Process POST */
static function earlyProcess ()
{
global $message;
if (isset($_POST['recoverycode'])) {
$fdWebauthnRecoveryCodes = session::get('fdWebauthnRecoveryCode');
foreach ($fdWebauthnRecoveryCodes as $fdWebauthnRecoveryCode) {
list(, $hash) = explode('|', $fdWebauthnRecoveryCode, 2);
if (password_verify($_POST['recoverycode'], $hash)) {
/* TODO:
  • :information_source: Complete the task associated to this "TODO" comment. :blue_book:

Please register or sign in to reply
* delete this recovery code so that it can only be used once
* Warn the user that he needs to generate a new one */
static::redirect();
}
}
/* Slow down brute force by making failing slow: wait 5 seconds for each fail attempt */
sleep(5);
/* Inform the user that validation failed */
$message = _('Validation of recovery code failed');
}
}
static public function execute ()
{
global $smarty;
if (!session::is_set('fdWebauthnRecoveryCode')) {
return NULL;
}
return '<label for="otpcode">'._('Or enter recovery Code here: ').'<input type="text" id="recoverycode" name="recoverycode"/></label>';
}
static function redirect ()
{
session::un_set('fdWebauthnRecoveryCode');
LoginMethod::redirect();
}
}
......@@ -58,7 +58,7 @@ class SecondFactorTotp
return TRUE;
}
/*! \brief Process WebAuthn Javascript Requests */
/*! \brief Process POST */
static function earlyProcess ()
{
global $message;
......
......@@ -43,22 +43,20 @@ class webauthnAccount extends simplePlugin
{
return [
'main' => [
'name' => _('Devices'),
'name' => _('Second factor'),
'class' => ['fullwidth'],
'attrs' => [
new WebauthnRegistrationsAttribute(
_('Registrations'), _('Registrations for this user'),
_('WebAuthn'), _('Registrations for this user'),
'fdWebauthnRegistrations'
)
]
],
'totp' => [
'name' => _('TOTP'),
'class' => ['fullwidth'],
'attrs' => [
),
new TOTPRegistrationsAttribute(
_('Codes'), _('TOTP codes for this user'),
_('TOTP'), _('TOTP codes for this user'),
'fdTOTPTokens'
),
new ButtonAttribute(
_('Recovery code'), _('Single use recovery code in case you lose all your devices. Print it and store it. Do not save it on your computer.'),
'fdWebauthnRecoveryCode', _('Generate')
)
]
]
......@@ -96,6 +94,24 @@ class webauthnAccount extends simplePlugin
return parent::execute();
}
function handle_fdWebauthnRecoveryCode()
{
$len = random_int(10, 15);
/* All letters and numbers except 0, O and o to avoid confusion */
$base = 'ABCDEFGHKLMNPQRSTWXYZabcdefghjkmnpqrstwxyz123456789';
$max = strlen($base) - 1;
$randomhash = '';
while (strlen($randomhash) < $len + 1) {
$randomhash .= $base[random_int(0, $max)];
}
$this->fdWebauthnRecoveryCode = date('Y-m-d').'|'.password_hash($randomhash, PASSWORD_DEFAULT);
msg_dialog::display(
_('Recovery code'),
sprintf(_('Here is your recovery code: %s<br/><br/>Print and hide it. Do not save it on your computer. You may only use it once.'), $randomhash),
INFO_DIALOG
);
}
static public function initWebAuthnObject ()
{
global $config;
......
  • SonarQube analysis reported 1 issue

    • :information_source: 1 info

    Watch the comments in this conversation to review them.

Supports Markdown
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