Commit 09ff51bc authored by Côme Chilliet's avatar Côme Chilliet

Merge branch '6013-add-plugin-for-webauthn' into '1.4-dev'

Resolve "Add plugin for WebAuthn"

See merge request fusiondirectory/fd-plugins!653
parents e82e5936 1331e7c0
<?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 webauthnConfig extends simplePlugin
{
static function plInfo (): array
{
return [
'plShortName' => _('WebAuthn'),
'plTitle' => _('WebAuthn configuration'),
'plDescription' => _('FusionDirectory WebAuthn plugin configuration'),
'plObjectClass' => ['fdWebauthnPluginConf'],
'plCategory' => ['configuration'],
'plObjectType' => ['smallConfig'],
'plProvidedAcls' => parent::generatePlProvidedAcls(static::getAttributesInfo())
];
}
static function getAttributesInfo (): array
{
global $config;
return [
'main' => [
'name' => _('WebAuthn'),
'attrs' => [
new SetAttribute(
new SelectAttribute(
_('Formats'), _('List of allowed formats'),
'fdWebauthnFormats', FALSE,
['android-key','android-safetynet','fido-u2f','packed','none']
),
['android-key','fido-u2f','packed']
),
]
],
];
}
}
##
## webauthn-fd-conf.schema - Needed by Fusion Directory for managing webauthn plugin configuration backend
##
attributetype ( 1.3.6.1.4.1.38414.74.1.1 NAME 'fdWebauthnFormats'
DESC 'FusionDirectory - Allowed webauthn formats'
EQUALITY caseExactMatch
SUBSTR caseExactSubstringsMatch
SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 )
objectclass ( 1.3.6.1.4.1.38414.74.2.1 NAME 'fdWebauthnPluginConf'
DESC 'FusionDirectory webauthn plugin configuration'
SUP top AUXILIARY
MUST ( cn )
MAY (
fdWebauthnFormats
) )
##
## webauthn-fd.schema - Needed by Fusion Directory for managing WebAuthn registrations
##
# Attributes
attributetype ( 1.3.6.1.4.1.38414.73.1.1 NAME 'fdWebauthnRegistrations'
DESC 'FusionDirectory - WebAuthn registrations stored as JSON'
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'
MUST ( fdWebauthnRegistrations ) )
/*
Copyright (C) 2018 Lukas Buchs
Copyright (C) 2019 FusionDirectory
license https://github.com/lbuchs/WebAuthn/blob/master/LICENSE MIT
*/
/**
* creates a new FIDO2 registration
* @returns {undefined}
*/
function webauthnNewRegistration()
{
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
window.alert('Browser not supported.');
return;
}
// get default args
window.fetch(window.location.href + '&webauthn=getCreateArgs', {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
}).then(function(json) {
// convert base64 to arraybuffer
if (json.success === false) {
// error handling
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;
}).then(function(createCredentialArgs) {
// create credentials
console.log(createCredentialArgs);
return navigator.credentials.create(createCredentialArgs);
}).then(function(cred) {
// convert to base64
return {
clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null,
attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null
};
}).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
// transfer to server
return window.fetch(window.location.href + '&webauthn=processCreate', {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'})
}).then(function(response) {
// convert to JSON
return response.json();
}).then(function(json) {
// analyze response
if (json.success) {
window.alert(json.msg || 'registration success');
document.mainform.submit();
} else {
throw new Error(json.msg);
}
}).catch(function(err) {
// catch errors
window.alert(err.message || 'unknown error occured');
});
}
/**
* checks a FIDO2 registration
* @returns {undefined}
*/
function webauthnCheckRegistration()
{
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
window.alert('Browser not supported.');
return;
}
// get default args
window.fetch(window.location.href + '?webauthn=getGetArgs', {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
}).then(function(json) {
// convert base64 to arraybuffer
if (json.success === false) {
// error handling
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;
}).then(function(getCredentialArgs) {
// create credentials
return navigator.credentials.get(getCredentialArgs);
}).then(function(cred) {
// 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) {
// transfer to server
return window.fetch(window.location.href + '?webauthn=processGet', {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
}).then(function(response) {
// convert to json
return response.json();
}).then(function(json) {
// analyze response
if (json.success) {
document.location = 'main.php';
} else {
throw new Error(json.msg);
}
}).catch(function(err) {
// catch errors
window.alert(err.message || 'unknown error occured');
});
}
/**
* convert RFC 1342-like base64 strings to array buffer
* @param {mixed} obj
* @returns {undefined}
*/
function recursiveBase64StrToArrayBuffer(obj)
{
let prefix = '?BINARY?B?';
let suffix = '?=';
if (typeof obj === 'object') {
for (let key in obj) {
if (typeof obj[key] === 'string') {
let str = obj[key];
if (str.substring(0, prefix.length) === prefix && str.substring(str.length - suffix.length) === suffix) {
str = str.substring(prefix.length, str.length - suffix.length);
let binary_string = window.atob(str);
let len = binary_string.length;
let bytes = new Uint8Array(len);
for (var i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
obj[key] = bytes.buffer;
}
} else {
recursiveBase64StrToArrayBuffer(obj[key]);
}
}
}
}
/**
* Convert a ArrayBuffer to Base64
* @param {ArrayBuffer} buffer
* @returns {String}
*/
function arrayBufferToBase64(buffer) {
var binary = '';
var bytes = new Uint8Array(buffer);
var len = bytes.byteLength;
for (var i = 0; i < len; i++) {
binary += String.fromCharCode( bytes[ i ] );
}
return window.btoa(binary);
}
/**
* force https on load
* @returns {undefined}
*/
window.onload = function() {
if (location.protocol !== 'https:' && location.host !== 'localhost') {
location.href = location.href.replace('http', 'https');
}
}
<?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');
class SecondFactorWebAuthn
{
static function init ()
{
session::un_set('WebauthnChallenge');
session::un_set('fdWebauthnRegistrations');
}
/*! \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, ['fdWebauthnRegistrations']);
$attrs = $ldap->fetch();
if (!$attrs) {
throw new FusionDirectoryException(_('Could not fetch user'));
}
unset($attrs['fdWebauthnRegistrations']['count']);
if (empty($attrs['fdWebauthnRegistrations'])) {
return FALSE;
}
session::set('fdWebauthnRegistrations', $attrs['fdWebauthnRegistrations']);
return TRUE;
}
/*! \brief Process WebAuthn Javascript Requests */
static function earlyProcess ()
{
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();
}
}
static protected function getGetArgs ()
{
global $ui;
$WebAuthn = webauthnAccount::initWebAuthnObject();
$ids = [];
$fdWebauthnRegistrations = session::get('fdWebauthnRegistrations');
foreach ($fdWebauthnRegistrations as $fdWebauthnRegistration) {
$reg = webauthnAccount::unserializeRegistration($fdWebauthnRegistration);
$ids[] = $reg['credentialId'];
}
$getArgs = $WebAuthn->getGetArgs($ids);
/* Serializing avoids failed autoload of WebAuthn classes before our require_once (at session start) */
session::set('WebauthnChallenge', serialize($WebAuthn->getChallenge()));
return $getArgs;
}
static protected function processGet ()
{
global $config;
$WebAuthn = webauthnAccount::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('WebauthnChallenge'));
$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 FusionDirectoryException('Public Key for credential ID not found!');
}
// process the get request. throws WebAuthnException if it fails
$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $credentialPublicKey, $challenge);
session::un_set('WebauthnChallenge');
static::connect();
$return = new stdClass();
$return->success = TRUE;
return $return;
}
static public function execute ()
{
global $smarty;
if (!session::is_set('fdWebauthnRegistrations')) {
return NULL;
}
$smarty->append('js_files', 'include/webauthn.js');
$message = _('Trying to communicate with your device. Plug it in (if you haven\'t already) and press the button on the device now.');
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');
}
}
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR FusionDirectory Project
# This file is distributed under the same license as the FusionDirectory package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: FusionDirectory VERSION\n"
"Report-Msgid-Bugs-To: bugs@fusiondirectory.org\n"
"POT-Creation-Date: 2019-11-21 11:12+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FusionDirectory project <contact@fusiondirectory.org>\n"
"Language-Team: English\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: config/webauthn/class_WebauthnConfig.inc:27
#: config/webauthn/class_WebauthnConfig.inc:44
#: personal/webauthn/class_webauthnAccount.inc:30
msgid "WebAuthn"
msgstr ""
#: config/webauthn/class_WebauthnConfig.inc:28
msgid "WebAuthn configuration"
msgstr ""
#: config/webauthn/class_WebauthnConfig.inc:29
msgid "FusionDirectory WebAuthn plugin configuration"
msgstr ""
#: config/webauthn/class_WebauthnConfig.inc:48
msgid "Formats"
msgstr ""
#: config/webauthn/class_WebauthnConfig.inc:48
msgid "List of allowed formats"
msgstr ""
#: include/login/class_LoginWebAuthnPost.inc:31
msgid "HTML form and 2nd factor"
msgstr ""
#: include/login/class_LoginWebAuthnPost.inc:91
msgid "Warning"
msgstr ""
#: include/login/class_LoginWebAuthnPost.inc:91
msgid ""
"Your browser has cookies disabled. Please enable cookies and reload this "
"page before logging in!"
msgstr ""
#: include/login/class_LoginWebAuthnPost.inc:106
msgid "Could not fetch user"
msgstr ""
#: include/login/class_LoginWebAuthnPost.inc:111
msgid "2nd factor information missing"
msgstr ""
#: include/login/class_LoginWebAuthnPost.inc:147
msgid "Second factor"
msgstr ""
#: include/login/class_LoginWebAuthnPost.inc:157
#, php-format
msgid "Warning: <a href=\"%s\">Session is not encrypted!</a>"
msgstr ""
#: personal/webauthn/class_WebauthnRegistrationsAttribute.inc:31
msgid "Issuer"
msgstr ""
#: personal/webauthn/class_WebauthnRegistrationsAttribute.inc:32
msgid "Subject"
msgstr ""
#: personal/webauthn/class_WebauthnRegistrationsAttribute.inc:33
msgid "Signature count"
msgstr ""
#: personal/webauthn/class_WebauthnRegistrationsAttribute.inc:34
msgid "Domain"
msgstr ""
#: personal/webauthn/class_webauthnAccount.inc:31
msgid "Manage double factor authentication"
msgstr ""
#: personal/webauthn/class_webauthnAccount.inc:46
msgid "Devices"
msgstr ""
#: personal/webauthn/class_webauthnAccount.inc:50
msgid "Registrations"
msgstr ""
#: personal/webauthn/class_webauthnAccount.inc:50
msgid "Registrations for this user"
msgstr ""
#: ihtml/themes/breezy/secondfactor.tpl.c:2
msgid "Two factor authentication"
msgstr ""
#: ihtml/themes/breezy/secondfactor.tpl.c:5
msgid ""
"Trying to communicate with your device. Plug it in (if you haven't already) "
"and press the button on the device now."
msgstr ""
<?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 WebauthnRegistrationsAttribute extends OrderedArrayAttribute
{
function __construct ($label, $description, $ldapName, $required = FALSE, $defaultValue = [], $acl = '')
{
Attribute::__construct($label, $description, $ldapName, $required, $defaultValue, $acl);
$this->edit_enabled = FALSE;
$this->attribute = FALSE;
$this->order = FALSE;
$this->setHeaders([
_('Issuer'),
_('Subject'),
_('Signature count'),
_('Domain'),
'',
]);
}
function addRegistration (string $value)
{
$this->value[] = $value;
$this->reIndexValues();
}
protected function getAttributeArrayValue ($key, $json)
{
$data = json_decode($json, TRUE);
return [
$data['certificateIssuer'],
$data['certificateSubject'],
($data['signatureCounter'] ?? 0),
$data['rpId'],
];
}
function renderButtons ()
{
$id = $this->getHtmlId();
$buttons = $this->renderInputField('button', 'add'.$id, ['value' => '{msgPool type=addButton}', 'formnovalidate' => 'formnovalidate', 'class' => 'subattribute', 'onclick' => 'webauthnNewRegistration()']);
return $buttons;
}
}
<?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');
class webauthnAccount extends simplePlugin
{
var $displayHeader = TRUE;
static function plInfo (): array
{
return [
'plShortName' => _('WebAuthn'),
'plDescription' => _('Manage double factor authentication'),
'plSelfModify' => TRUE,
'plObjectClass' => ['fdWebauthnAccount'],
'plObjectType' => ['user'],
'plIcon' => 'geticon.php?context=applications&icon=webauthn&size=48',
'plSmallIcon' => 'geticon.php?context=applications&icon=webauthn&size=16',
'plProvidedAcls' => parent::generatePlProvidedAcls(static::getAttributesInfo())
];
}
static function getAttributesInfo (): array
{
return [
'main' => [
'name' => _('Devices'),
'class' => ['fullwidth'],
'attrs' => [
new WebauthnRegistrationsAttribute(
_('Registrations'), _('Registrations for this user'),
'fdWebauthnRegistrations', TRUE
)
]
]
];
}
function execute (): string
{
global $smarty;
if (isset($_GET['webauthn'])) {
try {
switch ($_GET['webauthn']) {
case 'getCreateArgs':
print(json_encode($this->getCreateArgs()));
break;
case 'processCreate':
print(json_encode($this->processCreate()));
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->append('js_files', 'include/webauthn.js');
return parent::execute();
}
static public function initWebAuthnObject ()
{
global $config;
// Formats
$formats = $config->get_cfg_value('WebauthnFormats', ['android-key','fido-u2f','packed']);
// 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'];