diff options
Diffstat (limited to 'web_src/js/features/user-auth-webauthn.js')
-rw-r--r-- | web_src/js/features/user-auth-webauthn.js | 263 |
1 files changed, 129 insertions, 134 deletions
diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js index f30017afc6..8085313d22 100644 --- a/web_src/js/features/user-auth-webauthn.js +++ b/web_src/js/features/user-auth-webauthn.js @@ -1,11 +1,13 @@ -import $ from 'jquery'; -import {encode, decode} from 'uint8-to-base64'; -import {hideElem, showElem} from '../utils/dom.js'; +import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js'; +import {showElem, hideElem} from '../utils/dom.js'; const {appSubUrl, csrfToken} = window.config; -export function initUserAuthWebAuthn() { - if ($('.user.signin.webauthn-prompt').length === 0) { +export async function initUserAuthWebAuthn() { + hideElem('#webauthn-error'); + + const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); + if (!elPrompt) { return; } @@ -13,49 +15,52 @@ export function initUserAuthWebAuthn() { return; } - $.getJSON(`${appSubUrl}/user/webauthn/assertion`, {}) - .done((makeAssertionOptions) => { - makeAssertionOptions.publicKey.challenge = decodeURLEncodedBase64(makeAssertionOptions.publicKey.challenge); - for (let i = 0; i < makeAssertionOptions.publicKey.allowCredentials.length; i++) { - makeAssertionOptions.publicKey.allowCredentials[i].id = decodeURLEncodedBase64(makeAssertionOptions.publicKey.allowCredentials[i].id); - } - navigator.credentials.get({ - publicKey: makeAssertionOptions.publicKey - }) - .then((credential) => { - verifyAssertion(credential); - }).catch((err) => { - // Try again... without the appid - if (makeAssertionOptions.publicKey.extensions && makeAssertionOptions.publicKey.extensions.appid) { - delete makeAssertionOptions.publicKey.extensions['appid']; - navigator.credentials.get({ - publicKey: makeAssertionOptions.publicKey - }) - .then((credential) => { - verifyAssertion(credential); - }).catch((err) => { - webAuthnError('general', err.message); - }); - return; - } - webAuthnError('general', err.message); - }); - }).fail(() => { - webAuthnError('unknown'); + 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); + } + } } -function verifyAssertion(assertedCredential) { +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); - $.ajax({ - url: `${appSubUrl}/user/webauthn/assertion`, - type: 'POST', - data: JSON.stringify({ + + 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, @@ -67,50 +72,31 @@ function verifyAssertion(assertedCredential) { userHandle: encodeURLEncodedBase64(userHandle), }, }), - contentType: 'application/json; charset=utf-8', - dataType: 'json', - success: (resp) => { - if (resp && resp['redirect']) { - window.location.href = resp['redirect']; - } else { - window.location.href = '/'; - } - }, - error: (xhr) => { - if (xhr.status === 500) { - webAuthnError('unknown'); - return; - } - webAuthnError('unable-to-process'); - } }); -} - -// Encode an ArrayBuffer into a URLEncoded base64 string. -function encodeURLEncodedBase64(value) { - return encode(value) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); -} + if (res.status === 500) { + webAuthnError('unknown'); + return; + } else if (res.status !== 200) { + webAuthnError('unable-to-process'); + return; + } + const reply = await res.json(); -// Dccode a URLEncoded base64 to an ArrayBuffer string. -function decodeURLEncodedBase64(value) { - return decode(value - .replace(/_/g, '/') - .replace(/-/g, '+')); + window.location.href = reply?.redirect ?? `${appSubUrl}/`; } -function webauthnRegistered(newCredential) { +async function webauthnRegistered(newCredential) { const attestationObject = new Uint8Array(newCredential.response.attestationObject); const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); const rawId = new Uint8Array(newCredential.rawId); - return $.ajax({ - url: `${appSubUrl}/user/settings/security/webauthn/register`, - type: 'POST', - headers: {'X-Csrf-Token': csrfToken}, - data: JSON.stringify({ + 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, @@ -119,48 +105,47 @@ function webauthnRegistered(newCredential) { clientDataJSON: encodeURLEncodedBase64(clientDataJSON), }, }), - dataType: 'json', - contentType: 'application/json; charset=utf-8', - }).then(() => { - window.location.reload(); - }).fail((xhr) => { - if (xhr.status === 409) { - webAuthnError('duplicated'); - return; - } - webAuthnError('unknown'); }); + + if (res.status === 409) { + webAuthnError('duplicated'); + return; + } else if (res.status !== 201) { + webAuthnError('unknown'); + return; + } + + window.location.reload(); } function webAuthnError(errorType, message) { - hideElem($('#webauthn-error [data-webauthn-error-msg]')); - const $errorGeneral = $(`#webauthn-error [data-webauthn-error-msg=general]`); + const elErrorMsg = document.getElementById(`webauthn-error-msg`); + if (errorType === 'general') { - showElem($errorGeneral); - $errorGeneral.text(message || 'unknown error'); + elErrorMsg.textContent = message || 'unknown error'; } else { - const $errorTyped = $(`#webauthn-error [data-webauthn-error-msg=${errorType}]`); - if ($errorTyped.length) { - showElem($errorTyped); + const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${errorType}]`); + if (elTypedError) { + elErrorMsg.textContent = `${elTypedError.textContent}${message ? ` ${message}` : ''}`; } else { - showElem($errorGeneral); - $errorGeneral.text(`unknown error type: ${errorType}`); + elErrorMsg.textContent = `unknown error type: ${errorType}${message ? ` ${message}` : ''}`; } } - $('#webauthn-error').modal('show'); + + showElem('#webauthn-error'); } function detectWebAuthnSupport() { if (!window.isSecureContext) { - $('#register-button').prop('disabled', true); - $('#login-button').prop('disabled', true); + document.getElementById('register-button').disabled = true; + document.getElementById('login-button').disabled = true; webAuthnError('insecure'); return false; } if (typeof window.PublicKeyCredential !== 'function') { - $('#register-button').prop('disabled', true); - $('#login-button').prop('disabled', true); + document.getElementById('register-button').disabled = true; + document.getElementById('login-button').disabled = true; webAuthnError('browser'); return false; } @@ -169,12 +154,14 @@ function detectWebAuthnSupport() { } export function initUserAuthWebAuthnRegister() { - if ($('#register-webauthn').length === 0) { + const elRegister = document.getElementById('register-webauthn'); + if (!elRegister) { return; } - $('#webauthn-error').modal({allowMultiple: false}); - $('#register-webauthn').on('click', (e) => { + hideElem('#webauthn-error'); + + elRegister.addEventListener('click', (e) => { e.preventDefault(); if (!detectWebAuthnSupport()) { return; @@ -183,40 +170,48 @@ export function initUserAuthWebAuthnRegister() { }); } -function webAuthnRegisterRequest() { - if ($('#nickname').val() === '') { - webAuthnError('empty'); +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; } - $.post(`${appSubUrl}/user/settings/security/webauthn/request_register`, { - _csrf: csrfToken, - name: $('#nickname').val(), - }).done((makeCredentialOptions) => { - $('#nickname').closest('div.field').removeClass('error'); - - makeCredentialOptions.publicKey.challenge = decodeURLEncodedBase64(makeCredentialOptions.publicKey.challenge); - makeCredentialOptions.publicKey.user.id = decodeURLEncodedBase64(makeCredentialOptions.publicKey.user.id); - if (makeCredentialOptions.publicKey.excludeCredentials) { - for (let i = 0; i < makeCredentialOptions.publicKey.excludeCredentials.length; i++) { - makeCredentialOptions.publicKey.excludeCredentials[i].id = decodeURLEncodedBase64(makeCredentialOptions.publicKey.excludeCredentials[i].id); - } - } - navigator.credentials.create({ - publicKey: makeCredentialOptions.publicKey - }).then(webauthnRegistered) - .catch((err) => { - if (!err) { - webAuthnError('unknown'); - return; - } - webAuthnError('general', err.message); - }); - }).fail((xhr) => { - if (xhr.status === 409) { - webAuthnError('duplicated'); - 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); } - webAuthnError('unknown'); - }); + } + + let credential; + try { + credential = await navigator.credentials.create({ + publicKey: options.publicKey + }); + } catch (err) { + webAuthnError('unknown', err); + return; + } + + webauthnRegistered(credential); } |