webauthn_handler.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  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, data) {
  41. const url = document.body.dataset[urlKey];
  42. return sendPOSTRequest(url, data);
  43. }
  44. async get(urlKey) {
  45. const url = document.body.dataset[urlKey];
  46. return fetch(url);
  47. }
  48. async removeAllCredentials() {
  49. try {
  50. await this.post("webauthnDeleteAllUrl", {});
  51. } catch (err) {
  52. WebAuthnHandler.showErrorMessage(err);
  53. return;
  54. }
  55. window.location.reload();
  56. }
  57. async register() {
  58. let registerBeginResponse;
  59. try {
  60. registerBeginResponse = await this.get("webauthnRegisterBeginUrl");
  61. } catch (err) {
  62. WebAuthnHandler.showErrorMessage(err);
  63. return;
  64. }
  65. let credentialCreationOptions;
  66. try {
  67. credentialCreationOptions = await registerBeginResponse.json();
  68. } catch (err) {
  69. WebAuthnHandler.showErrorMessage("Failed to parse registration options");
  70. return;
  71. }
  72. credentialCreationOptions.publicKey.challenge = this.decodeBuffer(credentialCreationOptions.publicKey.challenge);
  73. credentialCreationOptions.publicKey.user.id = this.decodeBuffer(credentialCreationOptions.publicKey.user.id);
  74. if (Object.hasOwn(credentialCreationOptions.publicKey, 'excludeCredentials')) {
  75. credentialCreationOptions.publicKey.excludeCredentials.forEach((credential) => {
  76. credential.id = this.decodeBuffer(credential.id);
  77. });
  78. }
  79. let attestation;
  80. try {
  81. attestation = await navigator.credentials.create(credentialCreationOptions);
  82. } catch (err) {
  83. WebAuthnHandler.showErrorMessage(err);
  84. return;
  85. }
  86. let registrationFinishResponse;
  87. try {
  88. registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", {
  89. id: attestation.id,
  90. rawId: this.encodeBuffer(attestation.rawId),
  91. type: attestation.type,
  92. response: {
  93. attestationObject: this.encodeBuffer(attestation.response.attestationObject),
  94. clientDataJSON: this.encodeBuffer(attestation.response.clientDataJSON),
  95. },
  96. });
  97. } catch (err) {
  98. WebAuthnHandler.showErrorMessage(err);
  99. return;
  100. }
  101. if (!registrationFinishResponse.ok) {
  102. throw new Error(`Registration failed with HTTP status code ${registrationFinishResponse.status}`);
  103. }
  104. const jsonData = await registrationFinishResponse.json();
  105. window.location.href = jsonData.redirect;
  106. }
  107. async login(abortController) {
  108. let loginBeginResponse;
  109. try {
  110. loginBeginResponse = await this.get("webauthnLoginBeginUrl");
  111. } catch (err) {
  112. WebAuthnHandler.showErrorMessage(err);
  113. return;
  114. }
  115. let credentialRequestOptions;
  116. try {
  117. credentialRequestOptions = await loginBeginResponse.json();
  118. } catch (err) {
  119. WebAuthnHandler.showErrorMessage("Failed to parse login options");
  120. return;
  121. }
  122. credentialRequestOptions.publicKey.challenge = this.decodeBuffer(credentialRequestOptions.publicKey.challenge);
  123. if (Object.hasOwn(credentialRequestOptions.publicKey, 'allowCredentials')) {
  124. credentialRequestOptions.publicKey.allowCredentials.forEach((credential) => {
  125. credential.id = this.decodeBuffer(credential.id);
  126. });
  127. }
  128. if (abortController) {
  129. credentialRequestOptions.signal = abortController.signal;
  130. credentialRequestOptions.mediation = "conditional";
  131. }
  132. let assertion;
  133. try {
  134. assertion = await navigator.credentials.get(credentialRequestOptions);
  135. }
  136. catch (err) {
  137. // Swallow aborted conditional logins
  138. if (err instanceof DOMException && err.name === "AbortError") {
  139. return;
  140. }
  141. WebAuthnHandler.showErrorMessage(err);
  142. return;
  143. }
  144. if (!assertion) {
  145. return;
  146. }
  147. let loginFinishResponse;
  148. try {
  149. loginFinishResponse = await this.post("webauthnLoginFinishUrl", {
  150. id: assertion.id,
  151. rawId: this.encodeBuffer(assertion.rawId),
  152. type: assertion.type,
  153. response: {
  154. authenticatorData: this.encodeBuffer(assertion.response.authenticatorData),
  155. clientDataJSON: this.encodeBuffer(assertion.response.clientDataJSON),
  156. signature: this.encodeBuffer(assertion.response.signature),
  157. userHandle: this.encodeBuffer(assertion.response.userHandle),
  158. },
  159. });
  160. } catch (err) {
  161. WebAuthnHandler.showErrorMessage(err);
  162. return;
  163. }
  164. if (!loginFinishResponse.ok) {
  165. throw new Error(`Login failed with HTTP status code ${loginFinishResponse.status}`);
  166. }
  167. window.location.reload();
  168. }
  169. }