feat(core): Add MFA (#4767)
https://linear.app/n8n/issue/ADO-947/sync-branch-with-master-and-fix-fe-e2e-tets --------- Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
1
packages/cli/src/Mfa/constants.ts
Normal file
1
packages/cli/src/Mfa/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const MFA_FEATURE_ENABLED = 'mfa.enabled';
|
||||
21
packages/cli/src/Mfa/helpers.ts
Normal file
21
packages/cli/src/Mfa/helpers.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import config from '@/config';
|
||||
import * as Db from '@/Db';
|
||||
import { MFA_FEATURE_ENABLED } from './constants';
|
||||
|
||||
export const isMfaFeatureEnabled = () => config.get(MFA_FEATURE_ENABLED);
|
||||
|
||||
const isMfaFeatureDisabled = () => !isMfaFeatureEnabled();
|
||||
|
||||
const getUsersWithMfaEnabled = async () =>
|
||||
Db.collections.User.count({ where: { mfaEnabled: true } });
|
||||
|
||||
export const handleMfaDisable = async () => {
|
||||
if (isMfaFeatureDisabled()) {
|
||||
// check for users with MFA enabled, and if there are
|
||||
// users, then keep the feature enabled
|
||||
const users = await getUsersWithMfaEnabled();
|
||||
if (users) {
|
||||
config.set(MFA_FEATURE_ENABLED, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
79
packages/cli/src/Mfa/mfa.service.ts
Normal file
79
packages/cli/src/Mfa/mfa.service.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { AES, enc } from 'crypto-js';
|
||||
import { TOTPService } from './totp.service';
|
||||
import { Service } from 'typedi';
|
||||
import { UserRepository } from '@/databases/repositories';
|
||||
|
||||
@Service()
|
||||
export class MfaService {
|
||||
constructor(
|
||||
private userRepository: UserRepository,
|
||||
public totp: TOTPService,
|
||||
private encryptionKey: string,
|
||||
) {}
|
||||
|
||||
public generateRecoveryCodes(n = 10) {
|
||||
return Array.from(Array(n)).map(() => uuid());
|
||||
}
|
||||
|
||||
public generateEncryptedRecoveryCodes() {
|
||||
return this.generateRecoveryCodes().map((code) =>
|
||||
AES.encrypt(code, this.encryptionKey).toString(),
|
||||
);
|
||||
}
|
||||
|
||||
public async saveSecretAndRecoveryCodes(userId: string, secret: string, recoveryCodes: string[]) {
|
||||
const { encryptedSecret, encryptedRecoveryCodes } = this.encryptSecretAndRecoveryCodes(
|
||||
secret,
|
||||
recoveryCodes,
|
||||
);
|
||||
return this.userRepository.update(userId, {
|
||||
mfaSecret: encryptedSecret,
|
||||
mfaRecoveryCodes: encryptedRecoveryCodes,
|
||||
});
|
||||
}
|
||||
|
||||
public encryptSecretAndRecoveryCodes(rawSecret: string, rawRecoveryCodes: string[]) {
|
||||
const encryptedSecret = AES.encrypt(rawSecret, this.encryptionKey).toString(),
|
||||
encryptedRecoveryCodes = rawRecoveryCodes.map((code) =>
|
||||
AES.encrypt(code, this.encryptionKey).toString(),
|
||||
);
|
||||
return {
|
||||
encryptedRecoveryCodes,
|
||||
encryptedSecret,
|
||||
};
|
||||
}
|
||||
|
||||
private decryptSecretAndRecoveryCodes(mfaSecret: string, mfaRecoveryCodes: string[]) {
|
||||
return {
|
||||
decryptedSecret: AES.decrypt(mfaSecret, this.encryptionKey).toString(enc.Utf8),
|
||||
decryptedRecoveryCodes: mfaRecoveryCodes.map((code) =>
|
||||
AES.decrypt(code, this.encryptionKey).toString(enc.Utf8),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
public async getSecretAndRecoveryCodes(userId: string) {
|
||||
const { mfaSecret, mfaRecoveryCodes } = await this.userRepository.findOneOrFail({
|
||||
where: { id: userId },
|
||||
select: ['id', 'mfaSecret', 'mfaRecoveryCodes'],
|
||||
});
|
||||
return this.decryptSecretAndRecoveryCodes(mfaSecret ?? '', mfaRecoveryCodes ?? []);
|
||||
}
|
||||
|
||||
public async enableMfa(userId: string) {
|
||||
await this.userRepository.update(userId, { mfaEnabled: true });
|
||||
}
|
||||
|
||||
public encryptRecoveryCodes(mfaRecoveryCodes: string[]) {
|
||||
return mfaRecoveryCodes.map((code) => AES.encrypt(code, this.encryptionKey).toString());
|
||||
}
|
||||
|
||||
public async disableMfa(userId: string) {
|
||||
await this.userRepository.update(userId, {
|
||||
mfaEnabled: false,
|
||||
mfaSecret: null,
|
||||
mfaRecoveryCodes: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
36
packages/cli/src/Mfa/totp.service.ts
Normal file
36
packages/cli/src/Mfa/totp.service.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import OTPAuth from 'otpauth';
|
||||
export class TOTPService {
|
||||
generateSecret(): string {
|
||||
return new OTPAuth.Secret()?.base32;
|
||||
}
|
||||
|
||||
generateTOTPUri({
|
||||
issuer = 'n8n',
|
||||
secret,
|
||||
label,
|
||||
}: {
|
||||
secret: string;
|
||||
label: string;
|
||||
issuer?: string;
|
||||
}) {
|
||||
return new OTPAuth.TOTP({
|
||||
secret: OTPAuth.Secret.fromBase32(secret),
|
||||
issuer,
|
||||
label,
|
||||
}).toString();
|
||||
}
|
||||
|
||||
verifySecret({ secret, token, window = 1 }: { secret: string; token: string; window?: number }) {
|
||||
return new OTPAuth.TOTP({
|
||||
secret: OTPAuth.Secret.fromBase32(secret),
|
||||
}).validate({ token, window }) === null
|
||||
? false
|
||||
: true;
|
||||
}
|
||||
|
||||
generateTOTP(secret: string) {
|
||||
return OTPAuth.TOTP.generate({
|
||||
secret: OTPAuth.Secret.fromBase32(secret),
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user