feat(core): Invalidate all sessions when MFA is enabled/disabled (#15524)

This commit is contained in:
Ricardo Espinoza
2025-05-21 08:59:22 +02:00
committed by GitHub
parent a1a33deee5
commit 2a35c19ef9
18 changed files with 92 additions and 76 deletions
@@ -1,7 +1,6 @@
import { UserUpdateRequestDto } from '@n8n/api-types';
import type { User } from '@n8n/db';
import type { PublicUser } from '@n8n/db';
import { AuthUserRepository } from '@n8n/db';
import { InvalidAuthTokenRepository } from '@n8n/db';
import { UserRepository } from '@n8n/db';
import { Container } from '@n8n/di';
@@ -30,7 +29,6 @@ describe('MeController', () => {
const userService = mockInstance(UserService);
const userRepository = mockInstance(UserRepository);
const mockMfaService = mockInstance(MfaService);
mockInstance(AuthUserRepository);
mockInstance(InvalidAuthTokenRepository);
mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
const controller = Container.get(MeController);
@@ -171,6 +169,7 @@ describe('MeController', () => {
authIdentities: [],
role: 'global:owner',
mfaEnabled: true,
mfaSecret: 'secret',
});
const req = mock<AuthenticatedRequest>({ user, browserId });
const res = mock<Response>();
@@ -316,7 +315,7 @@ describe('MeController', () => {
it('should succeed when mfa code is correct', async () => {
const req = mock<AuthenticatedRequest>({
user: mock({ password: passwordHash, mfaEnabled: true }),
user: mock({ password: passwordHash, mfaEnabled: true, mfaSecret: 'secret' }),
browserId,
});
const res = mock<Response>();
@@ -1,7 +1,7 @@
import type { PushMessage } from '@n8n/api-types';
import type { BooleanLicenseFeature, NumericLicenseFeature } from '@n8n/constants';
import { LICENSE_FEATURES, LICENSE_QUOTAS, UNLIMITED_LICENSE_QUOTA } from '@n8n/constants';
import { AuthUserRepository, SettingsRepository, UserRepository } from '@n8n/db';
import { SettingsRepository, UserRepository } from '@n8n/db';
import { Patch, Post, RestController } from '@n8n/decorators';
import { Container } from '@n8n/di';
import { Request } from 'express';
@@ -149,7 +149,6 @@ export class E2EController {
private readonly passwordUtility: PasswordUtility,
private readonly eventBus: MessageEventBus,
private readonly userRepository: UserRepository,
private readonly authUserRepository: AuthUserRepository,
) {
license.isLicensed = (feature: BooleanLicenseFeature) => this.enabledFeatures[feature] ?? false;
@@ -280,7 +279,7 @@ export class E2EController {
const { encryptedRecoveryCodes, encryptedSecret } =
this.mfaService.encryptSecretAndRecoveryCodes(owner.mfaSecret, owner.mfaRecoveryCodes);
await this.authUserRepository.update(newOwner.user.id, {
await this.userRepository.update(newOwner.user.id, {
mfaSecret: encryptedSecret,
mfaRecoveryCodes: encryptedRecoveryCodes,
});
+14 -3
View File
@@ -1,5 +1,8 @@
import { UserRepository } from '@n8n/db';
import { Get, 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';
@@ -10,6 +13,8 @@ export class MFAController {
constructor(
private mfaService: MfaService,
private externalHooks: ExternalHooks,
private authService: AuthService,
private userRepository: UserRepository,
) {}
@Post('/can-enable')
@@ -59,7 +64,7 @@ export class MFAController {
}
@Post('/enable', { rateLimit: true })
async activateMFA(req: MFA.Activate) {
async activateMFA(req: MFA.Activate, res: Response) {
const { mfaCode = null } = req.body;
const { id, mfaEnabled } = req.user;
@@ -81,11 +86,13 @@ export class MFAController {
if (!verified)
throw new BadRequestError('MFA code expired. Close the modal and enable MFA again', 997);
await this.mfaService.enableMfa(id);
const updatedUser = await this.mfaService.enableMfa(id);
this.authService.issueCookie(res, updatedUser, req.browserId);
}
@Post('/disable', { rateLimit: true })
async disableMFA(req: MFA.Disable) {
async disableMFA(req: MFA.Disable, res: Response) {
const { id: userId } = req.user;
const { mfaCode, mfaRecoveryCode } = req.body;
@@ -105,6 +112,10 @@ export class MFAController {
} else if (mfaRecoveryCodeDefined) {
await this.mfaService.disableMfaWithRecoveryCode(userId, mfaRecoveryCode);
}
const updatedUser = await this.userRepository.findOneByOrFail({ id: userId });
this.authService.issueCookie(res, updatedUser, req.browserId);
}
@Post('/verify', { rateLimit: true })