There were several issues with the WebAuthn registration and testing code and the style was very old javascript with jquery callbacks. This PR uses async and fetch to replace the JQuery code. Ref #22651 Signed-off-by: Andrew Thornton <art27@cantab.net> --------- Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: delvh <dev.lh@web.de> Co-authored-by: silverwind <me@silverwind.io>
217 lines
6.2 KiB
JavaScript
217 lines
6.2 KiB
JavaScript
import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';
|
|
import {showElem, hideElem} from '../utils/dom.js';
|
|
|
|
const {appSubUrl, csrfToken} = window.config;
|
|
|
|
export async function initUserAuthWebAuthn() {
|
|
hideElem('#webauthn-error');
|
|
|
|
const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
|
|
if (!elPrompt) {
|
|
return;
|
|
}
|
|
|
|
if (!detectWebAuthnSupport()) {
|
|
return;
|
|
}
|
|
|
|
const res = await fetch(`${appSubUrl}/user/webauthn/assertion`);
|
|
if (res.status !== 200) {
|
|
webAuthnError('unknown');
|
|
return;
|
|
}
|
|
const options = await res.json();
|
|
options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
|
|
for (const cred of options.publicKey.allowCredentials) {
|
|
cred.id = decodeURLEncodedBase64(cred.id);
|
|
}
|
|
const credential = await navigator.credentials.get({
|
|
publicKey: options.publicKey
|
|
});
|
|
try {
|
|
await verifyAssertion(credential);
|
|
} catch (err) {
|
|
if (!options.publicKey.extensions?.appid) {
|
|
webAuthnError('general', err.message);
|
|
return;
|
|
}
|
|
delete options.publicKey.extensions.appid;
|
|
const credential = await navigator.credentials.get({
|
|
publicKey: options.publicKey
|
|
});
|
|
try {
|
|
await verifyAssertion(credential);
|
|
} catch (err) {
|
|
webAuthnError('general', err.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function verifyAssertion(assertedCredential) {
|
|
// Move data into Arrays incase it is super long
|
|
const authData = new Uint8Array(assertedCredential.response.authenticatorData);
|
|
const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
|
|
const rawId = new Uint8Array(assertedCredential.rawId);
|
|
const sig = new Uint8Array(assertedCredential.response.signature);
|
|
const userHandle = new Uint8Array(assertedCredential.response.userHandle);
|
|
|
|
const res = await fetch(`${appSubUrl}/user/webauthn/assertion`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json; charset=utf-8'
|
|
},
|
|
body: JSON.stringify({
|
|
id: assertedCredential.id,
|
|
rawId: encodeURLEncodedBase64(rawId),
|
|
type: assertedCredential.type,
|
|
clientExtensionResults: assertedCredential.getClientExtensionResults(),
|
|
response: {
|
|
authenticatorData: encodeURLEncodedBase64(authData),
|
|
clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
|
|
signature: encodeURLEncodedBase64(sig),
|
|
userHandle: encodeURLEncodedBase64(userHandle),
|
|
},
|
|
}),
|
|
});
|
|
if (res.status === 500) {
|
|
webAuthnError('unknown');
|
|
return;
|
|
} else if (res.status !== 200) {
|
|
webAuthnError('unable-to-process');
|
|
return;
|
|
}
|
|
const reply = await res.json();
|
|
|
|
window.location.href = reply?.redirect ?? `${appSubUrl}/`;
|
|
}
|
|
|
|
async function webauthnRegistered(newCredential) {
|
|
const attestationObject = new Uint8Array(newCredential.response.attestationObject);
|
|
const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
|
|
const rawId = new Uint8Array(newCredential.rawId);
|
|
|
|
const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/register`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Csrf-Token': csrfToken,
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
},
|
|
body: JSON.stringify({
|
|
id: newCredential.id,
|
|
rawId: encodeURLEncodedBase64(rawId),
|
|
type: newCredential.type,
|
|
response: {
|
|
attestationObject: encodeURLEncodedBase64(attestationObject),
|
|
clientDataJSON: encodeURLEncodedBase64(clientDataJSON),
|
|
},
|
|
}),
|
|
});
|
|
|
|
if (res.status === 409) {
|
|
webAuthnError('duplicated');
|
|
return;
|
|
} else if (res.status !== 201) {
|
|
webAuthnError('unknown');
|
|
return;
|
|
}
|
|
|
|
window.location.reload();
|
|
}
|
|
|
|
function webAuthnError(errorType, message) {
|
|
const elErrorMsg = document.getElementById(`webauthn-error-msg`);
|
|
|
|
if (errorType === 'general') {
|
|
elErrorMsg.textContent = message || 'unknown error';
|
|
} else {
|
|
const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${errorType}]`);
|
|
if (elTypedError) {
|
|
elErrorMsg.textContent = `${elTypedError.textContent}${message ? ` ${message}` : ''}`;
|
|
} else {
|
|
elErrorMsg.textContent = `unknown error type: ${errorType}${message ? ` ${message}` : ''}`;
|
|
}
|
|
}
|
|
|
|
showElem('#webauthn-error');
|
|
}
|
|
|
|
function detectWebAuthnSupport() {
|
|
if (!window.isSecureContext) {
|
|
document.getElementById('register-button').disabled = true;
|
|
document.getElementById('login-button').disabled = true;
|
|
webAuthnError('insecure');
|
|
return false;
|
|
}
|
|
|
|
if (typeof window.PublicKeyCredential !== 'function') {
|
|
document.getElementById('register-button').disabled = true;
|
|
document.getElementById('login-button').disabled = true;
|
|
webAuthnError('browser');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
export function initUserAuthWebAuthnRegister() {
|
|
const elRegister = document.getElementById('register-webauthn');
|
|
if (!elRegister) {
|
|
return;
|
|
}
|
|
|
|
hideElem('#webauthn-error');
|
|
|
|
elRegister.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
if (!detectWebAuthnSupport()) {
|
|
return;
|
|
}
|
|
webAuthnRegisterRequest();
|
|
});
|
|
}
|
|
|
|
async function webAuthnRegisterRequest() {
|
|
const elNickname = document.getElementById('nickname');
|
|
|
|
const body = new FormData();
|
|
body.append('name', elNickname.value);
|
|
|
|
const res = await fetch(`${appSubUrl}/user/settings/security/webauthn/request_register`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-Csrf-Token': csrfToken,
|
|
},
|
|
body,
|
|
});
|
|
|
|
if (res.status === 409) {
|
|
webAuthnError('duplicated');
|
|
return;
|
|
} else if (res.status !== 200) {
|
|
webAuthnError('unknown');
|
|
return;
|
|
}
|
|
|
|
const options = await res.json();
|
|
elNickname.closest('div.field').classList.remove('error');
|
|
|
|
options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge);
|
|
options.publicKey.user.id = decodeURLEncodedBase64(options.publicKey.user.id);
|
|
if (options.publicKey.excludeCredentials) {
|
|
for (const cred of options.publicKey.excludeCredentials) {
|
|
cred.id = decodeURLEncodedBase64(cred.id);
|
|
}
|
|
}
|
|
|
|
let credential;
|
|
try {
|
|
credential = await navigator.credentials.create({
|
|
publicKey: options.publicKey
|
|
});
|
|
} catch (err) {
|
|
webAuthnError('unknown', err);
|
|
return;
|
|
}
|
|
|
|
webauthnRegistered(credential);
|
|
}
|