code.gitea.io/gitea@v1.21.7/web_src/js/features/user-auth-webauthn.js (about) 1 import {encodeURLEncodedBase64, decodeURLEncodedBase64} from '../utils.js'; 2 import {showElem} from '../utils/dom.js'; 3 import {GET, POST} from '../modules/fetch.js'; 4 5 const {appSubUrl} = window.config; 6 7 export async function initUserAuthWebAuthn() { 8 const elPrompt = document.querySelector('.user.signin.webauthn-prompt'); 9 if (!elPrompt) { 10 return; 11 } 12 13 if (!detectWebAuthnSupport()) { 14 return; 15 } 16 17 const res = await GET(`${appSubUrl}/user/webauthn/assertion`); 18 if (res.status !== 200) { 19 webAuthnError('unknown'); 20 return; 21 } 22 const options = await res.json(); 23 options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge); 24 for (const cred of options.publicKey.allowCredentials) { 25 cred.id = decodeURLEncodedBase64(cred.id); 26 } 27 try { 28 const credential = await navigator.credentials.get({ 29 publicKey: options.publicKey 30 }); 31 await verifyAssertion(credential); 32 } catch (err) { 33 if (!options.publicKey.extensions?.appid) { 34 webAuthnError('general', err.message); 35 return; 36 } 37 delete options.publicKey.extensions.appid; 38 try { 39 const credential = await navigator.credentials.get({ 40 publicKey: options.publicKey 41 }); 42 await verifyAssertion(credential); 43 } catch (err) { 44 webAuthnError('general', err.message); 45 } 46 } 47 } 48 49 async function verifyAssertion(assertedCredential) { 50 // Move data into Arrays in case it is super long 51 const authData = new Uint8Array(assertedCredential.response.authenticatorData); 52 const clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON); 53 const rawId = new Uint8Array(assertedCredential.rawId); 54 const sig = new Uint8Array(assertedCredential.response.signature); 55 const userHandle = new Uint8Array(assertedCredential.response.userHandle); 56 57 const res = await POST(`${appSubUrl}/user/webauthn/assertion`, { 58 data: { 59 id: assertedCredential.id, 60 rawId: encodeURLEncodedBase64(rawId), 61 type: assertedCredential.type, 62 clientExtensionResults: assertedCredential.getClientExtensionResults(), 63 response: { 64 authenticatorData: encodeURLEncodedBase64(authData), 65 clientDataJSON: encodeURLEncodedBase64(clientDataJSON), 66 signature: encodeURLEncodedBase64(sig), 67 userHandle: encodeURLEncodedBase64(userHandle), 68 }, 69 }, 70 }); 71 if (res.status === 500) { 72 webAuthnError('unknown'); 73 return; 74 } else if (res.status !== 200) { 75 webAuthnError('unable-to-process'); 76 return; 77 } 78 const reply = await res.json(); 79 80 window.location.href = reply?.redirect ?? `${appSubUrl}/`; 81 } 82 83 async function webauthnRegistered(newCredential) { 84 const attestationObject = new Uint8Array(newCredential.response.attestationObject); 85 const clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON); 86 const rawId = new Uint8Array(newCredential.rawId); 87 88 const res = await POST(`${appSubUrl}/user/settings/security/webauthn/register`, { 89 data: { 90 id: newCredential.id, 91 rawId: encodeURLEncodedBase64(rawId), 92 type: newCredential.type, 93 response: { 94 attestationObject: encodeURLEncodedBase64(attestationObject), 95 clientDataJSON: encodeURLEncodedBase64(clientDataJSON), 96 }, 97 }, 98 }); 99 100 if (res.status === 409) { 101 webAuthnError('duplicated'); 102 return; 103 } else if (res.status !== 201) { 104 webAuthnError('unknown'); 105 return; 106 } 107 108 window.location.reload(); 109 } 110 111 function webAuthnError(errorType, message) { 112 const elErrorMsg = document.getElementById(`webauthn-error-msg`); 113 114 if (errorType === 'general') { 115 elErrorMsg.textContent = message || 'unknown error'; 116 } else { 117 const elTypedError = document.querySelector(`#webauthn-error [data-webauthn-error-msg=${errorType}]`); 118 if (elTypedError) { 119 elErrorMsg.textContent = `${elTypedError.textContent}${message ? ` ${message}` : ''}`; 120 } else { 121 elErrorMsg.textContent = `unknown error type: ${errorType}${message ? ` ${message}` : ''}`; 122 } 123 } 124 125 showElem('#webauthn-error'); 126 } 127 128 function detectWebAuthnSupport() { 129 if (!window.isSecureContext) { 130 webAuthnError('insecure'); 131 return false; 132 } 133 134 if (typeof window.PublicKeyCredential !== 'function') { 135 webAuthnError('browser'); 136 return false; 137 } 138 139 return true; 140 } 141 142 export function initUserAuthWebAuthnRegister() { 143 const elRegister = document.getElementById('register-webauthn'); 144 if (!elRegister) { 145 return; 146 } 147 if (!detectWebAuthnSupport()) { 148 elRegister.disabled = true; 149 return; 150 } 151 elRegister.addEventListener('click', async (e) => { 152 e.preventDefault(); 153 await webAuthnRegisterRequest(); 154 }); 155 } 156 157 async function webAuthnRegisterRequest() { 158 const elNickname = document.getElementById('nickname'); 159 160 const formData = new FormData(); 161 formData.append('name', elNickname.value); 162 163 const res = await POST(`${appSubUrl}/user/settings/security/webauthn/request_register`, { 164 data: formData, 165 }); 166 167 if (res.status === 409) { 168 webAuthnError('duplicated'); 169 return; 170 } else if (res.status !== 200) { 171 webAuthnError('unknown'); 172 return; 173 } 174 175 const options = await res.json(); 176 elNickname.closest('div.field').classList.remove('error'); 177 178 options.publicKey.challenge = decodeURLEncodedBase64(options.publicKey.challenge); 179 options.publicKey.user.id = decodeURLEncodedBase64(options.publicKey.user.id); 180 if (options.publicKey.excludeCredentials) { 181 for (const cred of options.publicKey.excludeCredentials) { 182 cred.id = decodeURLEncodedBase64(cred.id); 183 } 184 } 185 186 try { 187 const credential = await navigator.credentials.create({ 188 publicKey: options.publicKey 189 }); 190 await webauthnRegistered(credential); 191 } catch (err) { 192 webAuthnError('unknown', err); 193 } 194 }