mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	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>
This commit is contained in:
		@@ -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');
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										197
									
								
								web_src/js/features/user-auth-webauthn.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								web_src/js/features/user-auth-webauthn.js
									
									
									
									
									
										Normal file
									
								
							@@ -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');
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -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();
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user