From 7e4c8a5d0d2dbe5bf8fd272ff6acafb27d24744f Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 10 Aug 2025 15:05:16 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20fix:=20OTP=20Verificati?= =?UTF-8?q?on=20For=202FA=20Disable=20Operation=20(#8975)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server/controllers/TwoFactorController.js | 26 +++++++++++++ .../Nav/SettingsTabs/Account/Account.tsx | 6 +-- .../Account/TwoFactorAuthentication.tsx | 38 ++++++++----------- client/src/data-provider/Auth/mutations.ts | 6 +-- packages/data-provider/src/data-service.ts | 4 +- packages/data-provider/src/types.ts | 8 ++++ 6 files changed, 58 insertions(+), 30 deletions(-) diff --git a/api/server/controllers/TwoFactorController.js b/api/server/controllers/TwoFactorController.js index 44baf92ee..7b1fc9291 100644 --- a/api/server/controllers/TwoFactorController.js +++ b/api/server/controllers/TwoFactorController.js @@ -99,10 +99,36 @@ const confirm2FA = async (req, res) => { /** * Disable 2FA by clearing the stored secret and backup codes. + * Requires verification with either TOTP token or backup code if 2FA is fully enabled. */ const disable2FA = async (req, res) => { try { const userId = req.user.id; + const { token, backupCode } = req.body; + const user = await getUserById(userId); + + if (!user || !user.totpSecret) { + return res.status(400).json({ message: '2FA is not setup for this user' }); + } + + if (user.twoFactorEnabled) { + const secret = await getTOTPSecret(user.totpSecret); + let isVerified = false; + + if (token) { + isVerified = await verifyTOTP(secret, token); + } else if (backupCode) { + isVerified = await verifyBackupCode({ user, backupCode }); + } else { + return res + .status(400) + .json({ message: 'Either token or backup code is required to disable 2FA' }); + } + + if (!isVerified) { + return res.status(401).json({ message: 'Invalid token or backup code' }); + } + } await updateUser(userId, { totpSecret: null, backupCodes: [], twoFactorEnabled: false }); return res.status(200).json(); } catch (err) { diff --git a/client/src/components/Nav/SettingsTabs/Account/Account.tsx b/client/src/components/Nav/SettingsTabs/Account/Account.tsx index c1dfad190..27f442f96 100644 --- a/client/src/components/Nav/SettingsTabs/Account/Account.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/Account.tsx @@ -7,7 +7,7 @@ import BackupCodesItem from './BackupCodesItem'; import { useAuthContext } from '~/hooks'; function Account() { - const user = useAuthContext(); + const { user } = useAuthContext(); return (
@@ -17,12 +17,12 @@ function Account() {
- {user?.user?.provider === 'local' && ( + {user?.provider === 'local' && ( <>
- {user?.user?.twoFactorEnabled && ( + {user?.twoFactorEnabled && (
diff --git a/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx b/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx index 0541ffced..21cd6a980 100644 --- a/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx +++ b/client/src/components/Nav/SettingsTabs/Account/TwoFactorAuthentication.tsx @@ -39,8 +39,8 @@ const TwoFactorAuthentication: React.FC = () => { const [secret, setSecret] = useState(''); const [otpauthUrl, setOtpauthUrl] = useState(''); const [downloaded, setDownloaded] = useState(false); - const [disableToken, setDisableToken] = useState(''); const [backupCodes, setBackupCodes] = useState([]); + const [_disableToken, setDisableToken] = useState(''); const [isDialogOpen, setDialogOpen] = useState(false); const [verificationToken, setVerificationToken] = useState(''); const [phase, setPhase] = useState(user?.twoFactorEnabled ? 'disable' : 'setup'); @@ -166,32 +166,26 @@ const TwoFactorAuthentication: React.FC = () => { payload.token = token.trim(); } - verify2FAMutate(payload, { + disable2FAMutate(payload, { onSuccess: () => { - disable2FAMutate(undefined, { - onSuccess: () => { - showToast({ message: localize('com_ui_2fa_disabled') }); - setDialogOpen(false); - setUser( - (prev) => - ({ - ...prev, - totpSecret: '', - backupCodes: [], - twoFactorEnabled: false, - }) as TUser, - ); - setPhase('setup'); - setOtpauthUrl(''); - }, - onError: () => - showToast({ message: localize('com_ui_2fa_disable_error'), status: 'error' }), - }); + showToast({ message: localize('com_ui_2fa_disabled') }); + setDialogOpen(false); + setUser( + (prev) => + ({ + ...prev, + totpSecret: '', + backupCodes: [], + twoFactorEnabled: false, + }) as TUser, + ); + setPhase('setup'); + setOtpauthUrl(''); }, onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }), }); }, - [verify2FAMutate, disable2FAMutate, showToast, localize, setUser], + [disable2FAMutate, showToast, localize, setUser], ); return ( diff --git a/client/src/data-provider/Auth/mutations.ts b/client/src/data-provider/Auth/mutations.ts index 88fdcf06f..2d00320c0 100644 --- a/client/src/data-provider/Auth/mutations.ts +++ b/client/src/data-provider/Auth/mutations.ts @@ -134,12 +134,12 @@ export const useConfirmTwoFactorMutation = (): UseMutationResult< export const useDisableTwoFactorMutation = (): UseMutationResult< t.TDisable2FAResponse, unknown, - void, + t.TDisable2FARequest | undefined, unknown > => { const queryClient = useQueryClient(); - return useMutation(() => dataService.disableTwoFactor(), { - onSuccess: (data) => { + return useMutation((payload?: t.TDisable2FARequest) => dataService.disableTwoFactor(payload), { + onSuccess: () => { queryClient.setQueryData([QueryKeys.user, '2fa'], null); }, }); diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index faa62a37d..0f018d09c 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -815,8 +815,8 @@ export function confirmTwoFactor(payload: t.TVerify2FARequest): Promise { - return request.post(endpoints.disableTwoFactor()); +export function disableTwoFactor(payload?: t.TDisable2FARequest): Promise { + return request.post(endpoints.disableTwoFactor(), payload); } export function regenerateBackupCodes(): Promise { diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index b26e93ec8..d9e24ca30 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -413,6 +413,14 @@ export type TVerify2FATempResponse = { message?: string; }; +/** + * Request for disabling 2FA. + */ +export type TDisable2FARequest = { + token?: string; + backupCode?: string; +}; + /** * Response from disabling 2FA. */