Files
Agent-n8n/n8n-n8n-1.109.2/packages/cli/src/controllers/mfa.controller.ts
2025-09-08 04:48:28 +08:00

156 lines
4.7 KiB
TypeScript
Executable File

import { AuthenticatedRequest, UserRepository } from '@n8n/db';
import { Get, GlobalScope, Post, RestController } from '@n8n/decorators';
import { Response } from 'express';
import { AuthService } from '@/auth/auth.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ExternalHooks } from '@/external-hooks';
import { MfaService } from '@/mfa/mfa.service';
import { MFA } from '@/requests';
@RestController('/mfa')
export class MFAController {
constructor(
private mfaService: MfaService,
private externalHooks: ExternalHooks,
private authService: AuthService,
private userRepository: UserRepository,
) {}
@Post('/enforce-mfa')
@GlobalScope('user:enforceMfa')
async enforceMFA(req: MFA.Enforce) {
if (req.body.enforce && !(req.authInfo?.usedMfa ?? false)) {
// The current user tries to enforce MFA, but does not have
// MFA set up for them self. We are forbidding this, to
// help the user not lock them selfs out.
throw new BadRequestError(
'You must enable two-factor authentication on your own account before enforcing it for all users',
);
}
await this.mfaService.enforceMFA(req.body.enforce);
return;
}
@Post('/can-enable', {
allowSkipMFA: true,
})
async canEnableMFA(req: AuthenticatedRequest) {
await this.externalHooks.run('mfa.beforeSetup', [req.user]);
return;
}
@Get('/qr', {
allowSkipMFA: true,
})
async getQRCode(req: AuthenticatedRequest) {
const { email, id, mfaEnabled } = req.user;
if (mfaEnabled)
throw new BadRequestError(
'MFA already enabled. Disable it to generate new secret and recovery codes',
);
const { decryptedSecret: secret, decryptedRecoveryCodes: recoveryCodes } =
await this.mfaService.getSecretAndRecoveryCodes(id);
if (secret && recoveryCodes.length) {
const qrCode = this.mfaService.totp.generateTOTPUri({
secret,
label: email,
});
return {
secret,
recoveryCodes,
qrCode,
};
}
const newRecoveryCodes = this.mfaService.generateRecoveryCodes();
const newSecret = this.mfaService.totp.generateSecret();
const qrCode = this.mfaService.totp.generateTOTPUri({ secret: newSecret, label: email });
await this.mfaService.saveSecretAndRecoveryCodes(id, newSecret, newRecoveryCodes);
return {
secret: newSecret,
qrCode,
recoveryCodes: newRecoveryCodes,
};
}
@Post('/enable', { rateLimit: true, allowSkipMFA: true })
async activateMFA(req: MFA.Activate, res: Response) {
const { mfaCode = null } = req.body;
const { id, mfaEnabled } = req.user;
await this.externalHooks.run('mfa.beforeSetup', [req.user]);
const { decryptedSecret: secret, decryptedRecoveryCodes: recoveryCodes } =
await this.mfaService.getSecretAndRecoveryCodes(id);
if (!mfaCode) throw new BadRequestError('Token is required to enable MFA feature');
if (mfaEnabled) throw new BadRequestError('MFA already enabled');
if (!secret || !recoveryCodes.length) {
throw new BadRequestError('Cannot enable MFA without generating secret and recovery codes');
}
const verified = this.mfaService.totp.verifySecret({ secret, mfaCode, window: 10 });
if (!verified)
throw new BadRequestError('MFA code expired. Close the modal and enable MFA again', 997);
const updatedUser = await this.mfaService.enableMfa(id);
this.authService.issueCookie(res, updatedUser, verified, req.browserId);
}
@Post('/disable', { rateLimit: true })
async disableMFA(req: MFA.Disable, res: Response) {
const { id: userId } = req.user;
const { mfaCode, mfaRecoveryCode } = req.body;
const mfaCodeDefined = mfaCode && typeof mfaCode === 'string';
const mfaRecoveryCodeDefined = mfaRecoveryCode && typeof mfaRecoveryCode === 'string';
if (!mfaCodeDefined === !mfaRecoveryCodeDefined) {
throw new BadRequestError(
'Either MFA code or recovery code is required to disable MFA feature',
);
}
if (mfaCodeDefined) {
await this.mfaService.disableMfaWithMfaCode(userId, mfaCode);
} else if (mfaRecoveryCodeDefined) {
await this.mfaService.disableMfaWithRecoveryCode(userId, mfaRecoveryCode);
}
const updatedUser = await this.userRepository.findOneByOrFail({ id: userId });
this.authService.issueCookie(res, updatedUser, false, req.browserId);
}
@Post('/verify', { rateLimit: true, allowSkipMFA: true })
async verifyMFA(req: MFA.Verify) {
const { id } = req.user;
const { mfaCode } = req.body;
const { decryptedSecret: secret } = await this.mfaService.getSecretAndRecoveryCodes(id);
if (!mfaCode) throw new BadRequestError('MFA code is required to enable MFA feature');
if (!secret) throw new BadRequestError('No MFA secret se for this user');
const verified = this.mfaService.totp.verifySecret({ secret, mfaCode });
if (!verified) throw new BadRequestError('MFA secret could not be verified');
}
}