mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +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,11 +1,9 @@ | ||||
| import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js'; | ||||
| import {showElem, hideElem} from '../utils/dom.js'; | ||||
| import {showElem} from '../utils/dom.js'; | ||||
|  | ||||
| const {appSubUrl, csrfToken} = window.config; | ||||
|  | ||||
| export async function initUserAuthWebAuthn() { | ||||
|   hideElem('#webauthn-error'); | ||||
|  | ||||
|   const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); | ||||
|   if (!elPrompt) { | ||||
|     return; | ||||
| @@ -25,10 +23,10 @@ export async function initUserAuthWebAuthn() { | ||||
|   for (const cred of options.publicKey.allowCredentials) { | ||||
|     cred.id = decodeURLEncodedBase64(cred.id); | ||||
|   } | ||||
|   const credential = await navigator.credentials.get({ | ||||
|     publicKey: options.publicKey | ||||
|   }); | ||||
|   try { | ||||
|     const credential = await navigator.credentials.get({ | ||||
|       publicKey: options.publicKey | ||||
|     }); | ||||
|     await verifyAssertion(credential); | ||||
|   } catch (err) { | ||||
|     if (!options.publicKey.extensions?.appid) { | ||||
| @@ -36,10 +34,10 @@ export async function initUserAuthWebAuthn() { | ||||
|       return; | ||||
|     } | ||||
|     delete options.publicKey.extensions.appid; | ||||
|     const credential = await navigator.credentials.get({ | ||||
|       publicKey: options.publicKey | ||||
|     }); | ||||
|     try { | ||||
|       const credential = await navigator.credentials.get({ | ||||
|         publicKey: options.publicKey | ||||
|       }); | ||||
|       await verifyAssertion(credential); | ||||
|     } catch (err) { | ||||
|       webAuthnError('general', err.message); | ||||
| @@ -48,7 +46,7 @@ export async function initUserAuthWebAuthn() { | ||||
| } | ||||
|  | ||||
| async function verifyAssertion(assertedCredential) { | ||||
|   // Move data into Arrays incase it is super long | ||||
|   // Move data into Arrays in case it is super long | ||||
|   const authData = new Uint8Array(assertedCredential.response.authenticatorData); | ||||
|   const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); | ||||
|   const rawId = new Uint8Array(assertedCredential.rawId); | ||||
| @@ -137,15 +135,11 @@ function webAuthnError(errorType, message) { | ||||
|  | ||||
| function detectWebAuthnSupport() { | ||||
|   if (!window.isSecureContext) { | ||||
|     document.getElementById('register-button').disabled = true; | ||||
|     document.getElementById('login-button').disabled = true; | ||||
|     webAuthnError('insecure'); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   if (typeof window.PublicKeyCredential !== 'function') { | ||||
|     document.getElementById('register-button').disabled = true; | ||||
|     document.getElementById('login-button').disabled = true; | ||||
|     webAuthnError('browser'); | ||||
|     return false; | ||||
|   } | ||||
| @@ -158,15 +152,13 @@ export function initUserAuthWebAuthnRegister() { | ||||
|   if (!elRegister) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   hideElem('#webauthn-error'); | ||||
|  | ||||
|   elRegister.addEventListener('click', (e) => { | ||||
|   if (!detectWebAuthnSupport()) { | ||||
|     elRegister.disabled = true; | ||||
|     return; | ||||
|   } | ||||
|   elRegister.addEventListener('click', async (e) => { | ||||
|     e.preventDefault(); | ||||
|     if (!detectWebAuthnSupport()) { | ||||
|       return; | ||||
|     } | ||||
|     webAuthnRegisterRequest(); | ||||
|     await webAuthnRegisterRequest(); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| @@ -203,15 +195,12 @@ async function webAuthnRegisterRequest() { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   let credential; | ||||
|   try { | ||||
|     credential = await navigator.credentials.create({ | ||||
|     const credential = await navigator.credentials.create({ | ||||
|       publicKey: options.publicKey | ||||
|     }); | ||||
|     await webauthnRegistered(credential); | ||||
|   } catch (err) { | ||||
|     webAuthnError('unknown', err); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   webauthnRegistered(credential); | ||||
| } | ||||
|   | ||||
| @@ -133,8 +133,17 @@ test('toAbsoluteUrl', () => { | ||||
|   expect(() => toAbsoluteUrl('path')).toThrowError('unsupported'); | ||||
| }); | ||||
|  | ||||
| const uint8array = (s) => new TextEncoder().encode(s); | ||||
| test('encodeURLEncodedBase64, decodeURLEncodedBase64', () => { | ||||
|   expect(encodeURLEncodedBase64(decodeURLEncodedBase64('foo'))).toEqual('foo'); // No = padding | ||||
|   expect(encodeURLEncodedBase64(decodeURLEncodedBase64('a-minus'))).toEqual('a-minus'); | ||||
|   expect(encodeURLEncodedBase64(decodeURLEncodedBase64('_underscorc'))).toEqual('_underscorc'); | ||||
|   expect(encodeURLEncodedBase64(uint8array('AA?'))).toEqual('QUE_'); // standard base64: "QUE/" | ||||
|   expect(encodeURLEncodedBase64(uint8array('AA~'))).toEqual('QUF-'); // standard base64: "QUF+" | ||||
|  | ||||
|   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