diff options
author | zeripath <art27@cantab.net> | 2023-06-06 06:29:37 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-06 13:29:37 +0800 |
commit | 036fb7861fb8fb12f1dcabbd8edab9b322b7c96c (patch) | |
tree | 53866bb0ed5cdfc8b1c000871e4f2d6e3119cc67 /web_src/js/features/user-auth-webauthn.js | |
parent | c09f747b516f52b16f3d699e5f115789600a432a (diff) | |
download | gitea-036fb7861fb8fb12f1dcabbd8edab9b322b7c96c.tar.gz gitea-036fb7861fb8fb12f1dcabbd8edab9b322b7c96c.zip |
Clean up WebAuthn javascript code and remove JQuery code (#22697)
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>
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); } |