Skip to content

Commit

Permalink
Merge pull request #17989 from MauricioFauth/webauthn-custom
Browse files Browse the repository at this point in the history
Add support for Web Authentication API
  • Loading branch information
MauricioFauth committed Jan 16, 2023
2 parents d0fc1da + f143d2a commit b8578c3
Show file tree
Hide file tree
Showing 20 changed files with 2,225 additions and 5 deletions.
6 changes: 4 additions & 2 deletions composer.json
Expand Up @@ -93,7 +93,8 @@
"tecnickcom/tcpdf": "For PDF support",
"pragmarx/google2fa-qrcode": "^2.1 - For 2FA authentication",
"bacon/bacon-qr-code": "^2.0 - For 2FA authentication",
"code-lts/u2f-php-server": "For FIDO U2F authentication"
"code-lts/u2f-php-server": "For FIDO U2F authentication",
"web-auth/webauthn-lib": "For better WebAuthn/FIDO2 authentication support"
},
"require-dev": {
"bacon/bacon-qr-code": "^2.0",
Expand All @@ -111,7 +112,8 @@
"squizlabs/php_codesniffer": "~3.6.0",
"symfony/console": "^5.2.3",
"tecnickcom/tcpdf": "^6.4.4",
"vimeo/psalm": "^4.22"
"vimeo/psalm": "^4.22",
"web-auth/webauthn-lib": "^3.3"
},
"extra": {
"branch-alias": {
Expand Down
133 changes: 133 additions & 0 deletions js/src/webauthn.js
@@ -0,0 +1,133 @@
/**
* @param {ArrayBuffer} buffer
*
* @return {string}
*/
const arrayBufferToBase64 = buffer => {
const bytes = new Uint8Array(buffer);
let string = '';
for (const byte of bytes) {
string += String.fromCharCode(byte);
}

return window.btoa(string);
};

/**
* @param {string} string
*
* @return {Uint8Array}
*/
const base64ToUint8Array = string => {
return Uint8Array.from(window.atob(string), char => char.charCodeAt(0));
};

/**
* @param {JQuery<HTMLElement>} $input
*
* @return {void}
*/
const handleCreation = $input => {
const $form = $input.parents('form');
$form.find('input[type=submit]').hide();

const creationOptionsJson = $input.attr('data-creation-options');
const creationOptions = JSON.parse(creationOptionsJson);

const publicKey = creationOptions;
publicKey.challenge = base64ToUint8Array(creationOptions.challenge);
publicKey.user.id = base64ToUint8Array(creationOptions.user.id);
if (creationOptions.excludeCredentials) {
const excludedCredentials = [];
for (let value of creationOptions.excludeCredentials) {
let excludedCredential = value;
excludedCredential.id = base64ToUint8Array(value.id);
excludedCredentials.push(excludedCredential);
}
publicKey.excludeCredentials = excludedCredentials;
}

// eslint-disable-next-line compat/compat
navigator.credentials.create({ publicKey: publicKey })
.then((credential) => {
const credentialJson = JSON.stringify({
id: credential.id,
rawId: arrayBufferToBase64(credential.rawId),
type: credential.type,
response: {
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
attestationObject: arrayBufferToBase64(credential.response.attestationObject),
}
});
$input.val(credentialJson);
$form.trigger('submit');
})
.catch((error) => Functions.ajaxShowMessage(error, false, 'error'));
};

/**
* @param {JQuery<HTMLElement>} $input
*
* @return {void}
*/
const handleRequest = $input => {
const $form = $input.parents('form');
$form.find('input[type=submit]').hide();

const requestOptionsJson = $input.attr('data-request-options');
const requestOptions = JSON.parse(requestOptionsJson);

const publicKey = requestOptions;
publicKey.challenge = base64ToUint8Array(requestOptions.challenge);
if (requestOptions.allowCredentials) {
const allowedCredentials = [];
for (let value of requestOptions.allowCredentials) {
let allowedCredential = value;
allowedCredential.id = base64ToUint8Array(value.id);
allowedCredentials.push(allowedCredential);
}
publicKey.allowCredentials = allowedCredentials;
}

// eslint-disable-next-line compat/compat
navigator.credentials.get({ publicKey: publicKey })
.then((credential) => {
const credentialJson = JSON.stringify({
id: credential.id,
rawId: arrayBufferToBase64(credential.rawId),
type: credential.type,
response: {
authenticatorData: arrayBufferToBase64(credential.response.authenticatorData),
clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),
signature: arrayBufferToBase64(credential.response.signature),
userHandle: arrayBufferToBase64(credential.response.userHandle),
}
});
$input.val(credentialJson);
$form.trigger('submit');
})
.catch((error) => Functions.ajaxShowMessage(error, false, 'error'));
};

AJAX.registerOnload('webauthn.js', function () {
if (
! navigator.credentials
|| ! navigator.credentials.create
|| ! navigator.credentials.get
|| ! window.PublicKeyCredential
) {
Functions.ajaxShowMessage(Messages.webAuthnNotSupported, false, 'error');

return;
}

const $creationInput = $('#webauthn_creation_response');
if ($creationInput.length > 0) {
handleCreation($creationInput);
}

const $requestInput = $('#webauthn_request_response');
if ($requestInput.length > 0) {
handleRequest($requestInput);
}
});
Expand Up @@ -692,6 +692,10 @@ private function setMessages(): void
// l10n: error code 4 (from U2F API) on authanticate
'strU2FErrorAuthenticate' => _pgettext('U2F error', 'Invalid security key.'),

'webAuthnNotSupported' => __(
'WebAuthn is not available. Please use a supported browser in a secure context (HTTPS).'
),

/* Designer */
'strIndexedDBNotWorking' => __(
'You can not open, save or delete your page layout, as IndexedDB is not working'
Expand Down
2 changes: 1 addition & 1 deletion libraries/classes/Plugins/TwoFactor/Key.php
Expand Up @@ -215,6 +215,6 @@ public static function getName()
*/
public static function getDescription()
{
return __('Provides authentication using hardware security tokens supporting FIDO U2F, such as a Yubikey.');
return __('Provides authentication using hardware security tokens supporting FIDO U2F, such as a YubiKey.');
}
}

0 comments on commit b8578c3

Please sign in to comment.