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.
*/