webauthn.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. function isWebAuthnSupported() {
  2. return window.PublicKeyCredential;
  3. }
  4. async function isConditionalLoginSupported() {
  5. return isWebAuthnSupported() &&
  6. window.PublicKeyCredential.isConditionalMediationAvailable &&
  7. window.PublicKeyCredential.isConditionalMediationAvailable();
  8. }
  9. // URLBase64 to ArrayBuffer
  10. function bufferDecode(value) {
  11. return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
  12. }
  13. // ArrayBuffer to URLBase64
  14. function bufferEncode(value) {
  15. return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
  16. .replace(/\+/g, "-")
  17. .replace(/\//g, "_")
  18. .replace(/=/g, "");
  19. }
  20. function getCsrfToken() {
  21. let element = document.querySelector("body[data-csrf-token]");
  22. if (element !== null) {
  23. return element.dataset.csrfToken;
  24. }
  25. return "";
  26. }
  27. async function post(urlKey, username, data) {
  28. var url = document.body.dataset[urlKey];
  29. if (username) {
  30. url += "?username=" + username;
  31. }
  32. return fetch(url, {
  33. method: "POST",
  34. headers: {
  35. "Content-Type": "application/json",
  36. "X-Csrf-Token": getCsrfToken()
  37. },
  38. body: JSON.stringify(data),
  39. });
  40. }
  41. async function get(urlKey, username) {
  42. var url = document.body.dataset[urlKey];
  43. if (username) {
  44. url += "?username=" + username;
  45. }
  46. return fetch(url);
  47. }
  48. function showError(error) {
  49. console.log("webauthn error: " + error);
  50. let alert = document.getElementById("webauthn-error");
  51. if (alert) {
  52. alert.classList.remove("hidden");
  53. }
  54. }
  55. async function register() {
  56. let beginRegisterURL = "webauthnRegisterBeginUrl";
  57. let r = await get(beginRegisterURL);
  58. let credOptions = await r.json();
  59. credOptions.publicKey.challenge = bufferDecode(credOptions.publicKey.challenge);
  60. credOptions.publicKey.user.id = bufferDecode(credOptions.publicKey.user.id);
  61. if(Object.hasOwn(credOptions.publicKey, 'excludeCredentials')) {
  62. credOptions.publicKey.excludeCredentials.forEach((credential) => credential.id = bufferDecode(credential.id));
  63. }
  64. let attestation = await navigator.credentials.create(credOptions);
  65. let cred = {
  66. id: attestation.id,
  67. rawId: bufferEncode(attestation.rawId),
  68. type: attestation.type,
  69. response: {
  70. attestationObject: bufferEncode(attestation.response.attestationObject),
  71. clientDataJSON: bufferEncode(attestation.response.clientDataJSON),
  72. },
  73. };
  74. let finishRegisterURL = "webauthnRegisterFinishUrl";
  75. let response = await post(finishRegisterURL, null, cred);
  76. if (!response.ok) {
  77. throw new Error("Login failed with HTTP status " + response.status);
  78. }
  79. console.log("registration successful");
  80. let jsonData = await response.json();
  81. let redirect = jsonData.redirect;
  82. window.location.href = redirect;
  83. }
  84. async function login(username, conditional) {
  85. let beginLoginURL = "webauthnLoginBeginUrl";
  86. let r = await get(beginLoginURL, username);
  87. let credOptions = await r.json();
  88. credOptions.publicKey.challenge = bufferDecode(credOptions.publicKey.challenge);
  89. if(Object.hasOwn(credOptions.publicKey, 'allowCredentials')) {
  90. credOptions.publicKey.allowCredentials.forEach((credential) => credential.id = bufferDecode(credential.id));
  91. }
  92. if (conditional) {
  93. credOptions.signal = abortController.signal;
  94. credOptions.mediation = "conditional";
  95. }
  96. var assertion;
  97. try {
  98. assertion = await navigator.credentials.get(credOptions);
  99. }
  100. catch (err) {
  101. // swallow aborted conditional logins
  102. if (err instanceof DOMException && err.name == "AbortError") {
  103. return;
  104. }
  105. throw err;
  106. }
  107. if (!assertion) {
  108. return;
  109. }
  110. let assertionResponse = {
  111. id: assertion.id,
  112. rawId: bufferEncode(assertion.rawId),
  113. type: assertion.type,
  114. response: {
  115. authenticatorData: bufferEncode(assertion.response.authenticatorData),
  116. clientDataJSON: bufferEncode(assertion.response.clientDataJSON),
  117. signature: bufferEncode(assertion.response.signature),
  118. userHandle: bufferEncode(assertion.response.userHandle),
  119. },
  120. };
  121. let finishLoginURL = "webauthnLoginFinishUrl";
  122. let response = await post(finishLoginURL, username, assertionResponse);
  123. if (!response.ok) {
  124. throw new Error("Login failed with HTTP status " + response.status);
  125. }
  126. window.location.reload();
  127. }
  128. async function conditionalLogin() {
  129. if (await isConditionalLoginSupported()) {
  130. login("", true);
  131. }
  132. }
  133. async function removeCreds(event) {
  134. event.preventDefault();
  135. let removeCredsURL = "webauthnDeleteAllUrl";
  136. await post(removeCredsURL, null, {});
  137. window.location.reload();
  138. }
  139. let abortController = new AbortController();
  140. document.addEventListener("DOMContentLoaded", function () {
  141. if (!isWebAuthnSupported()) {
  142. return;
  143. }
  144. let registerButton = document.getElementById("webauthn-register");
  145. if (registerButton != null) {
  146. registerButton.disabled = false;
  147. registerButton.addEventListener("click", (e) => {
  148. e.preventDefault();
  149. register().catch((err) => showError(err));
  150. });
  151. }
  152. let removeCredsButton = document.getElementById("webauthn-delete");
  153. if (removeCredsButton != null) {
  154. removeCredsButton.addEventListener("click", removeCreds);
  155. }
  156. let loginButton = document.getElementById("webauthn-login");
  157. if (loginButton != null) {
  158. loginButton.disabled = false;
  159. let usernameField = document.getElementById("form-username");
  160. if (usernameField != null) {
  161. usernameField.autocomplete += " webauthn";
  162. }
  163. let passwordField = document.getElementById("form-password");
  164. if (passwordField != null) {
  165. passwordField.autocomplete += " webauthn";
  166. }
  167. loginButton.addEventListener("click", (e) => {
  168. e.preventDefault();
  169. abortController.abort();
  170. login(usernameField.value).catch(err => showError(err));
  171. });
  172. conditionalLogin().catch(err => showError(err));
  173. }
  174. });