webauthn_handler.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  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) => credential.id = this.decodeBuffer(credential.id));
  82. }
  83. let attestation;
  84. try {
  85. attestation = await navigator.credentials.create(credentialCreationOptions);
  86. } catch (err) {
  87. WebAuthnHandler.showErrorMessage(err);
  88. return;
  89. }
  90. let registrationFinishResponse;
  91. try {
  92. registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", null, {
  93. id: attestation.id,
  94. rawId: this.encodeBuffer(attestation.rawId),
  95. type: attestation.type,
  96. response: {
  97. attestationObject: this.encodeBuffer(attestation.response.attestationObject),
  98. clientDataJSON: this.encodeBuffer(attestation.response.clientDataJSON),
  99. },
  100. });
  101. } catch (err) {
  102. WebAuthnHandler.showErrorMessage(err);
  103. return;
  104. }
  105. if (!registrationFinishResponse.ok) {
  106. throw new Error("Registration failed with HTTP status code " + registrationFinishResponse.status);
  107. }
  108. const jsonData = await registrationFinishResponse.json();
  109. window.location.href = jsonData.redirect;
  110. }
  111. async login(username, abortController) {
  112. let loginBeginResponse;
  113. try {
  114. loginBeginResponse = await this.get("webauthnLoginBeginUrl", username);
  115. } catch (err) {
  116. WebAuthnHandler.showErrorMessage(err);
  117. return;
  118. }
  119. let credentialRequestOptions;
  120. try {
  121. credentialRequestOptions = await loginBeginResponse.json();
  122. } catch (err) {
  123. WebAuthnHandler.showErrorMessage("Failed to parse login options");
  124. return;
  125. }
  126. credentialRequestOptions.publicKey.challenge = this.decodeBuffer(credentialRequestOptions.publicKey.challenge);
  127. if (Object.hasOwn(credentialRequestOptions.publicKey, 'allowCredentials')) {
  128. credentialRequestOptions.publicKey.allowCredentials.forEach((credential) => credential.id = this.decodeBuffer(credential.id));
  129. }
  130. if (abortController) {
  131. credentialRequestOptions.signal = abortController.signal;
  132. credentialRequestOptions.mediation = "conditional";
  133. }
  134. let assertion;
  135. try {
  136. assertion = await navigator.credentials.get(credentialRequestOptions);
  137. }
  138. catch (err) {
  139. // Swallow aborted conditional logins
  140. if (err instanceof DOMException && err.name === "AbortError") {
  141. return;
  142. }
  143. WebAuthnHandler.showErrorMessage(err);
  144. return;
  145. }
  146. if (!assertion) {
  147. return;
  148. }
  149. let loginFinishResponse;
  150. try {
  151. loginFinishResponse = await this.post("webauthnLoginFinishUrl", username, {
  152. id: assertion.id,
  153. rawId: this.encodeBuffer(assertion.rawId),
  154. type: assertion.type,
  155. response: {
  156. authenticatorData: this.encodeBuffer(assertion.response.authenticatorData),
  157. clientDataJSON: this.encodeBuffer(assertion.response.clientDataJSON),
  158. signature: this.encodeBuffer(assertion.response.signature),
  159. userHandle: this.encodeBuffer(assertion.response.userHandle),
  160. },
  161. });
  162. } catch (err) {
  163. WebAuthnHandler.showErrorMessage(err);
  164. return;
  165. }
  166. if (!loginFinishResponse.ok) {
  167. throw new Error("Login failed with HTTP status code " + loginFinishResponse.status);
  168. }
  169. window.location.reload();
  170. }
  171. }