diff options
author | Lunny Xiao <xiaolunwen@gmail.com> | 2022-01-14 23:03:31 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-14 16:03:31 +0100 |
commit | 35c3553870e35b2e7cfcc599645791acda6afcef (patch) | |
tree | 0ad600c2d1cd94ef12566482832768c9efcf8a69 /web_src | |
parent | 8808293247bebd20482c3c625c64937174503781 (diff) | |
download | gitea-35c3553870e35b2e7cfcc599645791acda6afcef.tar.gz gitea-35c3553870e35b2e7cfcc599645791acda6afcef.zip |
Support webauthn (#17957)
Migrate from U2F to Webauthn
Co-authored-by: Andrew Thornton <art27@cantab.net>
Co-authored-by: 6543 <6543@obermui.de>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Diffstat (limited to 'web_src')
-rw-r--r-- | web_src/js/features/user-auth-u2f.js | 125 | ||||
-rw-r--r-- | web_src/js/features/user-auth-webauthn.js | 197 | ||||
-rw-r--r-- | web_src/js/index.js | 6 | ||||
-rw-r--r-- | web_src/less/_form.less | 4 |
4 files changed, 204 insertions, 128 deletions
diff --git a/web_src/js/features/user-auth-u2f.js b/web_src/js/features/user-auth-u2f.js deleted file mode 100644 index 30876a1d03..0000000000 --- a/web_src/js/features/user-auth-u2f.js +++ /dev/null @@ -1,125 +0,0 @@ -const {appSubUrl, csrfToken} = window.config; - -export function initUserAuthU2fAuth() { - if ($('#wait-for-key').length === 0) { - return; - } - u2fApi.ensureSupport().then(() => { - $.getJSON(`${appSubUrl}/user/u2f/challenge`).done((req) => { - u2fApi.sign(req.appId, req.challenge, req.registeredKeys, 30) - .then(u2fSigned) - .catch((err) => { - if (err === undefined) { - u2fError(1); - return; - } - u2fError(err.metaData.code); - }); - }); - }).catch(() => { - // Fallback in case browser do not support U2F - window.location.href = `${appSubUrl}/user/two_factor`; - }); -} - -function u2fSigned(resp) { - $.ajax({ - url: `${appSubUrl}/user/u2f/sign`, - type: 'POST', - headers: {'X-Csrf-Token': csrfToken}, - data: JSON.stringify(resp), - contentType: 'application/json; charset=utf-8', - }).done((res) => { - window.location.replace(res); - }).fail(() => { - u2fError(1); - }); -} - -function u2fRegistered(resp) { - if (checkError(resp)) { - return; - } - $.ajax({ - url: `${appSubUrl}/user/settings/security/u2f/register`, - type: 'POST', - headers: {'X-Csrf-Token': csrfToken}, - data: JSON.stringify(resp), - contentType: 'application/json; charset=utf-8', - success() { - window.location.reload(); - }, - fail() { - u2fError(1); - } - }); -} - -function checkError(resp) { - if (!('errorCode' in resp)) { - return false; - } - if (resp.errorCode === 0) { - return false; - } - u2fError(resp.errorCode); - return true; -} - -function u2fError(errorType) { - const u2fErrors = { - browser: $('#unsupported-browser'), - 1: $('#u2f-error-1'), - 2: $('#u2f-error-2'), - 3: $('#u2f-error-3'), - 4: $('#u2f-error-4'), - 5: $('.u2f_error_5') - }; - u2fErrors[errorType].removeClass('hide'); - - for (const type of Object.keys(u2fErrors)) { - if (type !== `${errorType}`) { - u2fErrors[type].addClass('hide'); - } - } - $('#u2f-error').modal('show'); -} - -export function initUserAuthU2fRegister() { - $('#register-device').modal({allowMultiple: false}); - $('#u2f-error').modal({allowMultiple: false}); - $('#register-security-key').on('click', (e) => { - e.preventDefault(); - u2fApi.ensureSupport() - .then(u2fRegisterRequest) - .catch(() => { - u2fError('browser'); - }); - }); -} - -function u2fRegisterRequest() { - $.post(`${appSubUrl}/user/settings/security/u2f/request_register`, { - _csrf: csrfToken, - name: $('#nickname').val() - }).done((req) => { - $('#nickname').closest('div.field').removeClass('error'); - $('#register-device').modal('show'); - if (req.registeredKeys === null) { - req.registeredKeys = []; - } - u2fApi.register(req.appId, req.registerRequests, req.registeredKeys, 30) - .then(u2fRegistered) - .catch((reason) => { - if (reason === undefined) { - u2fError(1); - return; - } - u2fError(reason.metaData.code); - }); - }).fail((xhr) => { - if (xhr.status === 409) { - $('#nickname').closest('div.field').addClass('error'); - } - }); -} diff --git a/web_src/js/features/user-auth-webauthn.js b/web_src/js/features/user-auth-webauthn.js new file mode 100644 index 0000000000..5b580e7949 --- /dev/null +++ b/web_src/js/features/user-auth-webauthn.js @@ -0,0 +1,197 @@ +import {encode, decode} from 'uint8-to-base64'; + +const {appSubUrl, csrfToken} = window.config; + + +export function initUserAuthWebAuthn() { + if ($('.user.signin.webauthn-prompt').length === 0) { + return; + } + + if (!detectWebAuthnSupport()) { + return; + } + + $.getJSON(`${appSubUrl}/user/webauthn/assertion`, {}) + .done((makeAssertionOptions) => { + makeAssertionOptions.publicKey.challenge = decode(makeAssertionOptions.publicKey.challenge); + for (let i = 0; i < makeAssertionOptions.publicKey.allowCredentials.length; i++) { + makeAssertionOptions.publicKey.allowCredentials[i].id = decode(makeAssertionOptions.publicKey.allowCredentials[i].id); + } + navigator.credentials.get({ + publicKey: makeAssertionOptions.publicKey + }) + .then((credential) => { + verifyAssertion(credential); + }).catch((err) => { + webAuthnError(0, err.message); + }); + }).fail(() => { + webAuthnError('unknown'); + }); +} + +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({ + id: assertedCredential.id, + rawId: bufferEncode(rawId), + type: assertedCredential.type, + clientExtensionResults: assertedCredential.getClientExtensionResults(), + response: { + authenticatorData: bufferEncode(authData), + clientDataJSON: bufferEncode(clientDataJSON), + signature: bufferEncode(sig), + userHandle: bufferEncode(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 base64 string. +function bufferEncode(value) { + return encode(value) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +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({ + id: newCredential.id, + rawId: bufferEncode(rawId), + type: newCredential.type, + response: { + attestationObject: bufferEncode(attestationObject), + clientDataJSON: bufferEncode(clientDataJSON), + }, + }), + dataType: 'json', + contentType: 'application/json; charset=utf-8', + }).then(() => { + window.location.reload(); + }).fail((xhr) => { + if (xhr.status === 409) { + webAuthnError('duplicated'); + return; + } + webAuthnError('unknown'); + }); +} + +function webAuthnError(errorType, message) { + $('#webauthn-error [data-webauthn-error-msg]').hide(); + if (errorType === 0 && message && message.length > 1) { + $(`#webauthn-error [data-webauthn-error-msg=0]`).text(message); + $(`#webauthn-error [data-webauthn-error-msg=0]`).show(); + } else { + $(`#webauthn-error [data-webauthn-error-msg=${errorType}]`).show(); + } + $('#webauthn-error').modal('show'); +} + +function detectWebAuthnSupport() { + if (!window.isSecureContext) { + $('#register-button').prop('disabled', true); + $('#login-button').prop('disabled', true); + webAuthnError('insecure'); + return false; + } + + if (typeof window.PublicKeyCredential !== 'function') { + $('#register-button').prop('disabled', true); + $('#login-button').prop('disabled', true); + webAuthnError('browser'); + return false; + } + + return true; +} + +export function initUserAuthWebAuthnRegister() { + if ($('#register-webauthn').length === 0) { + return; + } + + if (!detectWebAuthnSupport()) { + return; + } + + $('#register-device').modal({allowMultiple: false}); + $('#webauthn-error').modal({allowMultiple: false}); + $('#register-webauthn').on('click', (e) => { + e.preventDefault(); + webAuthnRegisterRequest(); + }); +} + +function webAuthnRegisterRequest() { + if ($('#nickname').val() === '') { + webAuthnError('empty'); + return; + } + $.post(`${appSubUrl}/user/settings/security/webauthn/request_register`, { + _csrf: csrfToken, + name: $('#nickname').val(), + }).done((makeCredentialOptions) => { + $('#nickname').closest('div.field').removeClass('error'); + $('#register-device').modal('show'); + + makeCredentialOptions.publicKey.challenge = decode(makeCredentialOptions.publicKey.challenge); + makeCredentialOptions.publicKey.user.id = decode(makeCredentialOptions.publicKey.user.id); + if (makeCredentialOptions.publicKey.excludeCredentials) { + for (let i = 0; i < makeCredentialOptions.publicKey.excludeCredentials.length; i++) { + makeCredentialOptions.publicKey.excludeCredentials[i].id = decode(makeCredentialOptions.publicKey.excludeCredentials[i].id); + } + } + + navigator.credentials.create({ + publicKey: makeCredentialOptions.publicKey + }).then(webauthnRegistered) + .catch((err) => { + if (!err) { + webAuthnError('unknown'); + return; + } + webAuthnError(0, err); + }); + }).fail((xhr) => { + if (xhr.status === 409) { + webAuthnError('duplicated'); + return; + } + webAuthnError('unknown'); + }); +} diff --git a/web_src/js/index.js b/web_src/js/index.js index c9bf197a35..8ea30f1fca 100644 --- a/web_src/js/index.js +++ b/web_src/js/index.js @@ -63,7 +63,7 @@ import { initRepoSettingSearchTeamBox, } from './features/repo-settings.js'; import {initOrgTeamSearchRepoBox, initOrgTeamSettings} from './features/org-team.js'; -import {initUserAuthU2fAuth, initUserAuthU2fRegister} from './features/user-auth-u2f.js'; +import {initUserAuthWebAuthn, initUserAuthWebAuthnRegister} from './features/user-auth-webauthn.js'; import {initRepoRelease, initRepoReleaseEditor} from './features/repo-release.js'; import {initRepoEditor} from './features/repo-editor.js'; import {initCompSearchUserBox} from './features/comp/SearchUserBox.js'; @@ -163,7 +163,7 @@ $(document).ready(() => { initUserAuthLinkAccountView(); initUserAuthOauth2(); - initUserAuthU2fAuth(); - initUserAuthU2fRegister(); + initUserAuthWebAuthn(); + initUserAuthWebAuthnRegister(); initUserSettings(); }); diff --git a/web_src/less/_form.less b/web_src/less/_form.less index 1a92b8e369..99aec18f48 100644 --- a/web_src/less/_form.less +++ b/web_src/less/_form.less @@ -276,6 +276,10 @@ textarea:focus, } } +.user.signin.webauthn-prompt { + margin-top: 15px; +} + .repository { &.new.repo, &.new.migrate, |