feat(core): Add log streaming events for 2FA enable/disable (#23302)

This commit is contained in:
Marc Littlemore
2025-12-17 08:58:51 +00:00
committed by GitHub
parent 1c8e76d9ba
commit ca96ffc804
5 changed files with 131 additions and 0 deletions
@@ -4,6 +4,7 @@ import { Response } from 'express';
import { AuthService } from '@/auth/auth.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { EventService } from '@/events/event.service';
import { ExternalHooks } from '@/external-hooks';
import { MfaService } from '@/mfa/mfa.service';
import { MFA } from '@/requests';
@@ -15,6 +16,7 @@ export class MFAController {
private externalHooks: ExternalHooks,
private authService: AuthService,
private userRepository: UserRepository,
private eventService: EventService,
) {}
@Post('/enforce-mfa')
@@ -107,6 +109,16 @@ export class MFAController {
const updatedUser = await this.mfaService.enableMfa(id);
this.eventService.emit('user-mfa-enabled', {
user: {
id: req.user.id,
email: req.user.email,
firstName: req.user.firstName,
lastName: req.user.lastName,
role: req.user.role,
},
});
this.authService.issueCookie(res, updatedUser, verified, req.browserId);
}
@@ -132,6 +144,17 @@ export class MFAController {
await this.mfaService.disableMfaWithRecoveryCode(userId, mfaRecoveryCode);
}
this.eventService.emit('user-mfa-disabled', {
user: {
id: req.user.id,
email: req.user.email,
firstName: req.user.firstName,
lastName: req.user.lastName,
role: req.user.role,
},
disableMethod: mfaCodeDefined ? 'mfaCode' : 'recoveryCode',
});
const updatedUser = await this.userRepository.findOneOrFail({
where: { id: userId },
relations: ['role'],
@@ -73,6 +73,8 @@ export const eventNamesAudit = [
'n8n.audit.user.credentials.deleted',
'n8n.audit.user.api.created',
'n8n.audit.user.api.deleted',
'n8n.audit.user.mfa.enabled',
'n8n.audit.user.mfa.disabled',
'n8n.audit.package.installed',
'n8n.audit.package.updated',
'n8n.audit.package.deleted',
@@ -657,6 +657,85 @@ describe('LogStreamingEventRelay', () => {
},
});
});
it('should log on `user-mfa-enabled` event', () => {
const event: RelayEventMap['user-mfa-enabled'] = {
user: {
id: 'user505',
email: 'mfauser@example.com',
firstName: 'MFA',
lastName: 'User',
role: { slug: 'global:member' },
},
};
eventService.emit('user-mfa-enabled', event);
expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({
eventName: 'n8n.audit.user.mfa.enabled',
payload: {
userId: 'user505',
_email: 'mfauser@example.com',
_firstName: 'MFA',
_lastName: 'User',
globalRole: 'global:member',
},
});
});
it('should log on `user-mfa-disabled` event with mfaCode method', () => {
const event: RelayEventMap['user-mfa-disabled'] = {
user: {
id: 'user606',
email: 'mfadisable@example.com',
firstName: 'Disable',
lastName: 'MFA',
role: { slug: 'global:member' },
},
disableMethod: 'mfaCode',
};
eventService.emit('user-mfa-disabled', event);
expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({
eventName: 'n8n.audit.user.mfa.disabled',
payload: {
userId: 'user606',
_email: 'mfadisable@example.com',
_firstName: 'Disable',
_lastName: 'MFA',
globalRole: 'global:member',
disableMethod: 'mfaCode',
},
});
});
it('should log on `user-mfa-disabled` event with recoveryCode method', () => {
const event: RelayEventMap['user-mfa-disabled'] = {
user: {
id: 'user707',
email: 'recovery@example.com',
firstName: 'Recovery',
lastName: 'User',
role: { slug: GLOBAL_OWNER_ROLE.slug },
},
disableMethod: 'recoveryCode',
};
eventService.emit('user-mfa-disabled', event);
expect(eventBus.sendAuditEvent).toHaveBeenCalledWith({
eventName: 'n8n.audit.user.mfa.disabled',
payload: {
userId: 'user707',
_email: 'recovery@example.com',
_firstName: 'Recovery',
_lastName: 'User',
globalRole: 'global:owner',
disableMethod: 'recoveryCode',
},
});
});
});
describe('click events', () => {
@@ -181,6 +181,15 @@ export type RelayEventMap = {
fieldsChanged: string[];
};
'user-mfa-enabled': {
user: UserLike;
};
'user-mfa-disabled': {
user: UserLike;
disableMethod: 'mfaCode' | 'recoveryCode';
};
'user-signed-up': {
user: UserLike;
userType: AuthProviderType;
@@ -35,6 +35,8 @@ export class LogStreamingEventRelay extends EventRelay {
'user-invited': (event) => this.userInvited(event),
'user-reinvited': (event) => this.userReinvited(event),
'user-updated': (event) => this.userUpdated(event),
'user-mfa-enabled': (event) => this.userMfaEnabled(event),
'user-mfa-disabled': (event) => this.userMfaDisabled(event),
'user-signed-up': (event) => this.userSignedUp(event),
'user-logged-in': (event) => this.userLoggedIn(event),
'user-login-failed': (event) => this.userLoginFailed(event),
@@ -320,6 +322,22 @@ export class LogStreamingEventRelay extends EventRelay {
});
}
@Redactable()
private userMfaEnabled({ user }: RelayEventMap['user-mfa-enabled']) {
void this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.mfa.enabled',
payload: user,
});
}
@Redactable()
private userMfaDisabled({ user, disableMethod }: RelayEventMap['user-mfa-disabled']) {
void this.eventBus.sendAuditEvent({
eventName: 'n8n.audit.user.mfa.disabled',
payload: { ...user, disableMethod },
});
}
// #endregion
// #region Auth