feat(core): Allow enforcement of MFA usage on instance (#16556)

Co-authored-by: Marc Littlemore <marc@n8n.io>
Co-authored-by: Csaba Tuncsik <csaba.tuncsik@gmail.com>
This commit is contained in:
Andreas Fitzek
2025-07-02 11:03:10 +02:00
committed by GitHub
parent 060acd2db8
commit 657e5a3b3a
56 changed files with 619 additions and 88 deletions

View File

@@ -137,6 +137,7 @@ export interface FrontendSettings {
ldap: boolean;
saml: boolean;
oidc: boolean;
mfaEnforcement: boolean;
logStreaming: boolean;
advancedExecutionFilters: boolean;
variables: boolean;
@@ -167,6 +168,7 @@ export interface FrontendSettings {
};
mfa: {
enabled: boolean;
enforced: boolean;
};
folders: {
enabled: boolean;

View File

@@ -63,6 +63,10 @@ export class LicenseState {
return this.isLicensed('feat:oidc');
}
isMFAEnforcementLicensed() {
return this.isLicensed('feat:mfaEnforcement');
}
isApiKeyScopesLicensed() {
return this.isLicensed('feat:apiKeyScopes');
}

View File

@@ -9,6 +9,7 @@ export const LICENSE_FEATURES = {
LDAP: 'feat:ldap',
SAML: 'feat:saml',
OIDC: 'feat:oidc',
MFA_ENFORCEMENT: 'feat:mfaEnforcement',
LOG_STREAMING: 'feat:logStreaming',
ADVANCED_EXECUTION_FILTERS: 'feat:advancedExecutionFilters',
VARIABLES: 'feat:variables',

View File

@@ -114,6 +114,7 @@ export interface PublicUser {
isOwner?: boolean;
featureFlags?: FeatureFlags; // External type from n8n-workflow
lastActiveAt?: Date | null;
mfaAuthenticated?: boolean;
}
export type UserSettings = Pick<User, 'id' | 'settings'>;
@@ -367,6 +368,10 @@ export type APIRequest<
browserId?: string;
};
export type AuthenticationInformation = {
usedMfa: boolean;
};
export type AuthenticatedRequest<
RouteParams = {},
ResponseBody = {},
@@ -374,6 +379,7 @@ export type AuthenticatedRequest<
RequestQuery = {},
> = Omit<APIRequest<RouteParams, ResponseBody, RequestBody, RequestQuery>, 'user' | 'cookies'> & {
user: User;
authInfo?: AuthenticationInformation;
cookies: Record<string, string | undefined>;
headers: express.Request['headers'] & {
'push-ref': string;

View File

@@ -9,6 +9,8 @@ interface RouteOptions {
usesTemplates?: boolean;
/** When this flag is set to true, auth cookie isn't validated, and req.user will not be set */
skipAuth?: boolean;
/** When this flag is set to true, the auth cookie does not enforce MFA to be used in the token */
allowSkipMFA?: boolean;
/** When these options are set, calls to this endpoint are rate limited using the options */
rateLimit?: boolean | RateLimit;
}
@@ -26,6 +28,7 @@ const RouteFactory =
routeMetadata.middlewares = options.middlewares ?? [];
routeMetadata.usesTemplates = options.usesTemplates ?? false;
routeMetadata.skipAuth = options.skipAuth ?? false;
routeMetadata.allowSkipMFA = options.allowSkipMFA ?? false;
routeMetadata.rateLimit = options.rateLimit;
};

View File

@@ -33,6 +33,7 @@ export interface RouteMetadata {
middlewares: RequestHandler[];
usesTemplates: boolean;
skipAuth: boolean;
allowSkipMFA: boolean;
rateLimit?: boolean | RateLimit;
licenseFeature?: BooleanLicenseFeature;
accessScope?: AccessScope;

View File

@@ -19,7 +19,7 @@ export const RESOURCES = {
securityAudit: ['generate'] as const,
sourceControl: ['pull', 'push', 'manage'] as const,
tag: [...DEFAULT_OPERATIONS] as const,
user: ['resetPassword', 'changeRole', ...DEFAULT_OPERATIONS] as const,
user: ['resetPassword', 'changeRole', 'enforceMfa', ...DEFAULT_OPERATIONS] as const,
variable: [...DEFAULT_OPERATIONS] as const,
workersView: ['manage'] as const,
workflow: ['share', 'execute', 'move', ...DEFAULT_OPERATIONS] as const,
@@ -34,7 +34,7 @@ export const API_KEY_RESOURCES = {
variable: ['create', 'update', 'delete', 'list'] as const,
securityAudit: ['generate'] as const,
project: ['create', 'update', 'delete', 'list'] as const,
user: ['read', 'list', 'create', 'changeRole', 'delete'] as const,
user: ['read', 'list', 'create', 'changeRole', 'delete', 'enforceMfa'] as const,
execution: ['delete', 'read', 'list', 'get'] as const,
credential: ['create', 'move', 'delete'] as const,
sourceControl: ['pull'] as const,

View File

@@ -6,6 +6,7 @@ export const OWNER_API_KEY_SCOPES: ApiKeyScope[] = [
'user:create',
'user:changeRole',
'user:delete',
'user:enforceMfa',
'sourceControl:pull',
'securityAudit:generate',
'project:create',

View File

@@ -56,6 +56,7 @@ export const GLOBAL_OWNER_SCOPES: Scope[] = [
'user:list',
'user:resetPassword',
'user:changeRole',
'user:enforceMfa',
'variable:create',
'variable:read',
'variable:update',