mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00
🛡️ fix: OTP Verification For 2FA Disable Operation (#8975)
This commit is contained in:
parent
edf33bedcb
commit
7e4c8a5d0d
6 changed files with 58 additions and 30 deletions
|
@ -99,10 +99,36 @@ const confirm2FA = async (req, res) => {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable 2FA by clearing the stored secret and backup codes.
|
* 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) => {
|
const disable2FA = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user.id;
|
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 });
|
await updateUser(userId, { totpSecret: null, backupCodes: [], twoFactorEnabled: false });
|
||||||
return res.status(200).json();
|
return res.status(200).json();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import BackupCodesItem from './BackupCodesItem';
|
||||||
import { useAuthContext } from '~/hooks';
|
import { useAuthContext } from '~/hooks';
|
||||||
|
|
||||||
function Account() {
|
function Account() {
|
||||||
const user = useAuthContext();
|
const { user } = useAuthContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
<div className="flex flex-col gap-3 p-1 text-sm text-text-primary">
|
||||||
|
@ -17,12 +17,12 @@ function Account() {
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<Avatar />
|
<Avatar />
|
||||||
</div>
|
</div>
|
||||||
{user?.user?.provider === 'local' && (
|
{user?.provider === 'local' && (
|
||||||
<>
|
<>
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<EnableTwoFactorItem />
|
<EnableTwoFactorItem />
|
||||||
</div>
|
</div>
|
||||||
{user?.user?.twoFactorEnabled && (
|
{user?.twoFactorEnabled && (
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<BackupCodesItem />
|
<BackupCodesItem />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,8 +39,8 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||||
const [secret, setSecret] = useState<string>('');
|
const [secret, setSecret] = useState<string>('');
|
||||||
const [otpauthUrl, setOtpauthUrl] = useState<string>('');
|
const [otpauthUrl, setOtpauthUrl] = useState<string>('');
|
||||||
const [downloaded, setDownloaded] = useState<boolean>(false);
|
const [downloaded, setDownloaded] = useState<boolean>(false);
|
||||||
const [disableToken, setDisableToken] = useState<string>('');
|
|
||||||
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
||||||
|
const [_disableToken, setDisableToken] = useState<string>('');
|
||||||
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [isDialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
const [verificationToken, setVerificationToken] = useState<string>('');
|
const [verificationToken, setVerificationToken] = useState<string>('');
|
||||||
const [phase, setPhase] = useState<Phase>(user?.twoFactorEnabled ? 'disable' : 'setup');
|
const [phase, setPhase] = useState<Phase>(user?.twoFactorEnabled ? 'disable' : 'setup');
|
||||||
|
@ -166,32 +166,26 @@ const TwoFactorAuthentication: React.FC = () => {
|
||||||
payload.token = token.trim();
|
payload.token = token.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
verify2FAMutate(payload, {
|
disable2FAMutate(payload, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
disable2FAMutate(undefined, {
|
showToast({ message: localize('com_ui_2fa_disabled') });
|
||||||
onSuccess: () => {
|
setDialogOpen(false);
|
||||||
showToast({ message: localize('com_ui_2fa_disabled') });
|
setUser(
|
||||||
setDialogOpen(false);
|
(prev) =>
|
||||||
setUser(
|
({
|
||||||
(prev) =>
|
...prev,
|
||||||
({
|
totpSecret: '',
|
||||||
...prev,
|
backupCodes: [],
|
||||||
totpSecret: '',
|
twoFactorEnabled: false,
|
||||||
backupCodes: [],
|
}) as TUser,
|
||||||
twoFactorEnabled: false,
|
);
|
||||||
}) as TUser,
|
setPhase('setup');
|
||||||
);
|
setOtpauthUrl('');
|
||||||
setPhase('setup');
|
|
||||||
setOtpauthUrl('');
|
|
||||||
},
|
|
||||||
onError: () =>
|
|
||||||
showToast({ message: localize('com_ui_2fa_disable_error'), status: 'error' }),
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }),
|
onError: () => showToast({ message: localize('com_ui_2fa_invalid'), status: 'error' }),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[verify2FAMutate, disable2FAMutate, showToast, localize, setUser],
|
[disable2FAMutate, showToast, localize, setUser],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -134,12 +134,12 @@ export const useConfirmTwoFactorMutation = (): UseMutationResult<
|
||||||
export const useDisableTwoFactorMutation = (): UseMutationResult<
|
export const useDisableTwoFactorMutation = (): UseMutationResult<
|
||||||
t.TDisable2FAResponse,
|
t.TDisable2FAResponse,
|
||||||
unknown,
|
unknown,
|
||||||
void,
|
t.TDisable2FARequest | undefined,
|
||||||
unknown
|
unknown
|
||||||
> => {
|
> => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation(() => dataService.disableTwoFactor(), {
|
return useMutation((payload?: t.TDisable2FARequest) => dataService.disableTwoFactor(payload), {
|
||||||
onSuccess: (data) => {
|
onSuccess: () => {
|
||||||
queryClient.setQueryData([QueryKeys.user, '2fa'], null);
|
queryClient.setQueryData([QueryKeys.user, '2fa'], null);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -815,8 +815,8 @@ export function confirmTwoFactor(payload: t.TVerify2FARequest): Promise<t.TVerif
|
||||||
return request.post(endpoints.confirmTwoFactor(), payload);
|
return request.post(endpoints.confirmTwoFactor(), payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function disableTwoFactor(): Promise<t.TDisable2FAResponse> {
|
export function disableTwoFactor(payload?: t.TDisable2FARequest): Promise<t.TDisable2FAResponse> {
|
||||||
return request.post(endpoints.disableTwoFactor());
|
return request.post(endpoints.disableTwoFactor(), payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function regenerateBackupCodes(): Promise<t.TRegenerateBackupCodesResponse> {
|
export function regenerateBackupCodes(): Promise<t.TRegenerateBackupCodesResponse> {
|
||||||
|
|
|
@ -413,6 +413,14 @@ export type TVerify2FATempResponse = {
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request for disabling 2FA.
|
||||||
|
*/
|
||||||
|
export type TDisable2FARequest = {
|
||||||
|
token?: string;
|
||||||
|
backupCode?: string;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response from disabling 2FA.
|
* Response from disabling 2FA.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue