webauthn_handler.js 6.5 KB

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