bewcloud/public/components/auth/MultiFactorAuthSettings.js

390 lines
15 KiB
JavaScript
Raw Normal View History

import { useSignal } from '@preact/signals';
import { startRegistration } from '@simplewebauthn/browser';
const methodTypeLabels = {
totp: 'Authenticator App',
passkey: 'Passkey',
email: 'Email'
};
const methodTypeDescriptions = {
totp: 'Use an authenticator app like Aegis Authenticator or Google Authenticator to generate codes.',
passkey: 'Use biometric authentication or security keys.',
email: 'Receive codes in your email.'
};
const availableMethodTypes = ['totp', 'passkey', 'email'];
export default function MultiFactorAuthSettings({
methods
}) {
const setupData = useSignal(null);
const isLoading = useSignal(false);
const error = useSignal(null);
const success = useSignal(null);
const showDisableForm = useSignal(null);
const verificationToken = useSignal('');
const disablePassword = useSignal('');
const enabledMethods = methods.filter(method => method.enabled);
const hasMultiFactorAuthEnabled = enabledMethods.length > 0;
const setupPasskey = async () => {
const beginResponse = await fetch('/api/auth/multi-factor/passkey/setup-begin', {
method: 'POST',
body: JSON.stringify({})
});
if (!beginResponse.ok) {
throw new Error(`Failed to begin passkey registration! ${beginResponse.statusText} ${await beginResponse.text()}`);
}
const beginData = await beginResponse.json();
if (!beginData.success) {
throw new Error(beginData.error || 'Failed to begin passkey registration.');
}
const registrationResponse = await startRegistration({
optionsJSON: beginData.options
});
const completeRequestBody = {
methodId: beginData.sessionData.methodId,
challenge: beginData.sessionData.challenge,
registrationResponse
};
const completeResponse = await fetch('/api/auth/multi-factor/passkey/setup-complete', {
method: 'POST',
body: JSON.stringify(completeRequestBody)
});
if (!completeResponse.ok) {
throw new Error(`Failed to complete passkey registration! ${completeResponse.statusText} ${await completeResponse.text()}`);
}
const completeData = await completeResponse.json();
if (!completeData.success) {
throw new Error(completeData.error || 'Failed to complete passkey registration.');
}
setupData.value = {
methodId: beginData.sessionData.methodId,
type: 'passkey'
};
};
const setupTOTP = async () => {
const response = await fetch('/api/auth/multi-factor/totp/setup', {
method: 'POST',
body: JSON.stringify({})
});
if (!response.ok) {
throw new Error(`Failed to setup TOTP multi-factor authentication. ${response.statusText} ${await response.text()}`);
}
const data = await response.json();
if (!data.success || !data.data) {
throw new Error(data.error || 'Failed to setup TOTP multi-factor authentication.');
}
setupData.value = {
type: 'totp',
secret: data.data.secret,
qrCodeUrl: data.data.qrCodeUrl,
backupCodes: data.data.backupCodes,
methodId: data.data.methodId
};
};
const setupEmail = async () => {
const response = await fetch('/api/auth/multi-factor/email/setup', {
method: 'POST',
body: JSON.stringify({})
});
if (!response.ok) {
throw new Error(`Failed to setup email multi-factor authentication. Please check your SMTP settings are valid and try again. ${response.statusText} ${await response.text()}`);
}
const data = await response.json();
if (!data.success || !data.data) {
throw new Error(data.error || 'Failed to setup email multi-factor authentication.');
}
setupData.value = {
type: 'email',
methodId: data.data.methodId
};
};
const setupMultiFactorAuth = async type => {
isLoading.value = true;
error.value = null;
try {
if (type === 'totp') {
await setupTOTP();
} else if (type === 'passkey') {
await setupPasskey();
} else if (type === 'email') {
await setupEmail();
}
} catch (setupError) {
error.value = setupError.message;
} finally {
isLoading.value = false;
}
};
const enableMultiFactorAuth = async () => {
if (!setupData.value) {
error.value = 'No setup data available';
return;
}
if (setupData.value.type !== 'passkey' && !verificationToken.value) {
error.value = 'Please enter a verification code/token';
return;
}
isLoading.value = true;
error.value = null;
try {
const requestBody = {
methodId: setupData.value.methodId,
code: setupData.value.type === 'passkey' ? 'passkey-verified' : verificationToken.value
};
const response = await fetch('/api/auth/multi-factor/enable', {
method: 'POST',
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`Failed to enable multi-factor authentication method. ${response.statusText} ${await response.text()}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to enable multi-factor authentication method.');
}
success.value = 'Multi-factor authentication method has been enabled successfully! Reloading...';
setupData.value = null;
verificationToken.value = '';
setTimeout(() => {
window.location.reload();
}, 2000);
} catch (enableError) {
error.value = enableError.message;
} finally {
isLoading.value = false;
}
};
const disableMultiFactorAuth = async (methodId, disableAll = false) => {
if (!disablePassword.value) {
error.value = 'Please enter your password';
return;
}
isLoading.value = true;
error.value = null;
try {
const requestBody = {
methodId,
password: disablePassword.value,
disableAll
};
const response = await fetch('/api/auth/multi-factor/disable', {
method: 'POST',
body: JSON.stringify(requestBody)
});
if (!response.ok) {
throw new Error(`Failed to disable multi-factor authentication method. ${response.statusText} ${await response.text()}`);
}
const data = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to disable multi-factor authentication method.');
}
success.value = 'Multi-factor authentication method has been disabled successfully! Reloading...';
showDisableForm.value = null;
disablePassword.value = '';
setTimeout(() => {
window.location.reload();
}, 2000);
} catch (disableError) {
error.value = disableError.message;
} finally {
isLoading.value = false;
}
};
const cancelSetup = () => {
setupData.value = null;
verificationToken.value = '';
error.value = null;
};
const cancelDisable = () => {
showDisableForm.value = null;
disablePassword.value = '';
error.value = null;
};
return h("section", {
class: "mb-16"
}, h("h2", {
class: "text-2xl mb-4 text-left px-4 max-w-3xl mx-auto lg:min-w-96"
}, "Multi-Factor Authentication (MFA)"), h("section", {
class: "px-4 max-w-3xl mx-auto lg:min-w-96"
}, error.value ? h("section", {
class: "notification-error mb-4"
}, h("p", null, error.value)) : null, success.value ? h("section", {
class: "notification-success mb-4"
}, h("p", null, success.value)) : null, h("p", {
class: "mb-6"
}, "Multi-factor authentication adds an extra layer of security to your account by requiring additional verification beyond your password."), availableMethodTypes.filter(type => !enabledMethods.some(method => method.type === type)).length > 0 ? h("section", {
class: "mb-6 mt-4"
}, h("h3", {
class: "text-lg font-semibold mb-4"
}, "Available Authentication Methods"), h("section", {
class: "space-y-4"
}, availableMethodTypes.filter(type => !enabledMethods.some(method => method.type === type) && setupData.value?.type !== type).map(type => h("section", {
key: type,
class: "border rounded-lg p-4"
}, h("section", {
class: "flex items-center justify-between"
}, h("section", null, h("h4", {
class: "font-medium"
}, methodTypeLabels[type]), h("p", {
class: "text-sm text-gray-400"
}, methodTypeDescriptions[type])), h("button", {
type: "button",
onClick: () => setupMultiFactorAuth(type),
disabled: isLoading.value,
class: "button-secondary"
}, isLoading.value ? '...' : 'Add')))))) : null, setupData.value && setupData.value.type === 'totp' ? h("section", {
class: "mb-6"
}, h("h3", {
class: "text-lg font-semibold mb-4"
}, "Setup Authenticator App"), h("section", {
class: "mb-6"
}, h("p", {
class: "mb-4"
}, "1. Scan this QR code with your authenticator app (Aegis Authenticator, Google Authenticator, etc.):"), h("section", {
class: "flex justify-center mb-4 max-w-sm mx-auto"
}, h("img", {
src: setupData.value.qrCodeUrl,
alt: "TOTP QR Code",
class: "border-8 border-white"
})), h("p", {
class: "text-sm text-gray-400 mb-4"
}, "Or manually enter this secret:", ' ', h("code", {
class: "bg-gray-200 px-2 py-1 rounded text-gray-900"
}, setupData.value.secret))), h("section", {
class: "mb-6"
}, h("p", {
class: "mb-4"
}, "2. Save these backup codes ", h("strong", {
class: "font-bold text-sky-500"
}, "NOW"), " in a safe place:"), h("section", {
class: "bg-gray-200 border rounded p-4 font-mono text-sm text-gray-900"
}, setupData.value.backupCodes.map((code, index) => h("section", {
key: index,
class: "mb-1"
}, code))), h("p", {
class: "text-sm text-gray-400 mt-2"
}, "These codes can be used to access your account if you lose your authenticator device.", ' ', h("strong", {
class: "font-bold text-sky-500"
}, "They won't be visible again"), ".")), h("fieldset", {
class: "block mb-6"
}, h("label", {
class: "text-slate-300 block pb-1"
}, "3. Enter the 6-digit code from your authenticator app:"), h("input", {
type: "text",
value: verificationToken.value,
onInput: event => verificationToken.value = event.target.value,
placeholder: "123456",
class: "mt-1 input-field",
maxLength: 6
})), h("section", {
class: "flex justify-end gap-2 mt-8 mb-4"
}, h("button", {
type: "button",
onClick: cancelSetup,
disabled: isLoading.value,
class: "button-secondary"
}, "Cancel"), h("button", {
type: "button",
onClick: enableMultiFactorAuth,
disabled: isLoading.value || !verificationToken.value,
class: "button"
}, isLoading.value ? 'Enabling...' : 'Enable TOTP MFA'))) : null, setupData.value && setupData.value.type === 'passkey' ? h("section", {
class: "mb-6"
}, h("h3", {
class: "text-lg font-semibold mb-4"
}, "Passkey Setup Complete"), h("p", {
class: "mb-4"
}, "Your passkey has been successfully registered! You can now enable it for multi-factor authentication."), h("section", {
class: "flex justify-end gap-2 mt-8 mb-4"
}, h("button", {
type: "button",
onClick: cancelSetup,
disabled: isLoading.value,
class: "button-secondary"
}, "Cancel"), h("button", {
type: "button",
onClick: enableMultiFactorAuth,
disabled: isLoading.value,
class: "button"
}, isLoading.value ? 'Enabling...' : 'Enable Passkey MFA'))) : null, setupData.value && setupData.value.type === 'email' ? h("section", {
class: "mb-6"
}, h("h3", {
class: "text-lg font-semibold mb-4"
}, "Setup Email"), h("fieldset", {
class: "block mb-6"
}, h("label", {
class: "text-slate-300 block pb-1"
}, "Enter the 6-digit code you received in your email:"), h("input", {
type: "text",
value: verificationToken.value,
onInput: event => verificationToken.value = event.target.value,
placeholder: "123456",
class: "mt-1 input-field",
maxLength: 6
})), h("section", {
class: "flex justify-end gap-2 mt-8 mb-4"
}, h("button", {
type: "button",
onClick: cancelSetup,
disabled: isLoading.value,
class: "button-secondary"
}, "Cancel"), h("button", {
type: "button",
onClick: enableMultiFactorAuth,
disabled: isLoading.value || !verificationToken.value,
class: "button"
}, isLoading.value ? 'Enabling...' : 'Enable Email MFA'))) : null, hasMultiFactorAuthEnabled && !showDisableForm.value ? h("section", null, h("section", {
class: "mb-6"
}, h("h3", {
class: "text-lg font-semibold mb-4"
}, "Active Authentication Methods"), enabledMethods.map(method => h("section", {
key: method.id,
class: "border rounded-lg p-4 mb-4"
}, h("section", {
class: "flex items-center justify-between"
}, h("section", null, h("section", {
class: `flex items-center ${method.type === 'totp' && typeof method.backupCodesCount !== 'undefined' ? 'mb-2' : ''}`
}, h("span", {
class: "inline-block w-3 h-3 bg-green-500 rounded-full mr-2"
}), h("span", {
class: "font-medium"
}, method.name)), method.type === 'totp' && typeof method.backupCodesCount !== 'undefined' ? h("p", {
class: "text-sm text-gray-600"
}, method.backupCodesCount > 0 ? `${method.backupCodesCount} backup codes remaining` : 'No backup codes remaining') : null), h("button", {
type: "button",
onClick: () => showDisableForm.value = method.id,
class: "button-secondary"
}, "Disable"))))), h("section", {
class: "flex justify-end mt-8 mb-4"
}, h("button", {
type: "button",
onClick: () => showDisableForm.value = 'all',
class: "button-danger"
}, "Disable All MFA"))) : null, showDisableForm.value ? h("section", {
class: "mb-6"
}, h("h3", {
class: "text-lg font-semibold mb-4"
}, showDisableForm.value === 'all' ? 'Disable All Multi-Factor Authentication' : 'Disable Authentication Method'), h("p", {
class: "mb-4"
}, showDisableForm.value === 'all' ? 'This will disable all multi-factor authentication methods and make your account less secure.' : 'This will disable this authentication method.', " Please enter your password to confirm."), h("fieldset", {
class: "block mb-4"
}, h("label", {
class: "text-slate-300 block pb-1"
}, "Password"), h("input", {
type: "password",
value: disablePassword.value,
onInput: event => disablePassword.value = event.target.value,
placeholder: "Enter your password",
class: "mt-1 input-field"
})), h("section", {
class: "flex justify-end gap-2 mt-8 mb-4"
}, h("button", {
type: "button",
onClick: cancelDisable,
disabled: isLoading.value,
class: "button-secondary"
}, "Cancel"), h("button", {
type: "button",
onClick: () => disableMultiFactorAuth(showDisableForm.value === 'all' ? undefined : showDisableForm.value || undefined, showDisableForm.value === 'all'),
disabled: isLoading.value || !disablePassword.value,
class: "button-danger"
}, isLoading.value ? 'Disabling...' : 'Disable'))) : null));
}