gitea源码

user-auth-webauthn.ts 7.9KB

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