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:
96
packages/cli/src/controllers/mfa.controller.ts
Normal file
96
packages/cli/src/controllers/mfa.controller.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { Authorized, Delete, Get, Post, RestController } from '@/decorators';
|
||||
import { AuthenticatedRequest, MFA } from '@/requests';
|
||||
import { BadRequestError } from '@/ResponseHelper';
|
||||
import { MfaService } from '@/Mfa/mfa.service';
|
||||
@Authorized()
|
||||
@RestController('/mfa')
|
||||
export class MFAController {
|
||||
constructor(private mfaService: MfaService) {}
|
||||
|
||||
@Get('/qr')
|
||||
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')
|
||||
async activateMFA(req: MFA.Activate) {
|
||||
const { token = null } = req.body;
|
||||
const { id, mfaEnabled } = req.user;
|
||||
|
||||
const { decryptedSecret: secret, decryptedRecoveryCodes: recoveryCodes } =
|
||||
await this.mfaService.getSecretAndRecoveryCodes(id);
|
||||
|
||||
if (!token) 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, token, window: 10 });
|
||||
|
||||
if (!verified)
|
||||
throw new BadRequestError('MFA token expired. Close the modal and enable MFA again', 997);
|
||||
|
||||
await this.mfaService.enableMfa(id);
|
||||
}
|
||||
|
||||
@Delete('/disable')
|
||||
async disableMFA(req: AuthenticatedRequest) {
|
||||
const { id } = req.user;
|
||||
|
||||
await this.mfaService.disableMfa(id);
|
||||
}
|
||||
|
||||
@Post('/verify')
|
||||
async verifyMFA(req: MFA.Verify) {
|
||||
const { id } = req.user;
|
||||
const { token } = req.body;
|
||||
|
||||
const { decryptedSecret: secret } = await this.mfaService.getSecretAndRecoveryCodes(id);
|
||||
|
||||
if (!token) throw new BadRequestError('Token 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, token });
|
||||
|
||||
if (!verified) throw new BadRequestError('MFA secret could not be verified');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user