mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(auth): make passkey two-factor authentication opt-in (#2857)
This commit is contained in:
1 parent
3256af8d92
commit
ba7d8781cf
94 files changed
+7322
-2747
No files matched your search
+126
-8
@@ -2,6 +2,7 @@
|
||||
|
||||
import {ConfirmModal} from '@app/features/app/components/dialogs/ConfirmModal';
|
||||
import {SettingsTabSection} from '@app/features/app/components/dialogs/shared/SettingsTabLayout';
|
||||
import {BackupCodesModal} from '@app/features/auth/components/modals/BackupCodesModal';
|
||||
import {BackupCodesViewModal} from '@app/features/auth/components/modals/BackupCodesViewModal';
|
||||
import {openClaimAccountModal} from '@app/features/auth/components/modals/ClaimAccountModal';
|
||||
import {MfaTotpDisableModal} from '@app/features/auth/components/modals/MfaTotpDisableModal';
|
||||
@@ -16,6 +17,7 @@ import {Logger} from '@app/features/platform/utils/AppLogger';
|
||||
import {Button} from '@app/features/ui/button/Button';
|
||||
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
|
||||
import {modal} from '@app/features/ui/commands/ModalCommands';
|
||||
import {Switch} from '@app/features/ui/components/form/FormSwitch';
|
||||
import * as UserCommands from '@app/features/user/commands/UserCommands';
|
||||
import styles from '@app/features/user/components/modals/tabs/account_security_tab/SecurityTab.module.css';
|
||||
import type {User} from '@app/features/user/models/User';
|
||||
@@ -46,6 +48,43 @@ const VERIFY_EMAIL_BEFORE_PASSKEY_DESCRIPTOR = msg({
|
||||
message: 'Verify your email before adding a passkey.',
|
||||
comment: 'Security settings warning shown when an unverified account cannot register a new passkey.',
|
||||
});
|
||||
const PASSKEY_TWO_FACTOR_ENABLE_TITLE_DESCRIPTOR = msg({
|
||||
message: 'Use passkeys for two-factor authentication?',
|
||||
comment: 'Security settings: confirmation modal title for turning on passkey-based two-factor authentication.',
|
||||
});
|
||||
const PASSKEY_TWO_FACTOR_ENABLE_DESCRIPTION_DESCRIPTOR = msg({
|
||||
message:
|
||||
"You'll need one of your passkeys to sign in on a new device. If you lose every passkey you can be locked out of your account. Backup codes will be saved for you so you still have a way in.",
|
||||
comment: 'Security settings: confirmation modal body for turning on passkey-based two-factor authentication.',
|
||||
});
|
||||
const PASSKEY_TWO_FACTOR_ENABLE_CONFIRM_DESCRIPTOR = msg({
|
||||
message: 'Turn on',
|
||||
comment: 'Security settings: confirm button for turning on passkey-based two-factor authentication.',
|
||||
});
|
||||
const PASSKEY_TWO_FACTOR_DISABLE_TITLE_DESCRIPTOR = msg({
|
||||
message: 'Stop using passkeys for two-factor authentication?',
|
||||
comment: 'Security settings: confirmation modal title for turning off passkey-based two-factor authentication.',
|
||||
});
|
||||
const PASSKEY_TWO_FACTOR_DISABLE_DESCRIPTION_DESCRIPTOR = msg({
|
||||
message:
|
||||
"Your account will no longer be protected by two-factor authentication when you sign in with your password. You'll also lose elevated permissions in servers that require two-factor authentication.",
|
||||
comment:
|
||||
'Security settings: confirmation modal body for turning off passkey-based two-factor authentication when passkeys are the only second factor on the account.',
|
||||
});
|
||||
const PASSKEY_TWO_FACTOR_DISABLE_DESCRIPTION_WITH_TOTP_DESCRIPTOR = msg({
|
||||
message:
|
||||
"You won't be asked for a passkey after your password anymore. Your authenticator app stays as your second factor.",
|
||||
comment:
|
||||
'Security settings: confirmation modal body for turning off passkey-based two-factor authentication when the account also has an authenticator app.',
|
||||
});
|
||||
const PASSKEY_TWO_FACTOR_DISABLE_CONFIRM_DESCRIPTOR = msg({
|
||||
message: 'Turn off',
|
||||
comment: 'Security settings: confirm button for turning off passkey-based two-factor authentication.',
|
||||
});
|
||||
const COULD_NOT_UPDATE_PASSKEY_TWO_FACTOR_DESCRIPTOR = msg({
|
||||
message: "Couldn't update passkey two-factor authentication",
|
||||
comment: 'Title of the error modal shown when the passkey two-factor setting could not be saved.',
|
||||
});
|
||||
const ACCOUNT_ACCESS_DESCRIPTOR = msg({
|
||||
message: 'Account access',
|
||||
comment: 'Security settings section for third-party app access and signed-in devices.',
|
||||
@@ -85,6 +124,8 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
({user, isClaimed, passkeys, authorizedAppsSubmitting, onManageAuthorizedApps, onManageLinkedDevices}) => {
|
||||
const {i18n} = useLingui();
|
||||
const hasTotpMfa = user.authenticatorTypes?.includes(UserAuthenticatorTypes.TOTP) ?? false;
|
||||
const hasPasskeyMfa = user.authenticatorTypes?.includes(UserAuthenticatorTypes.WEBAUTHN) ?? false;
|
||||
const hasAnyMfa = hasTotpMfa || hasPasskeyMfa;
|
||||
const needsEmailVerification = user.email != null && user.verified === false;
|
||||
const hasReachedPasskeyLimit = passkeys.length >= 10;
|
||||
const canAddSecurityCredential = !needsEmailVerification;
|
||||
@@ -129,15 +170,60 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
)),
|
||||
);
|
||||
};
|
||||
const applyPasskeyTwoFactor = async (enabled: boolean) => {
|
||||
try {
|
||||
const backupCodes = await UserCommands.setWebAuthnTwoFactor(enabled);
|
||||
if (backupCodes && backupCodes.length > 0) {
|
||||
ModalCommands.pushWithKey(
|
||||
modal(() => (
|
||||
<BackupCodesModal
|
||||
backupCodes={backupCodes}
|
||||
user={user}
|
||||
data-flx="user.account-security-tab.security-tab.apply-passkey-two-factor.backup-codes-modal"
|
||||
/>
|
||||
)),
|
||||
'backup-codes',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to update passkey two-factor authentication', error);
|
||||
pushApiErrorModal(i18n, error, i18n._(COULD_NOT_UPDATE_PASSKEY_TWO_FACTOR_DESCRIPTOR));
|
||||
}
|
||||
};
|
||||
const handleTogglePasskeyTwoFactor = (enabled: boolean) => {
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={i18n._(
|
||||
enabled ? PASSKEY_TWO_FACTOR_ENABLE_TITLE_DESCRIPTOR : PASSKEY_TWO_FACTOR_DISABLE_TITLE_DESCRIPTOR,
|
||||
)}
|
||||
description={i18n._(
|
||||
enabled
|
||||
? PASSKEY_TWO_FACTOR_ENABLE_DESCRIPTION_DESCRIPTOR
|
||||
: hasTotpMfa
|
||||
? PASSKEY_TWO_FACTOR_DISABLE_DESCRIPTION_WITH_TOTP_DESCRIPTOR
|
||||
: PASSKEY_TWO_FACTOR_DISABLE_DESCRIPTION_DESCRIPTOR,
|
||||
)}
|
||||
primaryText={i18n._(
|
||||
enabled ? PASSKEY_TWO_FACTOR_ENABLE_CONFIRM_DESCRIPTOR : PASSKEY_TWO_FACTOR_DISABLE_CONFIRM_DESCRIPTOR,
|
||||
)}
|
||||
primaryVariant={enabled ? 'primary' : 'danger'}
|
||||
onPrimary={() => applyPasskeyTwoFactor(enabled)}
|
||||
data-flx="user.account-security-tab.security-tab.handle-toggle-passkey-two-factor.confirm-modal"
|
||||
/>
|
||||
)),
|
||||
);
|
||||
};
|
||||
const handleDeletePasskey = (credentialId: string) => {
|
||||
const passkey = passkeys.find((p) => p.id === credentialId);
|
||||
const isLastPasskeyWithMfa = hasPasskeyMfa && passkeys.length === 1;
|
||||
ModalCommands.push(
|
||||
modal(() => (
|
||||
<ConfirmModal
|
||||
title={i18n._(DELETE_PASSKEY_DESCRIPTOR)}
|
||||
description={
|
||||
passkey ? (
|
||||
<div data-flx="user.account-security-tab.security-tab.handle-delete-passkey.div">
|
||||
<div data-flx="user.account-security-tab.security-tab.handle-delete-passkey.div">
|
||||
{passkey ? (
|
||||
<Trans>
|
||||
Are you sure you want to delete the passkey{' '}
|
||||
<strong data-flx="user.account-security-tab.security-tab.handle-delete-passkey.strong">
|
||||
@@ -145,10 +231,28 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
</strong>
|
||||
?
|
||||
</Trans>
|
||||
</div>
|
||||
) : (
|
||||
<Trans>Are you sure you want to delete this passkey?</Trans>
|
||||
)
|
||||
) : (
|
||||
<Trans>Are you sure you want to delete this passkey?</Trans>
|
||||
)}
|
||||
{isLastPasskeyWithMfa && (
|
||||
<div
|
||||
className={styles.warningText}
|
||||
data-flx="user.account-security-tab.security-tab.handle-delete-passkey.warning-text"
|
||||
>
|
||||
{hasTotpMfa ? (
|
||||
<Trans>
|
||||
This is your last passkey. Once it's gone you won't be able to use a passkey as your second
|
||||
factor.
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
This is your last passkey and passkeys are your second factor. Deleting it turns off two-factor
|
||||
authentication for your account, and your current backup codes will stop working.
|
||||
</Trans>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
primaryText={i18n._(DELETE_PASSKEY_DESCRIPTOR)}
|
||||
primaryVariant="danger"
|
||||
@@ -257,7 +361,7 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{hasTotpMfa && (
|
||||
{hasAnyMfa && (
|
||||
<div
|
||||
className={styles.divider}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.divider"
|
||||
@@ -303,7 +407,7 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
</SettingsTabSection>
|
||||
<SettingsTabSection
|
||||
title={<Trans>Passkeys</Trans>}
|
||||
description={<Trans>Use passkeys for passwordless sign-in and two-factor authentication</Trans>}
|
||||
description={<Trans>Use passkeys to sign in without a password</Trans>}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.settings-tab-section--3"
|
||||
>
|
||||
<div className={styles.row} data-flx="user.account-security-tab.security-tab.security-tab-content.row--3">
|
||||
@@ -335,6 +439,20 @@ export const SecurityTabContent: React.FC<SecurityTabProps> = observer(
|
||||
<Trans>Add passkey</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
{passkeys.length > 0 && (
|
||||
<div
|
||||
className={styles.divider}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.passkey-two-factor-divider"
|
||||
>
|
||||
<Switch
|
||||
label={<Trans>Require a passkey as your second factor</Trans>}
|
||||
description={<Trans>Ask for a passkey after your password when you sign in</Trans>}
|
||||
value={hasPasskeyMfa}
|
||||
onChange={handleTogglePasskeyTwoFactor}
|
||||
data-flx="user.account-security-tab.security-tab.security-tab-content.switch.passkey-two-factor"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{passkeys.length > 0 && (
|
||||
<div
|
||||
className={styles.divider}
|
||||
|
||||
+42
-3
@@ -131,8 +131,28 @@ const ADD_PASSKEY_DESCRIPTOR = msg({
|
||||
message: 'Add passkey',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const USE_PASSKEYS_FOR_PASSWORDLESS_SIGN_IN_AND_TWO_DESCRIPTOR = msg({
|
||||
message: 'Use passkeys for passwordless sign-in and two-factor authentication',
|
||||
const USE_PASSKEYS_TO_SIGN_IN_WITHOUT_A_PASSWORD_DESCRIPTOR = msg({
|
||||
message: 'Use passkeys to sign in without a password',
|
||||
comment: 'Settings search entry description. One-line summary of what the settings search entry controls.',
|
||||
});
|
||||
const PASSKEY_TWO_FACTOR_DESCRIPTOR = msg({
|
||||
message: 'Require a passkey as your second factor',
|
||||
comment: 'Settings search entry label. Also used as a search synonym in the settings search bar.',
|
||||
});
|
||||
const SECOND_FACTOR_DESCRIPTOR = msg({
|
||||
message: 'Second factor',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const REQUIRE_PASSKEY_DESCRIPTOR = msg({
|
||||
message: 'Require passkey',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const TWO_FA_DESCRIPTOR = msg({
|
||||
message: '2FA',
|
||||
comment: 'Settings search synonym. Used to match this term when the user types it in the settings search bar.',
|
||||
});
|
||||
const ASK_FOR_A_PASSKEY_AFTER_YOUR_PASSWORD_DESCRIPTOR = msg({
|
||||
message: 'Ask for a passkey after your password when you sign in',
|
||||
comment: 'Settings search entry description. One-line summary of what the settings search entry controls.',
|
||||
});
|
||||
const DISABLE_DESCRIPTOR = msg({
|
||||
@@ -235,7 +255,26 @@ export const accountSecurityIndex: Array<SearchableSettingDescriptor> = [
|
||||
HARDWARE_KEY_DESCRIPTOR,
|
||||
ADD_PASSKEY_DESCRIPTOR,
|
||||
],
|
||||
description: USE_PASSKEYS_FOR_PASSWORDLESS_SIGN_IN_AND_TWO_DESCRIPTOR,
|
||||
description: USE_PASSKEYS_TO_SIGN_IN_WITHOUT_A_PASSWORD_DESCRIPTOR,
|
||||
isVisible: shouldShowClaimedAccountSettings,
|
||||
},
|
||||
{
|
||||
id: 'account-passkey-two-factor',
|
||||
tabType: 'account_security',
|
||||
sectionId: 'security',
|
||||
label: PASSKEY_TWO_FACTOR_DESCRIPTOR,
|
||||
keywords: [
|
||||
PASSKEY_DESCRIPTOR,
|
||||
PASSKEYS_DESCRIPTOR,
|
||||
TWO_FACTOR_DESCRIPTOR,
|
||||
SECOND_FACTOR_DESCRIPTOR,
|
||||
REQUIRE_PASSKEY_DESCRIPTOR,
|
||||
TWO_FA_DESCRIPTOR,
|
||||
MULTI_FACTOR_DESCRIPTOR,
|
||||
AUTHENTICATION_DESCRIPTOR,
|
||||
SECURITY_DESCRIPTOR,
|
||||
],
|
||||
description: ASK_FOR_A_PASSKEY_AFTER_YOUR_PASSWORD_DESCRIPTOR,
|
||||
isVisible: shouldShowClaimedAccountSettings,
|
||||
},
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user