mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	Fix webauthn regression and improve code (#25113)
Follow: * #22697 There are some bugs in #22697: * https://github.com/go-gitea/gitea/pull/22697#issuecomment-1577957966 * the webauthn failure message is never shown and causes console error * The `document.getElementById('register-button')` and `document.getElementById('login-button')` is wrong * there is no such element in code * it causes JS error when a browser doesn't provide webauthn * the end user can't see the real error message These bugs are fixed in this PR. Other changes: * Use simple HTML/CSS layouts, no need to use too many `gt-` patches * Make the webauthn page have correct "page-content" layout * The "data-webauthn-error-msg" elements are only used to provide locale texts, so move them into a single "gt-hidden", then no need to repeat a lot of "gt-hidden" in code * The `{{.CsrfTokenHtml}}` is a no-op because there is no form * Many `hideElem('#webauthn-error')` in code is no-op because the `webauthn-error` already has "gt-hidden" by default * Make the tests for "URLEncodedBase64" really test with concrete cases. Screenshots: * Error message when webauthn fails (before, there is no error message): <details>  </details> * Error message when webauthn is unavailable <details>  </details>
This commit is contained in:
		@@ -1,18 +1,19 @@
 | 
				
			|||||||
{{template "base/head" .}}
 | 
					{{template "base/head" .}}
 | 
				
			||||||
<div class="user signin webauthn-prompt">
 | 
					<div role="main" aria-label="{{.Title}}" class="page-content user signin webauthn-prompt">
 | 
				
			||||||
	<div class="ui middle centered very relaxed page grid">
 | 
						<div class="ui page grid">
 | 
				
			||||||
		<div class="column center aligned">
 | 
							<div class="column center aligned">
 | 
				
			||||||
				<h3 class="ui top attached header">
 | 
					 | 
				
			||||||
				{{.locale.Tr "twofa"}}
 | 
					 | 
				
			||||||
				</h3>
 | 
					 | 
				
			||||||
			{{template "user/auth/webauthn_error" .}}
 | 
								{{template "user/auth/webauthn_error" .}}
 | 
				
			||||||
 | 
								<h3 class="ui top attached header">{{.locale.Tr "twofa"}}</h3>
 | 
				
			||||||
			<div class="ui attached segment">
 | 
								<div class="ui attached segment">
 | 
				
			||||||
				{{svg "octicon-key" 56}}
 | 
									{{svg "octicon-key" 56}}
 | 
				
			||||||
				<h3>{{.locale.Tr "webauthn_insert_key"}}</h3>
 | 
									<h3>{{.locale.Tr "webauthn_insert_key"}}</h3>
 | 
				
			||||||
				{{template "base/alert" .}}
 | 
									{{template "base/alert" .}}
 | 
				
			||||||
				<p>{{.locale.Tr "webauthn_sign_in"}}</p>
 | 
									<p>{{.locale.Tr "webauthn_sign_in"}}</p>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
				<div class="ui attached segment"><div class="ui active indeterminate inline loader"></div> {{.locale.Tr "webauthn_press_button"}} </div>
 | 
								<div class="ui attached segment">
 | 
				
			||||||
 | 
									<div class="ui active indeterminate inline loader"></div>
 | 
				
			||||||
 | 
									{{.locale.Tr "webauthn_press_button"}}
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
			<div class="ui attached segment">
 | 
								<div class="ui attached segment">
 | 
				
			||||||
				<a href="{{AppSubUrl}}/user/two_factor">{{.locale.Tr "webauthn_use_twofa"}}</a>
 | 
									<a href="{{AppSubUrl}}/user/two_factor">{{.locale.Tr "webauthn_use_twofa"}}</a>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,13 @@
 | 
				
			|||||||
<div id="webauthn-error" class="ui small gt-hidden">
 | 
					<div id="webauthn-error" class="ui negative message gt-hidden">
 | 
				
			||||||
	<div class="content ui negative message gt-df gt-fc gt-gap-3">
 | 
					 | 
				
			||||||
	<div class="header">{{.locale.Tr "webauthn_error"}}</div>
 | 
						<div class="header">{{.locale.Tr "webauthn_error"}}</div>
 | 
				
			||||||
		<div id="webauthn-error-msg"></div>
 | 
						<div id="webauthn-error-msg" class="gt-pt-3"></div>
 | 
				
			||||||
		<div class="gt-hidden" data-webauthn-error-msg="browser">{{.locale.Tr "webauthn_unsupported_browser"}}</div>
 | 
						<div class="gt-hidden">
 | 
				
			||||||
		<div class="gt-hidden" data-webauthn-error-msg="unknown">{{.locale.Tr "webauthn_error_unknown"}}</div>
 | 
							<div data-webauthn-error-msg="browser">{{.locale.Tr "webauthn_unsupported_browser"}}</div>
 | 
				
			||||||
		<div class="gt-hidden" data-webauthn-error-msg="insecure">{{.locale.Tr "webauthn_error_insecure"}}</div>
 | 
							<div data-webauthn-error-msg="unknown">{{.locale.Tr "webauthn_error_unknown"}}</div>
 | 
				
			||||||
		<div class="gt-hidden" data-webauthn-error-msg="unable-to-process">{{.locale.Tr "webauthn_error_unable_to_process"}}</div>
 | 
							<div data-webauthn-error-msg="insecure">{{.locale.Tr "webauthn_error_insecure"}}</div>
 | 
				
			||||||
		<div class="gt-hidden" data-webauthn-error-msg="duplicated">{{.locale.Tr "webauthn_error_duplicated"}}</div>
 | 
							<div data-webauthn-error-msg="unable-to-process">{{.locale.Tr "webauthn_error_unable_to_process"}}</div>
 | 
				
			||||||
		<div class="gt-hidden" data-webauthn-error-msg="empty">{{.locale.Tr "webauthn_error_empty"}}</div>
 | 
							<div data-webauthn-error-msg="duplicated">{{.locale.Tr "webauthn_error_duplicated"}}</div>
 | 
				
			||||||
		<div class="gt-hidden" data-webauthn-error-msg="timeout">{{.locale.Tr "webauthn_error_timeout"}}</div>
 | 
							<div data-webauthn-error-msg="empty">{{.locale.Tr "webauthn_error_empty"}}</div>
 | 
				
			||||||
 | 
							<div data-webauthn-error-msg="timeout">{{.locale.Tr "webauthn_error_timeout"}}</div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
<h4 class="ui top attached header">
 | 
					<h4 class="ui top attached header">{{.locale.Tr "settings.webauthn"}}</h4>
 | 
				
			||||||
{{.locale.Tr "settings.webauthn"}}
 | 
					 | 
				
			||||||
</h4>
 | 
					 | 
				
			||||||
<div class="ui attached segment">
 | 
					<div class="ui attached segment">
 | 
				
			||||||
	<p>{{.locale.Tr "settings.webauthn_desc" | Str2html}}</p>
 | 
						<p>{{.locale.Tr "settings.webauthn_desc" | Str2html}}</p>
 | 
				
			||||||
	{{template "user/auth/webauthn_error" .}}
 | 
						{{template "user/auth/webauthn_error" .}}
 | 
				
			||||||
@@ -20,7 +18,6 @@
 | 
				
			|||||||
		{{end}}
 | 
							{{end}}
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="ui form">
 | 
						<div class="ui form">
 | 
				
			||||||
		{{.CsrfTokenHtml}}
 | 
					 | 
				
			||||||
		<div class="required field">
 | 
							<div class="required field">
 | 
				
			||||||
			<label for="nickname">{{.locale.Tr "settings.webauthn_nickname"}}</label>
 | 
								<label for="nickname">{{.locale.Tr "settings.webauthn_nickname"}}</label>
 | 
				
			||||||
			<input id="nickname" name="nickname" type="text" required>
 | 
								<input id="nickname" name="nickname" type="text" required>
 | 
				
			||||||
@@ -29,7 +26,6 @@
 | 
				
			|||||||
	</div>
 | 
						</div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
<div class="ui g-modal-confirm delete modal" id="delete-registration">
 | 
					<div class="ui g-modal-confirm delete modal" id="delete-registration">
 | 
				
			||||||
	<div class="header">
 | 
						<div class="header">
 | 
				
			||||||
		{{svg "octicon-trash"}}
 | 
							{{svg "octicon-trash"}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -394,10 +394,6 @@ textarea:focus,
 | 
				
			|||||||
  margin: 0;
 | 
					  margin: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.user.signin.webauthn-prompt {
 | 
					 | 
				
			||||||
  margin-top: 15px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.repository.new.repo form,
 | 
					.repository.new.repo form,
 | 
				
			||||||
.repository.new.migrate form,
 | 
					.repository.new.migrate form,
 | 
				
			||||||
.repository.new.fork form {
 | 
					.repository.new.fork form {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,9 @@
 | 
				
			|||||||
import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';
 | 
					import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js';
 | 
				
			||||||
import {showElem, hideElem} from '../utils/dom.js';
 | 
					import {showElem} from '../utils/dom.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {appSubUrl, csrfToken} = window.config;
 | 
					const {appSubUrl, csrfToken} = window.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function initUserAuthWebAuthn() {
 | 
					export async function initUserAuthWebAuthn() {
 | 
				
			||||||
  hideElem('#webauthn-error');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
 | 
					  const elPrompt = document.querySelector('.user.signin.webauthn-prompt');
 | 
				
			||||||
  if (!elPrompt) {
 | 
					  if (!elPrompt) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
@@ -25,10 +23,10 @@ export async function initUserAuthWebAuthn() {
 | 
				
			|||||||
  for (const cred of options.publicKey.allowCredentials) {
 | 
					  for (const cred of options.publicKey.allowCredentials) {
 | 
				
			||||||
    cred.id = decodeURLEncodedBase64(cred.id);
 | 
					    cred.id = decodeURLEncodedBase64(cred.id);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    const credential = await navigator.credentials.get({
 | 
					    const credential = await navigator.credentials.get({
 | 
				
			||||||
      publicKey: options.publicKey
 | 
					      publicKey: options.publicKey
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    await verifyAssertion(credential);
 | 
					    await verifyAssertion(credential);
 | 
				
			||||||
  } catch (err) {
 | 
					  } catch (err) {
 | 
				
			||||||
    if (!options.publicKey.extensions?.appid) {
 | 
					    if (!options.publicKey.extensions?.appid) {
 | 
				
			||||||
@@ -36,10 +34,10 @@ export async function initUserAuthWebAuthn() {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    delete options.publicKey.extensions.appid;
 | 
					    delete options.publicKey.extensions.appid;
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
      const credential = await navigator.credentials.get({
 | 
					      const credential = await navigator.credentials.get({
 | 
				
			||||||
        publicKey: options.publicKey
 | 
					        publicKey: options.publicKey
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      await verifyAssertion(credential);
 | 
					      await verifyAssertion(credential);
 | 
				
			||||||
    } catch (err) {
 | 
					    } catch (err) {
 | 
				
			||||||
      webAuthnError('general', err.message);
 | 
					      webAuthnError('general', err.message);
 | 
				
			||||||
@@ -137,15 +135,11 @@ function webAuthnError(errorType, message) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function detectWebAuthnSupport() {
 | 
					function detectWebAuthnSupport() {
 | 
				
			||||||
  if (!window.isSecureContext) {
 | 
					  if (!window.isSecureContext) {
 | 
				
			||||||
    document.getElementById('register-button').disabled = true;
 | 
					 | 
				
			||||||
    document.getElementById('login-button').disabled = true;
 | 
					 | 
				
			||||||
    webAuthnError('insecure');
 | 
					    webAuthnError('insecure');
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (typeof window.PublicKeyCredential !== 'function') {
 | 
					  if (typeof window.PublicKeyCredential !== 'function') {
 | 
				
			||||||
    document.getElementById('register-button').disabled = true;
 | 
					 | 
				
			||||||
    document.getElementById('login-button').disabled = true;
 | 
					 | 
				
			||||||
    webAuthnError('browser');
 | 
					    webAuthnError('browser');
 | 
				
			||||||
    return false;
 | 
					    return false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -158,15 +152,13 @@ export function initUserAuthWebAuthnRegister() {
 | 
				
			|||||||
  if (!elRegister) {
 | 
					  if (!elRegister) {
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  hideElem('#webauthn-error');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  elRegister.addEventListener('click', (e) => {
 | 
					 | 
				
			||||||
    e.preventDefault();
 | 
					 | 
				
			||||||
  if (!detectWebAuthnSupport()) {
 | 
					  if (!detectWebAuthnSupport()) {
 | 
				
			||||||
 | 
					    elRegister.disabled = true;
 | 
				
			||||||
    return;
 | 
					    return;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
    webAuthnRegisterRequest();
 | 
					  elRegister.addEventListener('click', async (e) => {
 | 
				
			||||||
 | 
					    e.preventDefault();
 | 
				
			||||||
 | 
					    await webAuthnRegisterRequest();
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -203,15 +195,12 @@ async function webAuthnRegisterRequest() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let credential;
 | 
					 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    credential = await navigator.credentials.create({
 | 
					    const credential = await navigator.credentials.create({
 | 
				
			||||||
      publicKey: options.publicKey
 | 
					      publicKey: options.publicKey
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    await webauthnRegistered(credential);
 | 
				
			||||||
  } catch (err) {
 | 
					  } catch (err) {
 | 
				
			||||||
    webAuthnError('unknown', err);
 | 
					    webAuthnError('unknown', err);
 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  webauthnRegistered(credential);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -133,8 +133,17 @@ test('toAbsoluteUrl', () => {
 | 
				
			|||||||
  expect(() => toAbsoluteUrl('path')).toThrowError('unsupported');
 | 
					  expect(() => toAbsoluteUrl('path')).toThrowError('unsupported');
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uint8array = (s) => new TextEncoder().encode(s);
 | 
				
			||||||
test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
 | 
					test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => {
 | 
				
			||||||
  expect(encodeURLEncodedBase64(decodeURLEncodedBase64('foo'))).toEqual('foo'); // No = padding
 | 
					  expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/"
 | 
				
			||||||
  expect(encodeURLEncodedBase64(decodeURLEncodedBase64('a-minus'))).toEqual('a-minus');
 | 
					  expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+"
 | 
				
			||||||
  expect(encodeURLEncodedBase64(decodeURLEncodedBase64('_underscorc'))).toEqual('_underscorc');
 | 
					
 | 
				
			||||||
 | 
					  expect(decodeURLEncodedBase64('QUE/')).toEqual(uint8array('AA?'));
 | 
				
			||||||
 | 
					  expect(decodeURLEncodedBase64('QUF+')).toEqual(uint8array('AA~'));
 | 
				
			||||||
 | 
					  expect(decodeURLEncodedBase64('QUE_')).toEqual(uint8array('AA?'));
 | 
				
			||||||
 | 
					  expect(decodeURLEncodedBase64('QUF-')).toEqual(uint8array('AA~'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  expect(encodeURLEncodedBase64(uint8array('a'))).toEqual('YQ'); // standard base64: "YQ=="
 | 
				
			||||||
 | 
					  expect(decodeURLEncodedBase64('YQ')).toEqual(uint8array('a'));
 | 
				
			||||||
 | 
					  expect(decodeURLEncodedBase64('YQ==')).toEqual(uint8array('a'));
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user