Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactored WebAuthn with Windows Hello support #37910

Merged
merged 57 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
0a3d81c
Refactored WebAuthn plugin
nikosdion Apr 26, 2022
b8a6ad7
Fix the WebAuthn management page which was broken in https://github.c…
nikosdion Apr 26, 2022
c8c6446
Fix wrong `@since` doc tag
nikosdion Apr 27, 2022
abb868c
Fix docblock typo
nikosdion Apr 27, 2022
8246463
Fix docblock typo
nikosdion Apr 27, 2022
444a8db
Fix docblock typo
nikosdion Apr 27, 2022
6396ba8
Fix docblock typo
nikosdion Apr 27, 2022
ec5e32a
Fix docblock typo
nikosdion Apr 27, 2022
9c261d1
Fix broken management interface
nikosdion Apr 27, 2022
7966047
Make unnecessarily static method back into non-static
nikosdion Apr 27, 2022
833fe6e
Replace static helper with injected object
nikosdion Apr 27, 2022
7381a4b
Come on, commit the ENTIRE file!
nikosdion Apr 27, 2022
0bc36d6
Use the user factory
nikosdion Apr 27, 2022
b80b3b9
Fix error when going through the user factory
Apr 27, 2022
cb89159
Fix: cannot add WebAuthn authenticator right back after deleting it
nikosdion Apr 27, 2022
78d7fd5
Remove useless switch branch
nikosdion Apr 27, 2022
2dc8c4a
Remove useless exception
nikosdion Apr 27, 2022
1b3d4ee
Display make and model of the authenticator, if possible
nikosdion Apr 28, 2022
f019fde
Add missing JWT signature algorithms
nikosdion Apr 28, 2022
786d165
Fix copyright date
nikosdion Apr 28, 2022
d3671b9
Fix for PHP 8 using FIDO keys and Android phones
nikosdion Apr 28, 2022
ca5451a
Reactivate the tooltips after adding an authenticator
nikosdion Apr 28, 2022
09140d0
Option to disable the attestation support
nikosdion Apr 28, 2022
71815ff
The Windows Hello icon was invisible on white background
nikosdion Apr 28, 2022
d2e20ce
Attempt to fix Appveyor not having Sodium in the Windows build
nikosdion Apr 29, 2022
0c32939
Work around third party library bug...
nikosdion Apr 29, 2022
ae9234b
Merge branch '4.2-dev' into feature/webauthn-refactor-2
richard67 May 4, 2022
dace695
Merge branch '4.2-dev' into feature/webauthn-refactor-2
nikosdion May 24, 2022
6de7bdb
Create events in a forwards-compatible manner
nikosdion May 24, 2022
c03b191
Concrete events
nikosdion May 24, 2022
100605b
Merge branch '4.2-dev' into feature/webauthn-refactor-2
nikosdion May 27, 2022
9f8d55e
Merge branch '4.2-dev' into feature/webauthn-refactor-2
richard67 Jun 6, 2022
784f878
Merge branch '4.2-dev' into feature/webauthn-refactor-2
nikosdion Jun 12, 2022
9af4218
Fix event woes
nikosdion Jun 12, 2022
1d26630
Update plugins/system/webauthn/webauthn.xml
nikosdion Jun 13, 2022
0b973ea
Update administrator/language/en-GB/plg_system_webauthn.ini
nikosdion Jun 13, 2022
66249ac
Merge branch '4.2-dev' into feature/webauthn-refactor-2
nikosdion Jun 13, 2022
3cab3a8
Merge branch '4.2-dev' into feature/webauthn-refactor-2
roland-d Jun 15, 2022
07c2c05
Merge branch '4.2-dev' into feature/webauthn-refactor-2
nikosdion Jun 17, 2022
50f2f4e
Improve the layout for editing an authenticator
nikosdion Jun 17, 2022
843ccbd
Confirm deletion of authenticators
nikosdion Jun 17, 2022
d0a3b5f
Make the bots happy again
nikosdion Jun 17, 2022
4477f10
Merge branch '4.2-dev' into feature/webauthn-refactor-2
laoneo Jun 19, 2022
fbfba3c
Merge branch '4.2-dev' into feature/webauthn-refactor-2
nikosdion Jun 20, 2022
c848533
Code polishing
nikosdion Jun 20, 2022
95316ff
Merge remote-tracking branch 'nikosdion/feature/webauthn-refactor-2' …
nikosdion Jun 20, 2022
218ec93
Merge branch '4.2-dev' into feature/webauthn-refactor-2
roland-d Jun 22, 2022
a7cdd3f
Blind fix
nikosdion Jun 23, 2022
c451923
Bring application injection in sync with core
HLeithner Jun 24, 2022
bcdcbaa
Merge pull request #9 from HLeithner/hello-windows
nikosdion Jun 24, 2022
61296c8
Remove whitespace
wilsonge Jun 24, 2022
2ae690b
Add use statement
wilsonge Jun 24, 2022
7f97cc2
Merge branch '4.2-dev' into feature/webauthn-refactor-2
richard67 Jun 26, 2022
7ef74c5
Fix wrong event creation in AjaxHandlerLogin
nikosdion Jun 27, 2022
c8503a9
Merge branch '4.2-dev' into feature/webauthn-refactor-2
nikosdion Jun 27, 2022
fc94359
License change
nikosdion Jun 27, 2022
6f1a658
Merge branch '4.2-dev' into feature/webauthn-refactor-2
HLeithner Jun 27, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ install:
- choco install composer
- cd C:\projects\joomla-cms
- refreshenv
- composer install --no-progress --profile
- composer install --no-progress --profile --ignore-platform-req=ext-sodium
before_test:
# Database setup for MySQL via PowerShell tools
- >
Expand Down
8 changes: 6 additions & 2 deletions administrator/language/en-GB/plg_system_webauthn.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,21 @@ PLG_SYSTEM_WEBAUTHN_ERR_CREDENTIAL_ID_ALREADY_IN_USE="Cannot save credentials. T
PLG_SYSTEM_WEBAUTHN_ERR_EMPTY_USERNAME="You need to enter your username (but NOT your password) before selecting the Web Authentication login button."
PLG_SYSTEM_WEBAUTHN_ERR_INVALID_USERNAME="The specified username does not correspond to a user account that has enabled passwordless login on this site."
PLG_SYSTEM_WEBAUTHN_ERR_LABEL_NOT_SAVED="Could not save the new label"
PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED="Could not remove the authenticator"
PLG_SYSTEM_WEBAUTHN_ERR_NO_BROWSER_SUPPORT="Sorry, your browser does not support the W3C Web Authentication standard for passwordless logins or your site is not being served over HTTPS with a valid certificate, signed by a Certificate Authority your browser trusts. You will need to log into this site using your username and password."
PLG_SYSTEM_WEBAUTHN_ERR_NO_STORED_CREDENTIAL="Cannot find the stored credentials for your login authenticator."
PLG_SYSTEM_WEBAUTHN_ERR_NOT_DELETED="Could not remove the authenticator"
PLG_SYSTEM_WEBAUTHN_ERR_USER_REMOVED="The user for this authenticator seems to no longer exist on this site."
PLG_SYSTEM_WEBAUTHN_ERR_XHR_INITCREATE="Cannot get the authenticator registration information from your site."
PLG_SYSTEM_WEBAUTHN_FIELD_ATTESTATION_SUPPORT_DESC="Only allow authenticators with verifiable cryptographic signatures to be used for WebAuthn logins. Strongly recommended for high security environments. Requires your site to be able to access <code>https://mds.fidoalliance.org/</code> directly, a writeable cache directory, the system temporary directory being writeable by PHP, and the OpenSSL extension. May prevent some cheaper, non-certified authenticators from working at all. Disabling it also prevents Joomla from identifying the make and model of the authenticator you are using (no icon will be displayed next to the Authenticator Name).<br/><strong>Pro tip:</strong> If you are behind a firewall you can place the data downloaded from <a href='https://mds.fidoalliance.org/' target='_blank'>the FIDO Alliance</a> into the file <code>administrator/cache/fido.jwt</code> for this feature to work properly."
PLG_SYSTEM_WEBAUTHN_FIELD_ATTESTATION_SUPPORT_LABEL="Attestation Support"
PLG_SYSTEM_WEBAUTHN_FIELD_DESC="Lets you manage passwordless login methods using the W3C Web Authentication standard. You need a supported browser and authenticator (eg Google Chrome or Firefox with a FIDO2 certified security key).<br><br><strong>MacOS/iOS/watchOS:</strong> Touch/Face ID.<br><strong>Windows:</strong> Hello (Fingerprint / Facial Recognition / PIN).<br><strong>Android:</strong> Biometric screen lock.<br><br>You can find more details in the <a href=\"https://docs.joomla.org/Special:MyLanguage/WebAuthn_Passwordless_Login\" target=\"_blank\" rel=\"noopener noreferrer\">WebAuthn Passwordless Login documentation</a>."
PLG_SYSTEM_WEBAUTHN_FIELD_LABEL="W3C Web Authentication (WebAuthn) Login"
PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED="%d WebAuthn authenticators already set up: %s"
PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED_0="No WebAuthn authenticator has been set up yet"
PLG_SYSTEM_WEBAUTHN_FIELD_N_AUTHENTICATORS_REGISTERED_1="One WebAuthn authenticator already set up: %2$s"
PLG_SYSTEM_WEBAUTHN_HEADER="W3C Web Authentication (WebAuthn) Login"
PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL="Authenticator added on %s"
PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR="Generic Authenticator"
PLG_SYSTEM_WEBAUTHN_LBL_DEFAULT_AUTHENTICATOR_LABEL="%s added on %s"
PLG_SYSTEM_WEBAUTHN_LOGIN_DESC="Login without a password using the W3C Web Authentication (WebAuthn) standard in compatible browsers. You need to have already set up WebAuthn authentication in your user profile."
PLG_SYSTEM_WEBAUTHN_LOGIN_LABEL="Web Authentication"
PLG_SYSTEM_WEBAUTHN_MANAGE_BTN_ADD_LABEL="Add New Authenticator"
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 12 additions & 10 deletions build/media_source/plg_system_webauthn/js/login.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,8 @@ window.Joomla = window.Joomla || {};
* internal page which handles the login server-side.
*
* @param { Object} publicKey Public key request options, returned from the server
* @param {String} callbackUrl The URL we will use to post back to the server. Must include
* the anti-CSRF token.
*/
const handleLoginChallenge = (publicKey, callbackUrl) => {
const handleLoginChallenge = (publicKey) => {
const arrayToBase64String = (a) => btoa(String.fromCharCode(...a));

const base64url2base64 = (input) => {
Expand Down Expand Up @@ -172,7 +170,8 @@ window.Joomla = window.Joomla || {};
};

// Send the response to your server
window.location = `${callbackUrl}&option=com_ajax&group=system&plugin=webauthn&`
const paths = Joomla.getOptions('system.paths');
window.location = `${paths ? `${paths.base}/index.php` : window.location.pathname}?${Joomla.getOptions('csrf.token')}=1&option=com_ajax&group=system&plugin=webauthn&`
+ `format=raw&akaction=login&encoding=redirect&data=${
btoa(JSON.stringify(publicKeyCredential))}`;
})
Expand All @@ -187,13 +186,11 @@ window.Joomla = window.Joomla || {};
* for the user.
*
* @param {string} formId The login form's or login module's HTML ID
* @param {string} callbackUrl The URL we will use to post back to the server. Must include
* the anti-CSRF token.
*
* @returns {boolean} Always FALSE to prevent BUTTON elements from reloading the page.
*/
// eslint-disable-next-line no-unused-vars
Joomla.plgSystemWebauthnLogin = (formId, callbackUrl) => {
Joomla.plgSystemWebauthnLogin = (formId) => {
// Get the username
const elFormContainer = document.getElementById(formId);
const elUsername = lookForField(elFormContainer, 'input[name=username]');
Expand Down Expand Up @@ -226,9 +223,14 @@ window.Joomla = window.Joomla || {};
username,
returnUrl,
};
postBackData[Joomla.getOptions('csrf.token')] = 1;

const paths = Joomla.getOptions('system.paths');

Joomla.request({
url: callbackUrl,
url: `${paths ? `${paths.base}/index.php` : window.location.pathname}?${Joomla.getOptions(
'csrf.token',
)}=1`,
method: 'POST',
data: interpolateParameters(postBackData),
onSuccess(rawResponse) {
Expand All @@ -243,7 +245,7 @@ window.Joomla = window.Joomla || {};
*/
}

handleLoginChallenge(jsonData, callbackUrl);
handleLoginChallenge(jsonData);
},
onError: (xhr) => {
handleLoginError(`${xhr.status} ${xhr.statusText}`);
Expand All @@ -258,7 +260,7 @@ window.Joomla = window.Joomla || {};
if (loginButtons.length) {
loginButtons.forEach((button) => {
button.addEventListener('click', ({ currentTarget }) => {
Joomla.plgSystemWebauthnLogin(currentTarget.getAttribute('data-webauthn-form'), currentTarget.getAttribute('data-webauthn-url'));
Joomla.plgSystemWebauthnLogin(currentTarget.getAttribute('data-webauthn-form'));
});
});
}
Expand Down
133 changes: 97 additions & 36 deletions build/media_source/plg_system_webauthn/js/management.es6.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,29 +62,52 @@ window.Joomla = window.Joomla || {};
* Posts the credentials to the URL defined in post_url using AJAX.
* That URL must re-render the management interface.
* These contents will replace the element identified by the interface_selector CSS selector.
*
* @param {String} storeID CSS ID for the element storing the configuration in its
* data properties
* @param {String} interfaceSelector CSS selector for the GUI container
*/
// eslint-disable-next-line no-unused-vars
Joomla.plgSystemWebauthnCreateCredentials = (storeID, interfaceSelector) => {
Joomla.plgSystemWebauthnInitCreateCredentials = () => {
// Make sure the browser supports Webauthn
if (!('credentials' in navigator)) {
Joomla.renderMessages({ error: [Joomla.Text._('PLG_SYSTEM_WEBAUTHN_ERR_NO_BROWSER_SUPPORT')] });

return;
}

// Extract the configuration from the store
const elStore = document.getElementById(storeID);
// Get the public key creation options through AJAX.
const paths = Joomla.getOptions('system.paths');
const postURL = `${paths ? `${paths.base}/index.php` : window.location.pathname}`;

if (!elStore) {
return;
}
const postBackData = {
option: 'com_ajax',
group: 'system',
plugin: 'webauthn',
format: 'json',
akaction: 'initcreate',
encoding: 'json',
};
postBackData[Joomla.getOptions('csrf.token')] = 1;

Joomla.request({
url: postURL,
method: 'POST',
data: interpolateParameters(postBackData),
onSuccess(response) {
try {
const publicKey = JSON.parse(response);

Joomla.plgSystemWebauthnCreateCredentials(publicKey);
} catch (exception) {
handleCreationError(Joomla.Text._('PLG_SYSTEM_WEBAUTHN_ERR_XHR_INITCREATE'));
}
},
onError: (xhr) => {
handleCreationError(`${xhr.status} ${xhr.statusText}`);
},
});
};

const publicKey = JSON.parse(atob(elStore.dataset.public_key));
const postURL = atob(elStore.dataset.postback_url);
Joomla.plgSystemWebauthnCreateCredentials = (publicKey) => {
const paths = Joomla.getOptions('system.paths');
const postURL = `${paths ? `${paths.base}/index.php` : window.location.pathname}`;

const arrayToBase64String = (a) => btoa(String.fromCharCode(...a));

Expand Down Expand Up @@ -137,13 +160,14 @@ window.Joomla = window.Joomla || {};
encoding: 'raw',
data: btoa(JSON.stringify(publicKeyCredential)),
};
postBackData[Joomla.getOptions('csrf.token')] = 1;

Joomla.request({
url: postURL,
method: 'POST',
data: interpolateParameters(postBackData),
onSuccess(responseHTML) {
const elements = document.querySelectorAll(interfaceSelector);
const elements = document.querySelectorAll('#plg_system_webauthn-management-interface');

if (!elements) {
return;
Expand All @@ -154,6 +178,7 @@ window.Joomla = window.Joomla || {};
elContainer.outerHTML = responseHTML;

Joomla.plgSystemWebauthnInitialize();
Joomla.plgSystemWebauthnReactivateTooltips();
},
onError: (xhr) => {
handleCreationError(`${xhr.status} ${xhr.statusText}`);
Expand All @@ -175,15 +200,9 @@ window.Joomla = window.Joomla || {};
* properties
*/
// eslint-disable-next-line no-unused-vars
Joomla.plgSystemWebauthnEditLabel = (that, storeID) => {
// Extract the configuration from the store
const elStore = document.getElementById(storeID);

if (!elStore) {
return false;
}

const postURL = atob(elStore.dataset.postback_url);
Joomla.plgSystemWebauthnEditLabel = (that) => {
const paths = Joomla.getOptions('system.paths');
const postURL = `${paths ? `${paths.base}/index.php` : window.location.pathname}`;

// Find the UI elements
const elTR = that.parentElement.parentElement;
Expand All @@ -198,10 +217,14 @@ window.Joomla = window.Joomla || {};
// Show the editor
const oldLabel = elLabelTD.innerText;

const elContainer = document.createElement('div');
elContainer.className = 'webauthnManagementEditorRow d-flex gap-2';

const elInput = document.createElement('input');
elInput.type = 'text';
elInput.name = 'label';
elInput.defaultValue = oldLabel;
elInput.className = 'form-control';

const elSave = document.createElement('button');
elSave.className = 'btn btn-success btn-sm';
Expand All @@ -220,6 +243,7 @@ window.Joomla = window.Joomla || {};
credential_id: credentialId,
new_label: elNewLabel,
};
postBackData[Joomla.getOptions('csrf.token')] = 1;

Joomla.request({
url: postURL,
Expand Down Expand Up @@ -268,9 +292,10 @@ window.Joomla = window.Joomla || {};
}, false);

elLabelTD.innerHTML = '';
elLabelTD.appendChild(elInput);
elLabelTD.appendChild(elSave);
elLabelTD.appendChild(elCancel);
elContainer.appendChild(elInput);
elContainer.appendChild(elSave);
elContainer.appendChild(elCancel);
elLabelTD.appendChild(elContainer);
elEdit.disabled = true;
elDelete.disabled = true;

Expand All @@ -281,19 +306,15 @@ window.Joomla = window.Joomla || {};
* Delete button
*
* @param {Element} that The button being clicked
* @param {String} storeID CSS ID for the element storing the configuration in its data
* properties
*/
// eslint-disable-next-line no-unused-vars
Joomla.plgSystemWebauthnDelete = (that, storeID) => {
// Extract the configuration from the store
const elStore = document.getElementById(storeID);

if (!elStore) {
Joomla.plgSystemWebauthnDelete = (that) => {
if (!window.confirm(Joomla.Text._('JGLOBAL_CONFIRM_DELETE'))) {
return false;
}

const postURL = atob(elStore.dataset.postback_url);
const paths = Joomla.getOptions('system.paths');
const postURL = `${paths ? `${paths.base}/index.php` : window.location.pathname}`;

// Find the UI elements
const elTR = that.parentElement.parentElement;
Expand All @@ -317,6 +338,7 @@ window.Joomla = window.Joomla || {};
akaction: 'delete',
credential_id: credentialId,
};
postBackData[Joomla.getOptions('csrf.token')] = 1;

Joomla.request({
url: postURL,
Expand Down Expand Up @@ -354,6 +376,45 @@ window.Joomla = window.Joomla || {};
return false;
};

Joomla.plgSystemWebauthnReactivateTooltips = () => {
const tooltips = Joomla.getOptions('bootstrap.tooltip');
if (typeof tooltips === 'object' && tooltips !== null) {
Object.keys(tooltips).forEach((tooltip) => {
const opt = tooltips[tooltip];
const options = {
animation: opt.animation ? opt.animation : true,
container: opt.container ? opt.container : false,
delay: opt.delay ? opt.delay : 0,
html: opt.html ? opt.html : false,
selector: opt.selector ? opt.selector : false,
trigger: opt.trigger ? opt.trigger : 'hover focus',
fallbackPlacement: opt.fallbackPlacement ? opt.fallbackPlacement : null,
boundary: opt.boundary ? opt.boundary : 'clippingParents',
title: opt.title ? opt.title : '',
customClass: opt.customClass ? opt.customClass : '',
sanitize: opt.sanitize ? opt.sanitize : true,
sanitizeFn: opt.sanitizeFn ? opt.sanitizeFn : null,
popperConfig: opt.popperConfig ? opt.popperConfig : null,
};

if (opt.placement) {
options.placement = opt.placement;
}
if (opt.template) {
options.template = opt.template;
}
if (opt.allowList) {
options.allowList = opt.allowList;
}

const elements = Array.from(document.querySelectorAll(tooltip));
if (elements.length) {
elements.map((el) => new window.bootstrap.Tooltip(el, options));
}
});
}
};

/**
* Add New Authenticator button click handler
*
Expand All @@ -364,7 +425,7 @@ window.Joomla = window.Joomla || {};
Joomla.plgSystemWebauthnAddOnClick = (event) => {
event.preventDefault();

Joomla.plgSystemWebauthnCreateCredentials(event.currentTarget.getAttribute('data-random-id'), '#plg_system_webauthn-management-interface');
Joomla.plgSystemWebauthnInitCreateCredentials();

return false;
};
Expand All @@ -379,7 +440,7 @@ window.Joomla = window.Joomla || {};
Joomla.plgSystemWebauthnEditOnClick = (event) => {
event.preventDefault();

Joomla.plgSystemWebauthnEditLabel(event.currentTarget, event.currentTarget.getAttribute('data-random-id'));
Joomla.plgSystemWebauthnEditLabel(event.currentTarget);

return false;
};
Expand All @@ -394,7 +455,7 @@ window.Joomla = window.Joomla || {};
Joomla.plgSystemWebauthnDeleteOnClick = (event) => {
event.preventDefault();

Joomla.plgSystemWebauthnDelete(event.currentTarget, event.currentTarget.getAttribute('data-random-id'));
Joomla.plgSystemWebauthnDelete(event.currentTarget);

return false;
};
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@
"web-auth/webauthn-lib": "2.1.*",
"composer/ca-bundle": "^1.2",
"dragonmantank/cron-expression": "^3.1",
"enshrined/svg-sanitize": "^0.15.4"
"enshrined/svg-sanitize": "^0.15.4",
"lcobucci/jwt": "^3.4.6",
"web-token/signature-pack": "^2.2.11"
},
"require-dev": {
"phpunit/phpunit": "^8.5",
Expand Down
Loading