Files
upstream-rocketchat/apps/meteor/server/lib/2fa/functions/totp.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Code scanning - action / CodeQL-Build (push) Failing after 2s
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

155 lines
4.1 KiB
TypeScript

import { Users } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { notifyOnUserChange, notifyOnUserChangeAsync } from '../../notifyListener';
import { TOTP } from '../lib/totp';
const requireUser = async (userId: string | null) => {
if (!userId) {
throw new Meteor.Error('not-authorized');
}
const user = await Users.findOneById(userId);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'Invalid user');
}
return user;
};
export const enableTotp = async (userId: string | null): Promise<{ secret: string; url: string }> => {
const user = await requireUser(userId);
if (!user.username) {
throw new Meteor.Error('error-invalid-user', 'Invalid user');
}
if (user.services?.totp?.enabled) {
throw new Meteor.Error('error-2fa-already-enabled');
}
const secret = TOTP.generateSecret();
await Users.disable2FAAndSetTempSecretByUserId(user._id, secret.base32);
return {
secret: secret.base32,
url: TOTP.generateOtpauthURL(secret, user.username),
};
};
export const disableTotp = async (userId: string | null, code: string): Promise<boolean> => {
const user = await requireUser(userId);
if (!user.services?.totp?.enabled) {
return false;
}
const verified = await TOTP.verify({
secret: user.services.totp.secret,
token: code,
userId: user._id,
backupTokens: user.services.totp.hashedBackup,
});
if (!verified) {
return false;
}
const { modifiedCount } = await Users.disable2FAByUserId(user._id);
if (!modifiedCount) {
return false;
}
void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { 'services.totp.enabled': false } });
return true;
};
export const validateTotpTempToken = async (userId: string | null, userToken: string, authToken?: string): Promise<{ codes: string[] }> => {
const user = await requireUser(userId);
if (!user.services?.totp?.tempSecret) {
throw new Meteor.Error('invalid-totp');
}
const verified = await TOTP.verify({
secret: user.services.totp.tempSecret,
token: userToken,
});
if (!verified) {
throw new Meteor.Error('invalid-totp');
}
const { codes, hashedCodes } = TOTP.generateCodes();
await Users.enable2FAAndSetSecretAndCodesByUserId(user._id, user.services.totp.tempSecret, hashedCodes);
if (authToken) {
const hashedToken = Accounts._hashLoginToken(authToken);
const { modifiedCount } = await Users.removeNonPATLoginTokensExcept(user._id, hashedToken);
if (modifiedCount > 0) {
void notifyOnUserChangeAsync(async () => {
const refreshed = await Users.findOneById(user._id, {
projection: { 'services.resume.loginTokens': 1, 'services.totp': 1 },
});
return {
clientAction: 'updated',
id: user._id,
diff: {
'services.resume.loginTokens': refreshed?.services?.resume?.loginTokens,
...(refreshed?.services?.totp && { 'services.totp.enabled': refreshed.services.totp.enabled }),
},
};
});
} else {
void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { 'services.totp.enabled': true } });
}
} else {
void notifyOnUserChange({ clientAction: 'updated', id: user._id, diff: { 'services.totp.enabled': true } });
}
return { codes };
};
export const regenerateTotpCodes = async (userId: string | null, userToken: string): Promise<{ codes: string[] } | undefined> => {
const user = await requireUser(userId);
if (!user.services?.totp?.enabled) {
throw new Meteor.Error('invalid-totp');
}
const verified = await TOTP.verify({
secret: user.services.totp.secret,
token: userToken,
userId: user._id,
backupTokens: user.services.totp.hashedBackup,
});
if (!verified) {
return undefined;
}
const { codes, hashedCodes } = TOTP.generateCodes();
await Users.update2FABackupCodesByUserId(user._id, hashedCodes);
return { codes };
};
export const codesRemainingTotp = async (userId: string | null): Promise<{ remaining: number }> => {
const user = await requireUser(userId);
if (!user.services?.totp?.enabled) {
throw new Meteor.Error('invalid-totp');
}
return {
remaining: user.services.totp.hashedBackup?.length ?? 0,
};
};