feat(webauthn) Add WebAuthn plugin to store WebAuthn registrations

Only allows to store registration result is the LDAP for now.
Relies on https://github.com/lbuchs/WebAuthn
Needs PHP>=7.1

issue #6013
# Veuillez saisir le message de validation pour vos modifications. Les lignes
# commençant par '#' seront ignorées, et un message vide abandonne la validation.
# Sur la branche 6013-add-plugin-for-webauthn
# Votre branche est à jour avec 'gitlab/6013-add-plugin-for-webauthn'.
#
# Modifications qui seront validées :
#	nouveau fichier : webauthn/contrib/openldap/webauthn-fd.schema
#	nouveau fichier : webauthn/html/include/webauthn.js
#	nouveau fichier : webauthn/personal/webauthn/class_WebauthnRegistrationsAttribute.inc
#	nouveau fichier : webauthn/personal/webauthn/class_webauthnAccount.inc
#
# Fichiers non suivis:
#	.php_cs.cache
#	.php_cs.dist
#	mail/personal/mail/class_sieve_new.inc
#	webservice/contrib/test/composer.json
#	webservice/contrib/test/composer.lock
#	webservice/contrib/test/dredd.out
#	webservice/contrib/test/vendor/
#	webservice/html/openapi-custom.yaml
#	webservice/html/openapi-test.yaml
#
parent e82e5936
##
## 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 ) )
/**
* 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 checkregistration()
{
if (!window.fetch || !navigator.credentials || !navigator.credentials.create) {
window.alert('Browser not supported.');
return;
}
// get default args
window.fetch('server.php?fn=getGetArgs' + getGetParams(), {method:'GET',cache:'no-cache'}).then(function(response) {
return response.json();
// convert base64 to arraybuffer
}).then(function(json) {
// 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;
// create credentials
}).then(function(getCredentialArgs) {
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
}).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) {
return window.fetch('server.php?fn=processGet' + getGetParams(), {method:'POST', body: AuthenticatorAttestationResponse, cache:'no-cache'});
// convert to json
}).then(function(response) {
return response.json();
// analyze response
}).then(function(json) {
if (json.success) {
window.alert(json.msg || 'login success');
} else {
throw new Error(json.msg);
}
// catch errors
}).catch(function(err) {
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);
}
/**
* Get URL parameter
* @returns {String}
*/
function getGetParams() {
let url = '';
url += '&yubico=' + (document.getElementById('cert_yubico').checked ? '1' : '0');
url += '&solo=' + (document.getElementById('cert_solo').checked ? '1' : '0');
url += '&hypersecu=' + (document.getElementById('cert_hypersecu').checked ? '1' : '0');
url += '&google=' + (document.getElementById('cert_google').checked ? '1' : '0');
url += '&requireResidentKey=' + (document.getElementById('requireResidentKey').checked ? '1' : '0');
url += '&fmt_android-key=' + (document.getElementById('fmt_android-key').checked ? '1' : '0');
url += '&fmt_android-safetynet=' + (document.getElementById('fmt_android-safetynet').checked ? '1' : '0');
url += '&fmt_fido-u2f=' + (document.getElementById('fmt_fido-u2f').checked ? '1' : '0');
url += '&fmt_none=' + (document.getElementById('fmt_none').checked ? '1' : '0');
url += '&fmt_packed=' + (document.getElementById('fmt_packed').checked ? '1' : '0');
url += '&fmt_tpm=' + (document.getElementById('fmt_tpm').checked ? '1' : '0');
return url;
}
/**
* 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.
*/
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;
}
function addRegistration (string $value)
{
$this->value[] = $value;
$this->reIndexValues();
}
protected function getAttributeArrayValue ($key, $json)
{
$data = json_decode($json, TRUE);
return [
//~ $data['credentialId'],
//~ print_r($data['credentialPublicKey'], TRUE),
//~ print_r($data['certificateChain'], TRUE),
//~ print_r($data['certificate'], TRUE),
$data['certificateIssuer'],
$data['certificateSubject'],
($data['signatureCounter'] ?? 0),
//~ print_r($data['AAGUID'], TRUE),
$data['rpId'],
];
}
/*protected function genRowIcons ($key, $value)
{
return ['', 0];
}
public function htmlIds (): array
{
return [];
}
function renderButtons ()
{
return '';
}*/
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':
//~ TODO : corriger Unused bytes after data item.
print(json_encode($this->processCreate()));
break;
default:
throw new FusionDirectoryException('Unknown operation '.$_GET['webauthn']);
}
} catch (Throwable $ex) {
$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();
}
protected function initWebAuthnObject ()
{
// Formats
$formats = array();
//~ if ($_GET['fmt_android-key']) {
$formats[] = 'android-key';
//~ }
//~ if ($_GET['fmt_android-safetynet']) {
$formats[] = 'android-safetynet';
//~ }
//~ if ($_GET['fmt_fido-u2f']) {
$formats[] = 'fido-u2f';
//~ }
//~ if ($_GET['fmt_none']) {
$formats[] = 'none';
//~ }
//~ if ($_GET['fmt_packed']) {
$formats[] = 'packed';
//~ }
//~ if ($_GET['fmt_tpm']) {
//~ $formats[] = 'tpm';
//~ }
// new Instance of the server library.
// make sure that $rpId is the domain name.
$WebAuthn = new \WebAuthn\WebAuthn('WebAuthn Library', 'localhost', $formats);
// add root certificates to validate new registrations
/*if ($_GET['solo']) {
$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;
}
protected function getCreateArgs ()
{
global $ui;
$WebAuthn = $this->initWebAuthnObject();
//~ ($userId, $userName, $userDisplayName, $timeout=20, $requireResidentKey=false, $requireUserVerification=false, $excludeCredentialIds=array()) {
$createArgs = $WebAuthn->getCreateArgs($ui->dn, $ui->uid, $ui->cn, 20, FALSE);
session::set('challenge', $WebAuthn->getChallenge());
return $createArgs;
}
protected function processCreate ()
{
$WebAuthn = $this->initWebAuthnObject();
$post = trim(file_get_contents('php://input'));
if ($post) {
$post = json_decode($post);
}
$clientDataJSON = base64_decode($post->clientDataJSON);
$attestationObject = base64_decode($post->attestationObject);
$challenge = session::get('challenge');
$data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge);
$safeData = [];
foreach (['rpId','credentialPublicKey','certificateChain','certificate','certificateIssuer','certificateSubject','signatureCounter'] as $stringField) {
$safeData[$stringField] = $data->$stringField;
}
foreach (['credentialId','AAGUID'] as $binField) {
// base64 encode binary fields
$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.';
session::un_set('challenge');
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