webauthn_handler.js 6.5 KB

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