summaryrefslogtreecommitdiffstats
path: root/web_src/js/features/user-auth-webauthn.js
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/js/features/user-auth-webauthn.js
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/js/features/user-auth-webauthn.js')
-rw-r--r--web_src/js/features/user-auth-webauthn.js197
1 files changed, 197 insertions, 0 deletions
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');
+ });
+}