webauthn_handler.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. class WebAuthnHandler {
  2. static isWebAuthnSupported() {
  3. return typeof PublicKeyCredential !== "undefined";
  4. }
  5. static showErrorMessage(errorMessage) {
  6. console.error("WebAuthn error:", errorMessage);
  7. const alertElement = document.getElementById("webauthn-error-alert");
  8. if (alertElement) {
  9. alertElement.remove();
  10. }
  11. const alertTemplateElement = document.getElementById("webauthn-error");
  12. if (alertTemplateElement) {
  13. const clonedElement = alertTemplateElement.content.cloneNode(true);
  14. const errorMessageElement = clonedElement.getElementById("webauthn-error-message");
  15. if (errorMessageElement) {
  16. errorMessageElement.textContent = errorMessage;
  17. }
  18. alertTemplateElement.parentNode.insertBefore(clonedElement, alertTemplateElement);
  19. }
  20. }
  21. static async isConditionalLoginSupported() {
  22. return WebAuthnHandler.isWebAuthnSupported() &&
  23. window.PublicKeyCredential.isConditionalMediationAvailable &&
  24. await window.PublicKeyCredential.isConditionalMediationAvailable();
  25. }
  26. async conditionalLogin(abortController) {
  27. if (await WebAuthnHandler.isConditionalLoginSupported()) {
  28. return this.login("", abortController);
  29. }
  30. }
  31. decodeBuffer(value) {
  32. return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
  33. }
  34. encodeBuffer(value) {
  35. return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
  36. .replace(/\+/g, "-")
  37. .replace(/\//g, "_")
  38. .replace(/=+$/g, "");
  39. }
  40. async post(urlKey, username, data) {
  41. let url = document.body.dataset[urlKey];
  42. if (username) {
  43. url += `?username=${encodeURIComponent(username)}`;
  44. }
  45. return sendPOSTRequest(url, data);
  46. }
  47. async get(urlKey, username) {
  48. let url = document.body.dataset[urlKey];
  49. if (username) {
  50. url += `?username=${encodeURIComponent(username)}`;
  51. }
  52. return fetch(url);
  53. }
  54. async removeAllCredentials() {
  55. try {
  56. await this.post("webauthnDeleteAllUrl", null, {});
  57. } catch (err) {
  58. WebAuthnHandler.showErrorMessage(err);
  59. return;
  60. }
  61. window.location.reload();
  62. }
  63. async register() {
  64. let registerBeginResponse;
  65. try {
  66. registerBeginResponse = await this.get("webauthnRegisterBeginUrl");
  67. } catch (err) {
  68. WebAuthnHandler.showErrorMessage(err);
  69. return;
  70. }
  71. let credentialCreationOptions;
  72. try {
  73. credentialCreationOptions = await registerBeginResponse.json();
  74. } catch (err) {
  75. WebAuthnHandler.showErrorMessage("Failed to parse registration options");
  76. return;
  77. }
  78. credentialCreationOptions.publicKey.challenge = this.decodeBuffer(credentialCreationOptions.publicKey.challenge);
  79. credentialCreationOptions.publicKey.user.id = this.decodeBuffer(credentialCreationOptions.publicKey.user.id);
  80. if (Object.hasOwn(credentialCreationOptions.publicKey, 'excludeCredentials')) {
  81. credentialCreationOptions.publicKey.excludeCredentials.forEach((credential) => {
  82. credential.id = this.decodeBuffer(credential.id);
  83. });
  84. }
  85. let attestation;
  86. try {
  87. attestation = await navigator.credentials.create(credentialCreationOptions);
  88. } catch (err) {
  89. WebAuthnHandler.showErrorMessage(err);
  90. return;
  91. }
  92. let registrationFinishResponse;
  93. try {
  94. registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", null, {
  95. id: attestation.id,
  96. rawId: this.encodeBuffer(attestation.rawId),
  97. type: attestation.type,
  98. response: {
  99. attestationObject: this.encodeBuffer(attestation.response.attestationObject),
  100. clientDataJSON: this.encodeBuffer(attestation.response.clientDataJSON),
  101. },
  102. });
  103. } catch (err) {
  104. WebAuthnHandler.showErrorMessage(err);
  105. return;
  106. }
  107. if (!registrationFinishResponse.ok) {
  108. throw new Error(`Registration failed with HTTP status code ${registrationFinishResponse.status}`);
  109. }
  110. const jsonData = await registrationFinishResponse.json();
  111. window.location.href = jsonData.redirect;
  112. }
  113. async login(username, abortController) {
  114. let loginBeginResponse;
  115. try {
  116. loginBeginResponse = await this.get("webauthnLoginBeginUrl", username);
  117. } catch (err) {
  118. WebAuthnHandler.showErrorMessage(err);
  119. return;
  120. }
  121. let credentialRequestOptions;
  122. try {
  123. credentialRequestOptions = await loginBeginResponse.json();
  124. } catch (err) {
  125. WebAuthnHandler.showErrorMessage("Failed to parse login options");
  126. return;
  127. }
  128. credentialRequestOptions.publicKey.challenge = this.decodeBuffer(credentialRequestOptions.publicKey.challenge);
  129. if (Object.hasOwn(credentialRequestOptions.publicKey, 'allowCredentials')) {
  130. credentialRequestOptions.publicKey.allowCredentials.forEach((credential) => {
  131. credential.id = this.decodeBuffer(credential.id);
  132. });
  133. }
  134. if (abortController) {
  135. credentialRequestOptions.signal = abortController.signal;
  136. credentialRequestOptions.mediation = "conditional";
  137. }
  138. let assertion;
  139. try {
  140. assertion = await navigator.credentials.get(credentialRequestOptions);
  141. }
  142. catch (err) {
  143. // Swallow aborted conditional logins
  144. if (err instanceof DOMException && err.name === "AbortError") {
  145. return;
  146. }
  147. WebAuthnHandler.showErrorMessage(err);
  148. return;
  149. }
  150. if (!assertion) {
  151. return;
  152. }
  153. let loginFinishResponse;
  154. try {
  155. loginFinishResponse = await this.post("webauthnLoginFinishUrl", username, {
  156. id: assertion.id,
  157. rawId: this.encodeBuffer(assertion.rawId),
  158. type: assertion.type,
  159. response: {
  160. authenticatorData: this.encodeBuffer(assertion.response.authenticatorData),
  161. clientDataJSON: this.encodeBuffer(assertion.response.clientDataJSON),
  162. signature: this.encodeBuffer(assertion.response.signature),
  163. userHandle: this.encodeBuffer(assertion.response.userHandle),
  164. },
  165. });
  166. } catch (err) {
  167. WebAuthnHandler.showErrorMessage(err);
  168. return;
  169. }
  170. if (!loginFinishResponse.ok) {
  171. throw new Error(`Login failed with HTTP status code ${loginFinishResponse.status}`);
  172. }
  173. window.location.reload();
  174. }
  175. }