webauthn_handler.js 7.3 KB

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