diff options
Diffstat (limited to 'web_src')
-rw-r--r-- | web_src/css/base.css | 22 | ||||
-rw-r--r-- | web_src/css/repo.css | 5 | ||||
-rw-r--r-- | web_src/js/features/user-auth-webauthn.js | 263 | ||||
-rw-r--r-- | web_src/js/utils.js | 16 | ||||
-rw-r--r-- | web_src/js/utils.test.js | 8 |
5 files changed, 174 insertions, 140 deletions
diff --git a/web_src/css/base.css b/web_src/css/base.css index 49bdfed1b7..b352e6d98e 100644 --- a/web_src/css/base.css +++ b/web_src/css/base.css @@ -699,6 +699,11 @@ a.label, border: 1px solid var(--color-secondary); } +.ui.info.message .header, +.ui.blue.message .header { + color: var(--color-blue); +} + .ui.info.message, .ui.attached.info.message, .ui.blue.message, @@ -708,6 +713,12 @@ a.label, border-color: var(--color-info-border); } +.ui.success.message .header, +.ui.positive.message .header, +.ui.green.message .header { + color: var(--color-green); +} + .ui.success.message, .ui.attached.success.message, .ui.positive.message, @@ -717,6 +728,12 @@ a.label, border-color: var(--color-success-border); } +.ui.error.message .header, +.ui.negative.message .header, +.ui.red.message .header { + color: var(--color-red); +} + .ui.error.message, .ui.attached.error.message, .ui.red.message, @@ -728,6 +745,11 @@ a.label, border-color: var(--color-error-border); } +.ui.warning.message .header, +.ui.yellow.message .header { + color: var(--color-yellow); +} + .ui.warning.message, .ui.attached.warning.message, .ui.yellow.message, diff --git a/web_src/css/repo.css b/web_src/css/repo.css index b2b544a7ea..4e300be878 100644 --- a/web_src/css/repo.css +++ b/web_src/css/repo.css @@ -2405,11 +2405,6 @@ padding-bottom: 0 !important; } -.settings .content > .header, -.settings .content .segment { - box-shadow: 0 1px 2px 0 var(--color-box-header); -} - .settings.webhooks .list > .item:not(:first-child), .settings.githooks .list > .item:not(:first-child), .settings.actions .list > .item:not(:first-child) { 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); } diff --git a/web_src/js/utils.js b/web_src/js/utils.js index 6bee4f0836..4f9ad452f6 100644 --- a/web_src/js/utils.js +++ b/web_src/js/utils.js @@ -1,3 +1,5 @@ +import {encode, decode} from 'uint8-to-base64'; + // transform /path/to/file.ext to file.ext export function basename(path = '') { return path ? path.replace(/^.*\//, '') : ''; @@ -135,3 +137,17 @@ export function toAbsoluteUrl(url) { return `${window.location.origin}${url}`; } +// Encode an ArrayBuffer into a URLEncoded base64 string. +export function encodeURLEncodedBase64(arrayBuffer) { + return encode(arrayBuffer) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +// Decode a URLEncoded base64 to an ArrayBuffer string. +export function decodeURLEncodedBase64(base64url) { + return decode(base64url + .replace(/_/g, '/') + .replace(/-/g, '+')); +} diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js index 2f9e5fb47d..cf73b63b99 100644 --- a/web_src/js/utils.test.js +++ b/web_src/js/utils.test.js @@ -2,7 +2,7 @@ import {expect, test} from 'vitest'; import { basename, extname, isObject, stripTags, joinPaths, parseIssueHref, parseUrl, translateMonth, translateDay, blobToDataURI, - toAbsoluteUrl, + toAbsoluteUrl, encodeURLEncodedBase64, decodeURLEncodedBase64, } from './utils.js'; test('basename', () => { @@ -132,3 +132,9 @@ test('toAbsoluteUrl', () => { expect(() => toAbsoluteUrl('path')).toThrowError('unsupported'); }); + +test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { + expect(encodeURLEncodedBase64(decodeURLEncodedBase64('foo'))).toEqual('foo'); // No = padding + expect(encodeURLEncodedBase64(decodeURLEncodedBase64('a-minus'))).toEqual('a-minus'); + expect(encodeURLEncodedBase64(decodeURLEncodedBase64('_underscorc'))).toEqual('_underscorc'); +}); |