Commit 8ea045cb authored by Côme Chilliet's avatar Côme Chilliet

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