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
155 lines
4.1 KiB
TypeScript
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,
|
|
};
|
|
};
|