/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable import/no-cycle */ import cookieParser from 'cookie-parser'; import passport from 'passport'; import { NextFunction, Request, Response } from 'express'; import { LoggerProxy as Logger } from 'n8n-workflow'; import { N8nApp } from '../Interfaces'; import { AuthenticatedRequest } from '../../requests'; import { isAuthExcluded, isPostUsersId, isAuthenticatedRequest, isUserManagementDisabled, } from '../UserManagementHelper'; import { Db } from '../..'; import { jwtAuth, refreshExpiringCookie } from '../middlewares'; import { authenticationMethods } from './auth'; import { meNamespace } from './me'; import { usersNamespace } from './users'; import { passwordResetNamespace } from './passwordReset'; import { ownerNamespace } from './owner'; export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint: string): void { // needed for testing; not adding overhead since it directly returns if req.cookies exists this.app.use(cookieParser()); this.app.use(jwtAuth()); this.app.use(async (req: Request, res: Response, next: NextFunction) => { if ( // TODO: refactor me!!! // skip authentication for preflight requests req.method === 'OPTIONS' || req.url === '/index.html' || req.url === '/favicon.ico' || req.url.startsWith('/css/') || req.url.startsWith('/js/') || req.url.startsWith('/fonts/') || req.url.includes('.svg') || req.url.startsWith(`/${restEndpoint}/settings`) || req.url.startsWith(`/${restEndpoint}/login`) || req.url.startsWith(`/${restEndpoint}/logout`) || req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) || isPostUsersId(req, restEndpoint) || req.url.startsWith(`/${restEndpoint}/forgot-password`) || req.url.startsWith(`/${restEndpoint}/resolve-password-token`) || req.url.startsWith(`/${restEndpoint}/change-password`) || req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) || req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) || isAuthExcluded(req.url, ignoredEndpoints) ) { return next(); } // skip authentication if user management is disabled if (isUserManagementDisabled()) { req.user = await Db.collections.User.findOneOrFail( {}, { relations: ['globalRole'], }, ); return next(); } return passport.authenticate('jwt', { session: false })(req, res, next); }); this.app.use((req: Request | AuthenticatedRequest, res: Response, next: NextFunction) => { // req.user is empty for public routes, so just proceed // owner can do anything, so proceed as well if (!req.user || (isAuthenticatedRequest(req) && req.user.globalRole.name === 'owner')) { next(); return; } // Not owner and user exists. We now protect restricted urls. const postRestrictedUrls = [`/${this.restEndpoint}/users`, `/${this.restEndpoint}/owner`]; const getRestrictedUrls: string[] = []; const trimmedUrl = req.url.endsWith('/') ? req.url.slice(0, -1) : req.url; if ( (req.method === 'POST' && postRestrictedUrls.includes(trimmedUrl)) || (req.method === 'GET' && getRestrictedUrls.includes(trimmedUrl)) || (req.method === 'DELETE' && new RegExp(`/${restEndpoint}/users/[^/]+`, 'gm').test(trimmedUrl)) || (req.method === 'POST' && new RegExp(`/${restEndpoint}/users/[^/]+/reinvite`, 'gm').test(trimmedUrl)) || new RegExp(`/${restEndpoint}/owner/[^/]+`, 'gm').test(trimmedUrl) ) { Logger.verbose('User attempted to access endpoint without authorization', { endpoint: `${req.method} ${trimmedUrl}`, userId: isAuthenticatedRequest(req) ? req.user.id : 'unknown', }); res.status(403).json({ status: 'error', message: 'Unauthorized' }); return; } next(); }); this.app.use(refreshExpiringCookie); authenticationMethods.apply(this); ownerNamespace.apply(this); meNamespace.apply(this); passwordResetNamespace.apply(this); usersNamespace.apply(this); }