mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-21 03:42:16 +00:00
127 lines
4.9 KiB
TypeScript
127 lines
4.9 KiB
TypeScript
/* 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 = require('cookie-parser');
|
|
import * as passport from 'passport';
|
|
import { Strategy } from 'passport-jwt';
|
|
import { NextFunction, Request, Response } from 'express';
|
|
import * as jwt from 'jsonwebtoken';
|
|
import { LoggerProxy as Logger } from 'n8n-workflow';
|
|
|
|
import { JwtPayload, N8nApp } from '../Interfaces';
|
|
import { authenticationMethods } from './auth';
|
|
import config = require('../../../config');
|
|
import { AUTH_COOKIE_NAME } from '../../constants';
|
|
import { issueCookie, resolveJwtContent } from '../auth/jwt';
|
|
import { meNamespace } from './me';
|
|
import { usersNamespace } from './users';
|
|
import { passwordResetNamespace } from './passwordReset';
|
|
import { AuthenticatedRequest } from '../../requests';
|
|
import { ownerNamespace } from './owner';
|
|
import { isAuthExcluded, isPostUsersId, isAuthenticatedRequest } from '../UserManagementHelper';
|
|
|
|
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());
|
|
|
|
const options = {
|
|
jwtFromRequest: (req: Request) => {
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
return (req.cookies?.[AUTH_COOKIE_NAME] as string | undefined) ?? null;
|
|
},
|
|
secretOrKey: config.get('userManagement.jwtSecret') as string,
|
|
};
|
|
|
|
passport.use(
|
|
new Strategy(options, async function validateCookieContents(jwtPayload: JwtPayload, done) {
|
|
try {
|
|
const user = await resolveJwtContent(jwtPayload);
|
|
return done(null, user);
|
|
} catch (error) {
|
|
Logger.debug('Failed to extract user from JWT payload', { jwtPayload });
|
|
return done(null, false, { message: 'User not found' });
|
|
}
|
|
}),
|
|
);
|
|
|
|
this.app.use(passport.initialize());
|
|
|
|
this.app.use((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`) ||
|
|
isAuthExcluded(req.url, ignoredEndpoints)
|
|
) {
|
|
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 = [`/${this.restEndpoint}/users`];
|
|
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();
|
|
});
|
|
|
|
// middleware to refresh cookie before it expires
|
|
this.app.use(async (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
|
|
const cookieAuth = options.jwtFromRequest(req);
|
|
if (cookieAuth && req.user) {
|
|
const cookieContents = jwt.decode(cookieAuth) as JwtPayload & { exp: number };
|
|
if (cookieContents.exp * 1000 - Date.now() < 259200000) {
|
|
// if cookie expires in < 3 days, renew it.
|
|
await issueCookie(res, req.user);
|
|
}
|
|
}
|
|
next();
|
|
});
|
|
|
|
authenticationMethods.apply(this);
|
|
ownerNamespace.apply(this);
|
|
meNamespace.apply(this);
|
|
passwordResetNamespace.apply(this);
|
|
usersNamespace.apply(this);
|
|
}
|