refactor(core)!: Remove basic-auth, external-jwt-auth, and no-auth options (#6362)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
This commit is contained in:
Iván Ovejero
2023-06-07 16:53:53 +02:00
committed by कारतोफ्फेलस्क्रिप्ट™
parent a45a2c8c41
commit 8c008f5d22
85 changed files with 297 additions and 831 deletions

View File

@@ -306,7 +306,6 @@ export interface IDiagnosticInfo {
databaseType: DatabaseType;
notificationsEnabled: boolean;
disableProductionWebhooksOnMainProcess: boolean;
basicAuthActive: boolean;
systemInfo: {
os: {
type?: string;
@@ -324,7 +323,6 @@ export interface IDiagnosticInfo {
};
deploymentType: string;
binaryDataMode: string;
n8n_multi_user_allowed: boolean;
smtp_set_up: boolean;
ldap_allowed: boolean;
saml_enabled: boolean;

View File

@@ -75,12 +75,10 @@ export class InternalHooks implements IInternalHooksClass {
db_type: diagnosticInfo.databaseType,
n8n_version_notifications_enabled: diagnosticInfo.notificationsEnabled,
n8n_disable_production_main_process: diagnosticInfo.disableProductionWebhooksOnMainProcess,
n8n_basic_auth_active: diagnosticInfo.basicAuthActive,
system_info: diagnosticInfo.systemInfo,
execution_variables: diagnosticInfo.executionVariables,
n8n_deployment_type: diagnosticInfo.deploymentType,
n8n_binary_data_mode: diagnosticInfo.binaryDataMode,
n8n_multi_user_allowed: diagnosticInfo.n8n_multi_user_allowed,
smtp_set_up: diagnosticInfo.smtp_set_up,
ldap_allowed: diagnosticInfo.ldap_allowed,
saml_enabled: diagnosticInfo.saml_enabled,

View File

@@ -12,7 +12,6 @@ import { User } from '@db/entities/User';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import { RoleRepository } from '@db/repositories';
import type { AuthProviderSyncHistory } from '@db/entities/AuthProviderSyncHistory';
import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
import { LdapManager } from './LdapManager.ee';
import {
@@ -37,10 +36,7 @@ import { InternalServerError } from '../ResponseHelper';
/**
* Check whether the LDAP feature is disabled in the instance
*/
export const isLdapEnabled = (): boolean => {
const license = Container.get(License);
return isUserManagementEnabled() && license.isLdapEnabled();
};
export const isLdapEnabled = () => Container.get(License).isLdapEnabled();
/**
* Check whether the LDAP feature is enabled in the instance

View File

@@ -87,17 +87,6 @@ export class ServiceUnavailableError extends ResponseError {
}
}
export function basicAuthAuthorizationError(resp: Response, realm: string, message?: string) {
resp.statusCode = 401;
resp.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
resp.json({ code: resp.statusCode, message });
}
export function jwtAuthAuthorizationError(resp: Response, message?: string) {
resp.statusCode = 403;
resp.json({ code: resp.statusCode, message });
}
export function sendSuccessResponse(
res: Response,
data: any,

View File

@@ -105,7 +105,6 @@ import {
getInstanceBaseUrl,
isEmailSetUp,
isSharingEnabled,
isUserManagementEnabled,
whereClause,
} from '@/UserManagement/UserManagementHelper';
import { UserManagementMailer } from '@/UserManagement/email';
@@ -145,8 +144,6 @@ import {
} from './Ldap/helpers';
import { AbstractServer } from './AbstractServer';
import { configureMetrics } from './metrics';
import { setupBasicAuth } from './middlewares/basicAuth';
import { setupExternalJWTAuth } from './middlewares/externalJWTAuth';
import { PostHogClient } from './posthog';
import { eventBus } from './eventbus';
import { Container } from 'typedi';
@@ -261,11 +258,7 @@ export class Server extends AbstractServer {
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'),
userManagement: {
enabled: isUserManagementEnabled(),
showSetupOnFirstLoad:
config.getEnv('userManagement.disabled') === false &&
config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
config.getEnv('userManagement.skipInstanceOwnerSetup') === false,
showSetupOnFirstLoad: config.getEnv('userManagement.isInstanceOwnerSetUp') === false,
smtpSetup: isEmailSetUp(),
authenticationMethod: getCurrentAuthenticationMethod(),
},
@@ -349,7 +342,6 @@ export class Server extends AbstractServer {
const cpus = os.cpus();
const binaryDataConfig = config.getEnv('binaryDataManager');
const diagnosticInfo: IDiagnosticInfo = {
basicAuthActive: config.getEnv('security.basicAuth.active'),
databaseType: config.getEnv('database.type'),
disableProductionWebhooksOnMainProcess: config.getEnv(
'endpoints.disableProductionWebhooksOnMainProcess',
@@ -385,7 +377,6 @@ export class Server extends AbstractServer {
},
deploymentType: config.getEnv('deployment.type'),
binaryDataMode: binaryDataConfig.mode,
n8n_multi_user_allowed: isUserManagementEnabled(),
smtp_set_up: config.getEnv('userManagement.emails.mode') === 'smtp',
ldap_allowed: isLdapCurrentAuthenticationMethod(),
saml_enabled: isSamlCurrentAuthenticationMethod(),
@@ -414,12 +405,9 @@ export class Server extends AbstractServer {
getSettingsForFrontend(): IN8nUISettings {
// refresh user management status
Object.assign(this.frontendSettings.userManagement, {
enabled: isUserManagementEnabled(),
authenticationMethod: getCurrentAuthenticationMethod(),
showSetupOnFirstLoad:
config.getEnv('userManagement.disabled') === false &&
config.getEnv('userManagement.isInstanceOwnerSetUp') === false &&
config.getEnv('userManagement.skipInstanceOwnerSetup') === false &&
config.getEnv('deployment.type').startsWith('desktop_') === false,
});
@@ -461,7 +449,7 @@ export class Server extends AbstractServer {
private registerControllers(ignoredEndpoints: Readonly<string[]>) {
const { app, externalHooks, activeWorkflowRunner, nodeTypes } = this;
const repositories = Db.collections;
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint, repositories.User);
setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint);
const logger = LoggerProxy;
const internalHooks = Container.get(InternalHooks);
@@ -552,19 +540,6 @@ export class Server extends AbstractServer {
`REST endpoint cannot be set to any of these values: ${ignoredEndpoints.join()} `,
);
// eslint-disable-next-line no-useless-escape
const authIgnoreRegex = new RegExp(`^\/(${ignoredEndpoints.join('|')})\/?.*$`);
// Check for basic auth credentials if activated
if (config.getEnv('security.basicAuth.active')) {
setupBasicAuth(this.app, config, authIgnoreRegex);
}
// Check for and validate JWT if configured
if (config.getEnv('security.jwtAuth.active')) {
setupExternalJWTAuth(this.app, config, authIgnoreRegex);
}
// ----------------------------------------
// Public API
// ----------------------------------------
@@ -578,7 +553,7 @@ export class Server extends AbstractServer {
this.app.use(cookieParser());
const { restEndpoint, app } = this;
setupPushHandler(restEndpoint, app, isUserManagementEnabled());
setupPushHandler(restEndpoint, app);
// Make sure that Vue history mode works properly
this.app.use(

View File

@@ -36,26 +36,8 @@ export function isEmailSetUp(): boolean {
return smtp && host && user && pass;
}
export function isUserManagementEnabled(): boolean {
// This can be simplified but readability is more important here
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
// Short circuit - if owner is set up, UM cannot be disabled.
// Users must reset their instance in order to do so.
return true;
}
// UM is disabled for desktop by default
if (config.getEnv('deployment.type').startsWith('desktop_')) {
return false;
}
return config.getEnv('userManagement.disabled') ? false : true;
}
export function isSharingEnabled(): boolean {
const license = Container.get(License);
return isUserManagementEnabled() && license.isSharingEnabled();
return Container.get(License).isSharingEnabled();
}
export async function getRoleId(scope: Role['scope'], name: Role['name']): Promise<Role['id']> {

View File

@@ -95,7 +95,7 @@ const setupUserManagement = async () => {
`INSERT INTO user (id, globalRoleId) values ("${uuid()}", ${instanceOwnerRole[0].insertId})`,
);
await connection.query(
"INSERT INTO \"settings\" (key, value, loadOnStartup) values ('userManagement.isInstanceOwnerSetUp', 'false', true), ('userManagement.skipInstanceOwnerSetup', 'false', true)",
"INSERT INTO \"settings\" (key, value, loadOnStartup) values ('userManagement.isInstanceOwnerSetUp', 'false', true)",
);
config.set('userManagement.isInstanceOwnerSetUp', false);

View File

@@ -18,31 +18,17 @@ import { isApiEnabled } from '@/PublicApi';
function getSecuritySettings() {
if (config.getEnv('deployment.type') === 'cloud') return null;
const userManagementEnabled = !config.getEnv('userManagement.disabled');
const basicAuthActive = config.getEnv('security.basicAuth.active');
const jwtAuthActive = config.getEnv('security.jwtAuth.active');
const isInstancePubliclyAccessible = !userManagementEnabled && !basicAuthActive && !jwtAuthActive;
const settings: Record<string, unknown> = {};
if (isInstancePubliclyAccessible) {
settings.publiclyAccessibleInstance =
'Important! Your n8n instance is publicly accessible. Any third party who knows your instance URL can access your data.'.toUpperCase();
}
settings.features = {
communityPackagesEnabled: config.getEnv('nodes.communityPackages.enabled'),
versionNotificationsEnabled: config.getEnv('versionNotifications.enabled'),
templatesEnabled: config.getEnv('templates.enabled'),
publicApiEnabled: isApiEnabled(),
userManagementEnabled,
};
settings.auth = {
authExcludeEndpoints: config.getEnv('security.excludeEndpoints') || 'none',
basicAuthActive,
jwtAuthActive,
};
settings.nodes = {

View File

@@ -9,7 +9,7 @@ import { getLogger } from '@/Logger';
import config from '@/config';
import * as Db from '@/Db';
import * as CrashJournal from '@/CrashJournal';
import { USER_MANAGEMENT_DOCS_URL, inTest } from '@/constants';
import { inTest } from '@/constants';
import { CredentialTypes } from '@/CredentialTypes';
import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { initErrorHandling } from '@/ErrorReporting';
@@ -78,24 +78,6 @@ export abstract class BaseCommand extends Command {
);
}
if (process.env.N8N_BASIC_AUTH_ACTIVE === 'true') {
LoggerProxy.warn(
`Basic auth has been deprecated and will be removed in a future version of n8n. For authentication, please consider User Management. To learn more: ${USER_MANAGEMENT_DOCS_URL}`,
);
}
if (process.env.N8N_JWT_AUTH_ACTIVE === 'true') {
LoggerProxy.warn(
`JWT auth has been deprecated and will be removed in a future version of n8n. For authentication, please consider User Management. To learn more: ${USER_MANAGEMENT_DOCS_URL}`,
);
}
if (process.env.N8N_USER_MANAGEMENT_DISABLED === 'true') {
LoggerProxy.warn(
`User Management will be mandatory in a future version of n8n. Please set up the instance owner. To learn more: ${USER_MANAGEMENT_DOCS_URL}`,
);
}
this.instanceId = this.userSettings.instanceId ?? '';
await Container.get(PostHogClient).init(this.instanceId);
await Container.get(InternalHooks).init(this.instanceId);

View File

@@ -56,10 +56,6 @@ export class Reset extends BaseCommand {
{ key: 'userManagement.isInstanceOwnerSetUp' },
{ value: 'false' },
);
await Db.collections.Settings.update(
{ key: 'userManagement.skipInstanceOwnerSetup' },
{ value: 'false' },
);
this.logger.info('Successfully reset the database to default user state.');
}

View File

@@ -492,82 +492,6 @@ export const schema = {
default: '',
env: 'N8N_AUTH_EXCLUDE_ENDPOINTS',
},
basicAuth: {
active: {
format: 'Boolean',
default: false,
env: 'N8N_BASIC_AUTH_ACTIVE',
doc: '[DEPRECATED] If basic auth should be activated for editor and REST-API',
},
user: {
format: String,
default: '',
env: 'N8N_BASIC_AUTH_USER',
doc: '[DEPRECATED] The name of the basic auth user',
},
password: {
format: String,
default: '',
env: 'N8N_BASIC_AUTH_PASSWORD',
doc: '[DEPRECATED] The password of the basic auth user',
},
hash: {
format: 'Boolean',
default: false,
env: 'N8N_BASIC_AUTH_HASH',
doc: '[DEPRECATED] If password for basic auth is hashed',
},
},
jwtAuth: {
active: {
format: 'Boolean',
default: false,
env: 'N8N_JWT_AUTH_ACTIVE',
doc: '[DEPRECATED] If JWT auth should be activated for editor and REST-API',
},
jwtHeader: {
format: String,
default: '',
env: 'N8N_JWT_AUTH_HEADER',
doc: '[DEPRECATED] The request header containing a signed JWT',
},
jwtHeaderValuePrefix: {
format: String,
default: '',
env: 'N8N_JWT_AUTH_HEADER_VALUE_PREFIX',
doc: '[DEPRECATED] The request header value prefix to strip (optional)',
},
jwksUri: {
format: String,
default: '',
env: 'N8N_JWKS_URI',
doc: '[DEPRECATED] The URI to fetch JWK Set for JWT authentication',
},
jwtIssuer: {
format: String,
default: '',
env: 'N8N_JWT_ISSUER',
doc: '[DEPRECATED] JWT issuer to expect (optional)',
},
jwtNamespace: {
format: String,
default: '',
env: 'N8N_JWT_NAMESPACE',
doc: '[DEPRECATED] JWT namespace to expect (optional)',
},
jwtAllowedTenantKey: {
format: String,
default: '',
env: 'N8N_JWT_ALLOWED_TENANT_KEY',
doc: '[DEPRECATED] JWT tenant key name to inspect within JWT namespace (optional)',
},
jwtAllowedTenant: {
format: String,
default: '',
env: 'N8N_JWT_ALLOWED_TENANT',
doc: '[DEPRECATED] JWT tenant to allow (optional)',
},
},
},
endpoints: {
@@ -726,12 +650,6 @@ export const schema = {
},
userManagement: {
disabled: {
doc: '[DEPRECATED] Disable user management and hide it completely.',
format: Boolean,
default: false,
env: 'N8N_USER_MANAGEMENT_DISABLED',
},
jwtSecret: {
doc: 'Set a specific JWT secret (optional - n8n can generate one)', // Generated @ start.ts
format: String,
@@ -744,12 +662,6 @@ export const schema = {
format: Boolean,
default: false,
},
skipInstanceOwnerSetup: {
// n8n loads this setting from DB on startup
doc: 'Whether to hide the prompt the first time n8n starts with UM enabled',
format: Boolean,
default: false,
},
emails: {
mode: {
doc: 'How to send emails',

View File

@@ -80,7 +80,6 @@ type ExceptionPaths = {
'nodes.exclude': string[] | undefined;
'nodes.include': string[] | undefined;
'userManagement.isInstanceOwnerSetUp': boolean;
'userManagement.skipInstanceOwnerSetup': boolean;
};
// -----------------------------------

View File

@@ -86,6 +86,3 @@ export const enum LICENSE_QUOTAS {
}
export const CREDENTIAL_BLANKING_VALUE = '__n8n_BLANK_VALUE_e5362baf-c777-4d57-a609-6eaf1f9e87f6';
export const USER_MANAGEMENT_DOCS_URL =
'https://docs.n8n.io/hosting/authentication/user-management-self-hosted';

View File

@@ -148,19 +148,4 @@ export class OwnerController {
return sanitizeUser(owner);
}
/**
* Persist that the instance owner setup has been skipped
*/
@Post('/skip-setup')
async skipSetup() {
await this.settingsRepository.update(
{ key: 'userManagement.skipInstanceOwnerSetup' },
{ value: JSON.stringify(true) },
);
this.config.set('userManagement.skipInstanceOwnerSetup', true);
return { success: true };
}
}

View File

@@ -32,7 +32,6 @@ import type {
import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { AuthIdentity } from '@db/entities/AuthIdentity';
import type { PostHogClient } from '@/posthog';
import { userManagementEnabledMiddleware } from '../middlewares/userManagementEnabled';
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
import type {
RoleRepository,
@@ -106,7 +105,7 @@ export class UsersController {
/**
* Send email invite(s) to one or multiple users and create user shell(s).
*/
@Post('/', { middlewares: [userManagementEnabledMiddleware] })
@Post('/')
async sendEmailInvites(req: UserRequest.Invite) {
if (isSamlLicensedAndEnabled()) {
this.logger.debug(

View File

@@ -0,0 +1,9 @@
import type { IrreversibleMigration, MigrationContext } from '@db/types';
export class RemoveSkipOwnerSetup1681134145997 implements IrreversibleMigration {
async up({ queryRunner, tablePrefix }: MigrationContext) {
await queryRunner.query(
`DELETE FROM ${tablePrefix}settings WHERE \`key\` = 'userManagement.skipInstanceOwnerSetup';`,
);
}
}

View File

@@ -40,6 +40,7 @@ import { CreateVariables1677501636753 } from './1677501636753-CreateVariables';
import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty';
import { MigrateIntegerKeysToString1690000000001 } from './1690000000001-MigrateIntegerKeysToString';
import { SeparateExecutionData1690000000030 } from './1690000000030-SeparateExecutionData';
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
export const mysqlMigrations: Migration[] = [
InitialMigration1588157391238,
@@ -83,4 +84,5 @@ export const mysqlMigrations: Migration[] = [
AddUserActivatedProperty1681134145996,
MigrateIntegerKeysToString1690000000001,
SeparateExecutionData1690000000030,
RemoveSkipOwnerSetup1681134145997,
];

View File

@@ -0,0 +1,9 @@
import type { IrreversibleMigration, MigrationContext } from '@db/types';
export class RemoveSkipOwnerSetup1681134145997 implements IrreversibleMigration {
async up({ queryRunner, tablePrefix }: MigrationContext) {
await queryRunner.query(
`DELETE FROM ${tablePrefix}settings WHERE key = 'userManagement.skipInstanceOwnerSetup';`,
);
}
}

View File

@@ -38,6 +38,7 @@ import { CreateVariables1677501636754 } from './1677501636754-CreateVariables';
import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty';
import { MigrateIntegerKeysToString1690000000000 } from './1690000000000-MigrateIntegerKeysToString';
import { SeparateExecutionData1690000000020 } from './1690000000020-SeparateExecutionData';
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
export const postgresMigrations: Migration[] = [
InitialMigration1587669153312,
@@ -79,4 +80,5 @@ export const postgresMigrations: Migration[] = [
AddUserActivatedProperty1681134145996,
MigrateIntegerKeysToString1690000000000,
SeparateExecutionData1690000000020,
RemoveSkipOwnerSetup1681134145997,
];

View File

@@ -0,0 +1,9 @@
import type { IrreversibleMigration, MigrationContext } from '@db/types';
export class RemoveSkipOwnerSetup1681134145997 implements IrreversibleMigration {
async up({ queryRunner, tablePrefix }: MigrationContext) {
await queryRunner.query(
`DELETE FROM "${tablePrefix}settings" WHERE key = 'userManagement.skipInstanceOwnerSetup';`,
);
}
}

View File

@@ -37,6 +37,7 @@ import { CreateVariables1677501636752 } from './1677501636752-CreateVariables';
import { AddUserActivatedProperty1681134145996 } from './1681134145996-AddUserActivatedProperty';
import { MigrateIntegerKeysToString1690000000002 } from './1690000000002-MigrateIntegerKeysToString';
import { SeparateExecutionData1690000000010 } from './1690000000010-SeparateExecutionData';
import { RemoveSkipOwnerSetup1681134145997 } from './1681134145997-RemoveSkipOwnerSetup';
const sqliteMigrations: Migration[] = [
InitialMigration1588102412422,
@@ -77,6 +78,7 @@ const sqliteMigrations: Migration[] = [
AddUserActivatedProperty1681134145996,
MigrateIntegerKeysToString1690000000002,
SeparateExecutionData1690000000010,
RemoveSkipOwnerSetup1681134145997,
];
export { sqliteMigrations };

View File

@@ -10,7 +10,6 @@ import type { AuthenticatedRequest } from '@/requests';
import config from '@/config';
import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants';
import { issueCookie, resolveJwtContent } from '@/auth/jwt';
import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
import type { UserRepository } from '@db/repositories';
import { canSkipAuth } from '@/decorators/registerController';
@@ -19,7 +18,7 @@ const jwtFromRequest = (req: Request) => {
return (req.cookies?.[AUTH_COOKIE_NAME] as string | undefined) ?? null;
};
const jwtAuth = (): RequestHandler => {
const userManagementJwtAuth = (): RequestHandler => {
const jwtStrategy = new Strategy(
{
jwtFromRequest,
@@ -79,11 +78,10 @@ export const setupAuthMiddlewares = (
app: Application,
ignoredEndpoints: Readonly<string[]>,
restEndpoint: string,
userRepository: UserRepository,
) => {
// needed for testing; not adding overhead since it directly returns if req.cookies exists
app.use(cookieParser());
app.use(jwtAuth());
app.use(userManagementJwtAuth());
app.use(async (req: Request, res: Response, next: NextFunction) => {
if (
@@ -101,15 +99,6 @@ export const setupAuthMiddlewares = (
return next();
}
// skip authentication if user management is disabled
if (!isUserManagementEnabled()) {
req.user = await userRepository.findOneOrFail({
relations: ['globalRole'],
where: {},
});
return next();
}
return passportMiddleware(req, res, next);
});

View File

@@ -1,57 +0,0 @@
import type { Application } from 'express';
import basicAuth from 'basic-auth';
// IMPORTANT! Do not switch to anther bcrypt library unless really necessary and
// tested with all possible systems like Windows, Alpine on ARM, FreeBSD, ...
import { compare } from 'bcryptjs';
import type { Config } from '@/config';
import { basicAuthAuthorizationError } from '@/ResponseHelper';
export const setupBasicAuth = (app: Application, config: Config, authIgnoreRegex: RegExp) => {
const basicAuthUser = config.getEnv('security.basicAuth.user');
if (basicAuthUser === '') {
throw new Error('Basic auth is activated but no user got defined. Please set one!');
}
const basicAuthPassword = config.getEnv('security.basicAuth.password');
if (basicAuthPassword === '') {
throw new Error('Basic auth is activated but no password got defined. Please set one!');
}
const basicAuthHashEnabled = config.getEnv('security.basicAuth.hash');
let validPassword: null | string = null;
app.use(async (req, res, next) => {
// Skip basic auth for a few listed endpoints or when instance owner has been setup
if (authIgnoreRegex.exec(req.url) || config.getEnv('userManagement.isInstanceOwnerSetUp')) {
return next();
}
const realm = 'n8n - Editor UI';
const basicAuthData = basicAuth(req);
if (basicAuthData === undefined) {
// Authorization data is missing
return basicAuthAuthorizationError(res, realm, 'Authorization is required!');
}
if (basicAuthData.name === basicAuthUser) {
if (basicAuthHashEnabled) {
if (validPassword === null && (await compare(basicAuthData.pass, basicAuthPassword))) {
// Password is valid so save for future requests
validPassword = basicAuthData.pass;
}
if (validPassword === basicAuthData.pass && validPassword !== null) {
// Provided hash is correct
return next();
}
} else if (basicAuthData.pass === basicAuthPassword) {
// Provided password is correct
return next();
}
}
// Provided authentication data is wrong
return basicAuthAuthorizationError(res, realm, 'Authorization data is wrong!');
});
};

View File

@@ -1,85 +0,0 @@
import type { Application } from 'express';
import jwt from 'jsonwebtoken';
import jwks from 'jwks-rsa';
import type { Config } from '@/config';
import { jwtAuthAuthorizationError } from '@/ResponseHelper';
export const setupExternalJWTAuth = (app: Application, config: Config, authIgnoreRegex: RegExp) => {
const jwtAuthHeader = config.getEnv('security.jwtAuth.jwtHeader');
if (jwtAuthHeader === '') {
throw new Error('JWT auth is activated but no request header was defined. Please set one!');
}
const jwksUri = config.getEnv('security.jwtAuth.jwksUri');
if (jwksUri === '') {
throw new Error('JWT auth is activated but no JWK Set URI was defined. Please set one!');
}
const jwtHeaderValuePrefix = config.getEnv('security.jwtAuth.jwtHeaderValuePrefix');
const jwtIssuer = config.getEnv('security.jwtAuth.jwtIssuer');
const jwtNamespace = config.getEnv('security.jwtAuth.jwtNamespace');
const jwtAllowedTenantKey = config.getEnv('security.jwtAuth.jwtAllowedTenantKey');
const jwtAllowedTenant = config.getEnv('security.jwtAuth.jwtAllowedTenant');
// eslint-disable-next-line no-inner-declarations
function isTenantAllowed(decodedToken: object): boolean {
if (jwtNamespace === '' || jwtAllowedTenantKey === '' || jwtAllowedTenant === '') {
return true;
}
for (const [k, v] of Object.entries(decodedToken)) {
if (k === jwtNamespace) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
for (const [kn, kv] of Object.entries(v)) {
if (kn === jwtAllowedTenantKey && kv === jwtAllowedTenant) {
return true;
}
}
}
}
return false;
}
// eslint-disable-next-line consistent-return
app.use((req, res, next) => {
if (authIgnoreRegex.exec(req.url)) {
return next();
}
let token = req.header(jwtAuthHeader) as string;
if (token === undefined || token === '') {
return jwtAuthAuthorizationError(res, 'Missing token');
}
if (jwtHeaderValuePrefix !== '' && token.startsWith(jwtHeaderValuePrefix)) {
token = token.replace(`${jwtHeaderValuePrefix} `, '').trimStart();
}
const jwkClient = jwks({ cache: true, jwksUri });
const getKey: jwt.GetPublicKeyOrSecret = (header, callbackFn) => {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (!header.kid) throw jwtAuthAuthorizationError(res, 'No JWT key found');
jwkClient.getSigningKey(header.kid, (error, key) => {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (error) throw jwtAuthAuthorizationError(res, error.message);
callbackFn(null, key?.getPublicKey());
});
};
const jwtVerifyOptions: jwt.VerifyOptions = {
issuer: jwtIssuer !== '' ? jwtIssuer : undefined,
ignoreExpiration: false,
};
jwt.verify(token, getKey, jwtVerifyOptions, (error: jwt.VerifyErrors, decoded: object) => {
if (error) {
jwtAuthAuthorizationError(res, 'Invalid token');
} else if (!isTenantAllowed(decoded)) {
jwtAuthAuthorizationError(res, 'Tenant not allowed');
} else {
next();
}
});
});
};

View File

@@ -1,12 +0,0 @@
import type { RequestHandler } from 'express';
import { LoggerProxy } from 'n8n-workflow';
import { isUserManagementEnabled } from '../UserManagement/UserManagementHelper';
export const userManagementEnabledMiddleware: RequestHandler = (req, res, next) => {
if (isUserManagementEnabled()) {
next();
} else {
LoggerProxy.debug('Request failed because user management is disabled');
res.status(400).json({ status: 'error', message: 'User management is disabled' });
}
};

View File

@@ -57,11 +57,7 @@ export const setupPushServer = (restEndpoint: string, server: Server, app: Appli
}
};
export const setupPushHandler = (
restEndpoint: string,
app: Application,
isUserManagementEnabled: boolean,
) => {
export const setupPushHandler = (restEndpoint: string, app: Application) => {
const endpoint = `/${restEndpoint}/push`;
const pushValidationMiddleware: RequestHandler = async (
@@ -83,20 +79,19 @@ export const setupPushHandler = (
}
// Handle authentication
if (isUserManagementEnabled) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const authCookie: string = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
await resolveJwt(authCookie);
} catch (error) {
if (ws) {
ws.send(`Unauthorized: ${(error as Error).message}`);
ws.close(401);
} else {
res.status(401).send('Unauthorized');
}
return;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const authCookie: string = req.cookies?.[AUTH_COOKIE_NAME] ?? '';
await resolveJwt(authCookie);
} catch (error) {
if (ws) {
ws.send(`Unauthorized: ${(error as Error).message}`);
ws.close(401);
} else {
res.status(401).send('Unauthorized');
}
return;
}
next();

View File

@@ -6,7 +6,7 @@ import { User } from '@db/entities/User';
import { RoleRepository } from '@db/repositories';
import { License } from '@/License';
import { AuthError, InternalServerError } from '@/ResponseHelper';
import { hashPassword, isUserManagementEnabled } from '@/UserManagement/UserManagementHelper';
import { hashPassword } from '@/UserManagement/UserManagementHelper';
import type { SamlPreferences } from './types/samlPreferences';
import type { SamlUserAttributes } from './types/samlUserAttributes';
import type { FlowResult } from 'samlify/types/src/flow';
@@ -52,10 +52,7 @@ export function setSamlLoginLabel(label: string): void {
config.set(SAML_LOGIN_LABEL, label);
}
export function isSamlLicensed(): boolean {
const license = Container.get(License);
return isUserManagementEnabled() && license.isSamlEnabled();
}
export const isSamlLicensed = () => Container.get(License).isSamlEnabled();
export function isSamlLicensedAndEnabled(): boolean {
return isSamlLoginEnabled() && isSamlLicensed() && isSamlCurrentAuthenticationMethod();