diff --git a/app/2fa/server/functions/resetTOTP.ts b/app/2fa/server/functions/resetTOTP.ts new file mode 100644 index 0000000000000..059077929347b --- /dev/null +++ b/app/2fa/server/functions/resetTOTP.ts @@ -0,0 +1,68 @@ +import { Meteor } from 'meteor/meteor'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; + +import { settings } from '../../../settings/server'; +import * as Mailer from '../../../mailer'; +import { Users } from '../../../models/server/raw/index'; +import { IUser } from '../../../../definition/IUser'; + +const sendResetNotification = async function(uid: string): Promise { + const user: IUser = await Users.findOneById(uid, { projection: { language: 1, emails: 1 } }); + if (!user) { + throw new Meteor.Error('invalid-user'); + } + + const language = user.language || settings.get('Language') || 'en'; + const addresses = user.emails?.filter(({ verified }: { verified: boolean}) => verified).map((e) => e.address); + if (!addresses?.length) { + return; + } + + const t = (s: string): string => TAPi18n.__(s, { lng: language }); + const text = ` + ${ t('Your_TOTP_has_been_reset') } + + ${ t('TOTP_Reset_Other_Key_Warning') } + `; + const html = ` +

${ t('Your_TOTP_has_been_reset') }

+

${ t('TOTP_Reset_Other_Key_Warning') }

+ `; + + const from = settings.get('From_Email'); + const subject = t('TOTP_reset_email'); + + for (const address of addresses) { + Meteor.defer(() => { + try { + Mailer.send({ + to: address, + from, + subject, + text, + html, + } as any); + } catch (error) { + throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${ error.message }`, { + function: 'resetUserTOTP', + message: error.message, + }); + } + }); + } +}; + +export async function resetTOTP(userId: string, notifyUser = false): Promise { + if (notifyUser) { + await sendResetNotification(userId); + } + + const result = await Users.resetTOTPById(userId); + + if (result?.modifiedCount === 1) { + await Users.removeResumeService(userId); + return true; + } + + return false; +} diff --git a/app/api/server/v1/users.js b/app/api/server/v1/users.js index b4adea3365d3f..7a745bb5cca64 100644 --- a/app/api/server/v1/users.js +++ b/app/api/server/v1/users.js @@ -23,6 +23,7 @@ import { findUsersToAutocomplete } from '../lib/users'; import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers'; +import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; API.v1.addRoute('users.create', { authRequired: true }, { post() { @@ -797,7 +798,7 @@ API.v1.addRoute('users.removeOtherTokens', { authRequired: true }, { }, }); -API.v1.addRoute('users.resetE2EKey', { authRequired: true, twoFactorRequired: true }, { +API.v1.addRoute('users.resetE2EKey', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, { post() { // reset own keys if (this.isUserFromParams()) { @@ -826,3 +827,31 @@ API.v1.addRoute('users.resetE2EKey', { authRequired: true, twoFactorRequired: tr return API.v1.success(); }, }); + +API.v1.addRoute('users.resetTOTP', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, { + post() { + // reset own keys + if (this.isUserFromParams()) { + Promise.await(resetTOTP(this.userId, false)); + return API.v1.success(); + } + + // reset other user keys + const user = this.getUserFromParams(); + if (!user) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } + + if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + + if (!hasPermission(Meteor.userId(), 'edit-other-user-totp')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + + Promise.await(resetTOTP(user._id, true)); + + return API.v1.success(); + }, +}); diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index 9bc311170a69d..1cada0ba7a5c9 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -43,6 +43,7 @@ Meteor.startup(function() { { _id: 'edit-other-user-password', roles: ['admin'] }, { _id: 'edit-other-user-avatar', roles: ['admin'] }, { _id: 'edit-other-user-e2ee', roles: ['admin'] }, + { _id: 'edit-other-user-totp', roles: ['admin'] }, { _id: 'edit-privileged-setting', roles: ['admin'] }, { _id: 'edit-room', roles: ['admin', 'owner', 'moderator'] }, { _id: 'edit-room-avatar', roles: ['admin', 'owner', 'moderator'] }, diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index f3b02ad63c84e..a3650f3ee66f5 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -517,4 +517,24 @@ export class UsersRaw extends BaseRaw { return this.update(query, update, { multi: true }); } + + resetTOTPById(userId) { + return this.col.updateOne({ + _id: userId, + }, { + $unset: { + 'services.totp': 1, + }, + }); + } + + removeResumeService(userId) { + return this.col.updateOne({ + _id: userId, + }, { + $unset: { + 'services.resume': 1, + }, + }); + } } diff --git a/client/admin/users/UserInfoActions.js b/client/admin/users/UserInfoActions.js index 0d53f2e950c1b..af0564b421398 100644 --- a/client/admin/users/UserInfoActions.js +++ b/client/admin/users/UserInfoActions.js @@ -26,6 +26,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) const canEditOtherUserInfo = usePermission('edit-other-user-info'); const canAssignAdminRole = usePermission('assign-admin-role'); const canResetE2EEKey = usePermission('edit-other-user-e2ee'); + const canResetTOTP = usePermission('edit-other-user-totp'); const canEditOtherUserActiveStatus = usePermission('edit-other-user-active-status'); const canDeleteUser = usePermission('delete-user'); @@ -98,6 +99,7 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) }, [_id, dispatchToastMessage, isAdmin, onChange, setAdminStatus, t]); const resetE2EEKeyRequest = useEndpoint('POST', 'users.resetE2EKey'); + const resetTOTPRequest = useEndpoint('POST', 'users.resetTOTP'); const resetE2EEKey = useCallback(async () => { setModal(); const result = await resetE2EEKeyRequest({ userId: _id }); @@ -110,6 +112,18 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) } }, [resetE2EEKeyRequest, onChange, setModal, t, _id]); + const resetTOTP = useCallback(async () => { + setModal(); + const result = await resetTOTPRequest({ userId: _id }); + + if (result) { + setModal( { setModal(); onChange(); }} + />); + } + }, [resetTOTPRequest, onChange, setModal, t, _id]); + const confirmResetE2EEKey = useCallback(() => { setModal(); }, [resetE2EEKey, t, setModal]); + const confirmResetTOTP = useCallback(() => { + setModal( setModal()} + onDelete={resetTOTP} + />); + }, [resetTOTP, t, setModal]); + const activeStatusQuery = useMemo(() => ({ userId: _id, activeStatus: !isActive, @@ -174,6 +197,11 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) label: t('Reset_E2E_Key'), action: confirmResetE2EEKey, } }, + ...canResetTOTP && enforcePassword && { resetTOTP: { + icon: 'key', + label: t('Reset_TOTP'), + action: confirmResetTOTP, + } }, ...canDeleteUser && { delete: { icon: 'trash', label: t('Delete'), @@ -200,7 +228,9 @@ export const UserInfoActions = ({ username, _id, isActive, isAdmin, onChange }) changeActiveStatus, enforcePassword, canResetE2EEKey, + canResetTOTP, confirmResetE2EEKey, + confirmResetTOTP, username, ]); diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 96ba98256e5ec..94a163f484540 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1323,10 +1323,12 @@ "E2E_Encryption_Password_Change": "Change Encryption Password", "E2E_Encryption_Password_Explanation": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.

This is end to end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store your password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on.", "E2E_key_reset_email": "E2E Key Reset Notification", + "TOTP_reset_email": "Two Factor TOTP Reset Notification", "E2E_password_reveal_text": "You can now create encrypted private groups and direct messages. You may also change existing private groups or DMs to encrypted.

This is end to end encryption so the key to encode/decode your messages will not be saved on the server. For that reason you need to store this password somewhere safe. You will be required to enter it on other devices you wish to use e2e encryption on. Learn more here!

Your password is: %s

This is an auto generated password, you can setup a new password for your encryption key any time from any browser you have entered the existing password.
This password is only stored on this browser until you store the password and dismiss this message.", "E2E_password_request_text": "To access your encrypted private groups and direct messages, enter your encryption password.
You need to enter this password to encode/decode your messages on every client you use, since the key is not stored on the server.", "E2E_Reset_Key_Explanation": "This option will remove your current E2E key and log you out.
When you login again, Rocket.Chat will generate you a new key and restore your access to any encrypted room that has one or more members online.
Due to the nature of the E2E encryption, Rocket.Chat will not be able to restore access to any encrypted room that has no member online.", "E2E_Reset_Other_Key_Warning": "Reset the current E2E key will log out the user. When the user login again, Rocket.Chat will generate a new key and restore the user access to any encrypted room that has one or more members online. Due to the nature of the E2E encryption, Rocket.Chat will not be able to restore access to any encrypted room that has no member online.", + "TOTP_Reset_Other_Key_Warning": "Reset the current Two Factor TOTP will log out the user. The user will be able to set the Two Factor again later.", "E2E_Reset_Email_Content": "You've been automatically logged out. When you login again, Rocket.Chat will generate a new key and restore your access to any encrypted room that has one or more members online. Due to the nature of the E2E encryption, Rocket.Chat will not be able to restore access to any encrypted room that has no member online.", "Edit": "Edit", "Edit_User": "Edit User", @@ -1343,6 +1345,7 @@ "edit-other-user-password": "Edit Other User Password", "edit-other-user-password_description": "Permission to modify other user's passwords. Requires edit-other-user-info permission.", "edit-other-user-e2ee": "Edit Other User E2E Encryption", + "edit-other-user-totp": "Edit Other User Two Factor TOTP", "edit-other-user-e2ee_description": "Permission to modify other user's E2E Encryption.", "edit-privileged-setting": "Edit privileged Setting", "edit-privileged-setting_description": "Permission to edit settings", @@ -3006,6 +3009,7 @@ "Resend_verification_email": "Resend verification email", "Reset": "Reset", "Reset_E2E_Key": "Reset E2E Key", + "Reset_TOTP": "Reset TOTP", "Reset_password": "Reset password", "Reset_section_settings": "Reset Section to Default", "Reset_Connection": "Reset Connection", @@ -3830,6 +3834,7 @@ "Users_in_role": "Users in role", "Users must use Two Factor Authentication": "Users must use Two Factor Authentication", "Users_key_has_been_reset": "User's key has been reset", + "Users_TOTP_has_been_reset": "User's TOTP has been reset", "Leave_the_description_field_blank_if_you_dont_want_to_show_the_role": "Leave the description field blank if you don't want to show the role", "Uses": "Uses", "Uses_left": "Uses left", @@ -4002,6 +4007,7 @@ "You_will_not_be_able_to_recover_file": "You will not be able to recover this file!", "You_wont_receive_email_notifications_because_you_have_not_verified_your_email": "You won't receive email notifications because you have not verified your email.", "Your_e2e_key_has_been_reset": "Your e2e key has been reset.", + "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.", "Your_email_address_has_changed": "Your email address has been changed.", "Your_email_has_been_queued_for_sending": "Your email has been queued for sending", "Your_entry_has_been_deleted": "Your entry has been deleted.",