refactor(core): Add unit tests for all external auth middlewares (no-changelog) (#5386)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-02-07 15:49:35 +01:00
committed by GitHub
parent 3a435f7057
commit 7e2f2f7453
10 changed files with 492 additions and 398 deletions

View File

@@ -0,0 +1,57 @@
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 = async (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

@@ -0,0 +1,89 @@
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 = async (
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();
}
});
});
};