You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

user-auth-webauthn.js 5.6KB

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