webauthn_handler.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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");
  8. if (alertElement) {
  9. alertElement.textContent += " (" + errorMessage + ")";
  10. alertElement.classList.remove("hidden");
  11. }
  12. }
  13. async isConditionalLoginSupported() {
  14. return WebAuthnHandler.isWebAuthnSupported() &&
  15. window.PublicKeyCredential.isConditionalMediationAvailable &&
  16. window.PublicKeyCredential.isConditionalMediationAvailable();
  17. }
  18. async conditionalLogin(abortController) {
  19. if (await this.isConditionalLoginSupported()) {
  20. this.login("", abortController);
  21. }
  22. }
  23. decodeBuffer(value) {
  24. return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
  25. }
  26. encodeBuffer(value) {
  27. return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
  28. .replace(/\+/g, "-")
  29. .replace(/\//g, "_")
  30. .replace(/=/g, "");
  31. }
  32. async post(urlKey, username, data) {
  33. let url = document.body.dataset[urlKey];
  34. if (username) {
  35. url += "?username=" + username;
  36. }
  37. return fetch(url, {
  38. method: "POST",
  39. headers: {
  40. "Content-Type": "application/json",
  41. "X-Csrf-Token": getCsrfToken()
  42. },
  43. body: JSON.stringify(data),
  44. });
  45. }
  46. async get(urlKey, username) {
  47. let url = document.body.dataset[urlKey];
  48. if (username) {
  49. url += "?username=" + username;
  50. }
  51. return fetch(url);
  52. }
  53. async removeAllCredentials() {
  54. try {
  55. await this.post("webauthnDeleteAllUrl", null, {});
  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. const credentialCreationOptions = await registerBeginResponse.json();
  71. credentialCreationOptions.publicKey.challenge = this.decodeBuffer(credentialCreationOptions.publicKey.challenge);
  72. credentialCreationOptions.publicKey.user.id = this.decodeBuffer(credentialCreationOptions.publicKey.user.id);
  73. if (Object.hasOwn(credentialCreationOptions.publicKey, 'excludeCredentials')) {
  74. credentialCreationOptions.publicKey.excludeCredentials.forEach((credential) => credential.id = this.decodeBuffer(credential.id));
  75. }
  76. const attestation = await navigator.credentials.create(credentialCreationOptions);
  77. let registrationFinishResponse;
  78. try {
  79. registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", null, {
  80. id: attestation.id,
  81. rawId: this.encodeBuffer(attestation.rawId),
  82. type: attestation.type,
  83. response: {
  84. attestationObject: this.encodeBuffer(attestation.response.attestationObject),
  85. clientDataJSON: this.encodeBuffer(attestation.response.clientDataJSON),
  86. },
  87. });
  88. } catch (err) {
  89. WebAuthnHandler.showErrorMessage(err);
  90. return;
  91. }
  92. if (!registrationFinishResponse.ok) {
  93. throw new Error("Login failed with HTTP status code " + response.status);
  94. }
  95. const jsonData = await registrationFinishResponse.json();
  96. window.location.href = jsonData.redirect;
  97. }
  98. async login(username, abortController) {
  99. let loginBeginResponse;
  100. try {
  101. loginBeginResponse = await this.get("webauthnLoginBeginUrl", username);
  102. } catch (err) {
  103. WebAuthnHandler.showErrorMessage(err);
  104. return;
  105. }
  106. const credentialRequestOptions = await loginBeginResponse.json();
  107. credentialRequestOptions.publicKey.challenge = this.decodeBuffer(credentialRequestOptions.publicKey.challenge);
  108. if (Object.hasOwn(credentialRequestOptions.publicKey, 'allowCredentials')) {
  109. credentialRequestOptions.publicKey.allowCredentials.forEach((credential) => credential.id = this.decodeBuffer(credential.id));
  110. }
  111. if (abortController) {
  112. credentialRequestOptions.signal = abortController.signal;
  113. credentialRequestOptions.mediation = "conditional";
  114. }
  115. let assertion;
  116. try {
  117. assertion = await navigator.credentials.get(credentialRequestOptions);
  118. }
  119. catch (err) {
  120. // Swallow aborted conditional logins
  121. if (err instanceof DOMException && err.name == "AbortError") {
  122. return;
  123. }
  124. WebAuthnHandler.showErrorMessage(err);
  125. return;
  126. }
  127. if (!assertion) {
  128. return;
  129. }
  130. let loginFinishResponse;
  131. try {
  132. loginFinishResponse = await this.post("webauthnLoginFinishUrl", username, {
  133. id: assertion.id,
  134. rawId: this.encodeBuffer(assertion.rawId),
  135. type: assertion.type,
  136. response: {
  137. authenticatorData: this.encodeBuffer(assertion.response.authenticatorData),
  138. clientDataJSON: this.encodeBuffer(assertion.response.clientDataJSON),
  139. signature: this.encodeBuffer(assertion.response.signature),
  140. userHandle: this.encodeBuffer(assertion.response.userHandle),
  141. },
  142. });
  143. } catch (err) {
  144. WebAuthnHandler.showErrorMessage(err);
  145. return;
  146. }
  147. if (!loginFinishResponse.ok) {
  148. throw new Error("Login failed with HTTP status code " + loginFinishResponse.status);
  149. }
  150. window.location.reload();
  151. }
  152. }