summaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2022-01-14 23:03:31 +0800
committerGitHub <noreply@github.com>2022-01-14 16:03:31 +0100
commit35c3553870e35b2e7cfcc599645791acda6afcef (patch)
tree0ad600c2d1cd94ef12566482832768c9efcf8a69 /web_src
parent8808293247bebd20482c3c625c64937174503781 (diff)
downloadgitea-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.js125
-rw-r--r--web_src/js/features/user-auth-webauthn.js197
-rw-r--r--web_src/js/index.js6
-rw-r--r--web_src/less/_form.less4
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,