| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204 |
- class WebAuthnHandler {
- static isWebAuthnSupported() {
- return typeof PublicKeyCredential !== "undefined";
- }
- static showErrorMessage(errorMessage) {
- console.error("WebAuthn error:", errorMessage);
- const alertElement = document.getElementById("webauthn-error-alert");
- if (alertElement) {
- alertElement.remove();
- }
- const alertTemplateElement = document.getElementById("webauthn-error");
- if (alertTemplateElement) {
- const clonedElement = alertTemplateElement.content.cloneNode(true);
- const errorMessageElement = clonedElement.getElementById("webauthn-error-message");
- if (errorMessageElement) {
- errorMessageElement.textContent = errorMessage;
- }
- alertTemplateElement.parentNode.insertBefore(clonedElement, alertTemplateElement);
- }
- }
- static async isConditionalLoginSupported() {
- return WebAuthnHandler.isWebAuthnSupported() &&
- window.PublicKeyCredential.isConditionalMediationAvailable &&
- await window.PublicKeyCredential.isConditionalMediationAvailable();
- }
- async conditionalLogin(abortController) {
- if (await WebAuthnHandler.isConditionalLoginSupported()) {
- return this.login("", abortController);
- }
- }
- decodeBuffer(value) {
- return Uint8Array.from(atob(value.replace(/-/g, "+").replace(/_/g, "/")), c => c.charCodeAt(0));
- }
- encodeBuffer(value) {
- return btoa(String.fromCharCode.apply(null, new Uint8Array(value)))
- .replace(/\+/g, "-")
- .replace(/\//g, "_")
- .replace(/=+$/g, "");
- }
- async post(urlKey, username, data) {
- let url = document.body.dataset[urlKey];
- if (username) {
- url += `?username=${encodeURIComponent(username)}`;
- }
- return sendPOSTRequest(url, data);
- }
- async get(urlKey, username) {
- let url = document.body.dataset[urlKey];
- if (username) {
- url += `?username=${encodeURIComponent(username)}`;
- }
- return fetch(url);
- }
- async removeAllCredentials() {
- try {
- await this.post("webauthnDeleteAllUrl", null, {});
- } catch (err) {
- WebAuthnHandler.showErrorMessage(err);
- return;
- }
- window.location.reload();
- }
- async register() {
- let registerBeginResponse;
- try {
- registerBeginResponse = await this.get("webauthnRegisterBeginUrl");
- } catch (err) {
- WebAuthnHandler.showErrorMessage(err);
- return;
- }
- let credentialCreationOptions;
- try {
- credentialCreationOptions = await registerBeginResponse.json();
- } catch (err) {
- WebAuthnHandler.showErrorMessage("Failed to parse registration options");
- return;
- }
- credentialCreationOptions.publicKey.challenge = this.decodeBuffer(credentialCreationOptions.publicKey.challenge);
- credentialCreationOptions.publicKey.user.id = this.decodeBuffer(credentialCreationOptions.publicKey.user.id);
- if (Object.hasOwn(credentialCreationOptions.publicKey, 'excludeCredentials')) {
- credentialCreationOptions.publicKey.excludeCredentials.forEach((credential) => {
- credential.id = this.decodeBuffer(credential.id);
- });
- }
- let attestation;
- try {
- attestation = await navigator.credentials.create(credentialCreationOptions);
- } catch (err) {
- WebAuthnHandler.showErrorMessage(err);
- return;
- }
- let registrationFinishResponse;
- try {
- registrationFinishResponse = await this.post("webauthnRegisterFinishUrl", null, {
- id: attestation.id,
- rawId: this.encodeBuffer(attestation.rawId),
- type: attestation.type,
- response: {
- attestationObject: this.encodeBuffer(attestation.response.attestationObject),
- clientDataJSON: this.encodeBuffer(attestation.response.clientDataJSON),
- },
- });
- } catch (err) {
- WebAuthnHandler.showErrorMessage(err);
- return;
- }
- if (!registrationFinishResponse.ok) {
- throw new Error(`Registration failed with HTTP status code ${registrationFinishResponse.status}`);
- }
- const jsonData = await registrationFinishResponse.json();
- window.location.href = jsonData.redirect;
- }
- async login(username, abortController) {
- let loginBeginResponse;
- try {
- loginBeginResponse = await this.get("webauthnLoginBeginUrl", username);
- } catch (err) {
- WebAuthnHandler.showErrorMessage(err);
- return;
- }
- let credentialRequestOptions;
- try {
- credentialRequestOptions = await loginBeginResponse.json();
- } catch (err) {
- WebAuthnHandler.showErrorMessage("Failed to parse login options");
- return;
- }
- credentialRequestOptions.publicKey.challenge = this.decodeBuffer(credentialRequestOptions.publicKey.challenge);
- if (Object.hasOwn(credentialRequestOptions.publicKey, 'allowCredentials')) {
- credentialRequestOptions.publicKey.allowCredentials.forEach((credential) => {
- credential.id = this.decodeBuffer(credential.id);
- });
- }
- if (abortController) {
- credentialRequestOptions.signal = abortController.signal;
- credentialRequestOptions.mediation = "conditional";
- }
- let assertion;
- try {
- assertion = await navigator.credentials.get(credentialRequestOptions);
- }
- catch (err) {
- // Swallow aborted conditional logins
- if (err instanceof DOMException && err.name === "AbortError") {
- return;
- }
- WebAuthnHandler.showErrorMessage(err);
- return;
- }
- if (!assertion) {
- return;
- }
- let loginFinishResponse;
- try {
- loginFinishResponse = await this.post("webauthnLoginFinishUrl", username, {
- id: assertion.id,
- rawId: this.encodeBuffer(assertion.rawId),
- type: assertion.type,
- response: {
- authenticatorData: this.encodeBuffer(assertion.response.authenticatorData),
- clientDataJSON: this.encodeBuffer(assertion.response.clientDataJSON),
- signature: this.encodeBuffer(assertion.response.signature),
- userHandle: this.encodeBuffer(assertion.response.userHandle),
- },
- });
- } catch (err) {
- WebAuthnHandler.showErrorMessage(err);
- return;
- }
- if (!loginFinishResponse.ok) {
- throw new Error(`Login failed with HTTP status code ${loginFinishResponse.status}`);
- }
- window.location.reload();
- }
- }
|