@universis/janitor
Version:
Universis api plugin for handling user authorization and rate limiting
897 lines (843 loc) • 32.1 kB
JavaScript
import 'core-js/stable/string/replace-all';
import { ApplicationService, TraceUtils, ConfigurationStrategy, Args, HttpForbiddenError, DataError, HttpError } from '@themost/common';
import { rateLimit } from 'express-rate-limit';
import express from 'express';
import path from 'path';
import slowDown from 'express-slow-down';
import { RedisStore } from 'rate-limit-redis';
import { Redis } from 'ioredis';
import '@themost/promise-sequence';
import url, { URL } from 'url';
import { Request } from 'superagent';
import jwt from 'jsonwebtoken';
class RateLimitService extends ApplicationService {
/**
* @param {import('@themost/express').ExpressDataApplication} app
*/
constructor(app) {
super(app);
app.serviceRouter.subscribe((serviceRouter) => {
if (serviceRouter == null) {
return;
}
try {
const addRouter = express.Router();
let serviceConfiguration = app.getConfiguration().getSourceAt('settings/universis/janitor/rateLimit') || {
profiles: [],
paths: []
};
if (serviceConfiguration.extends) {
// get additional configuration
const configurationPath = app.getConfiguration().getConfigurationPath();
const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
TraceUtils.log(`/janitor#RateLimitService will try to extend service configuration from ${extendsPath}`);
serviceConfiguration = require(extendsPath);
}
const pathsArray = serviceConfiguration.paths || [];
const profilesArray = serviceConfiguration.profiles || [];
// create maps
const paths = new Map(pathsArray);
const profiles = new Map(profilesArray);
if (paths.size === 0) {
TraceUtils.warn('@universis/janitor#RateLimitService is being started but the collection of paths is empty.');
}
// get proxy address forwarding option
let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
if (typeof proxyAddressForwarding !== 'boolean') {
proxyAddressForwarding = false;
}
paths.forEach((value, path) => {
let profile;
// get profile
if (value.profile) {
profile = profiles.get(value.profile);
} else {
// or options defined inline
profile = value;
}
if (profile != null) {
const rateLimitOptions = Object.assign({
windowMs: 5 * 60 * 1000, // 5 minutes
limit: 50, // 50 requests
legacyHeaders: true // send headers
}, profile, {
keyGenerator: (req) => {
let remoteAddress;
if (proxyAddressForwarding) {
// get proxy headers or remote address
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
} else {
// get remote address
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
}
return `${path}:${remoteAddress}`;
}
});
if (typeof rateLimitOptions.store === 'string') {
// load store
const store = rateLimitOptions.store.split('#');
let StoreClass;
if (store.length === 2) {
const storeModule = require(store[0]);
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
StoreClass = storeModule[store[1]];
rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
} else {
throw new Error(`${store} cannot be found or is inaccessible`);
}
} else {
StoreClass = require(store[0]);
// create store
rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
}
}
addRouter.use(path, rateLimit(rateLimitOptions));
}
});
if (addRouter.stack.length) {
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
}
} catch (err) {
TraceUtils.error('An error occurred while validating rate limit configuration.');
TraceUtils.error(err);
TraceUtils.warn('Rate limit service is inactive due to an error occured while loading configuration.');
}
});
}
}
class SpeedLimitService extends ApplicationService {
constructor(app) {
super(app);
app.serviceRouter.subscribe((serviceRouter) => {
if (serviceRouter == null) {
return;
}
try {
const addRouter = express.Router();
let serviceConfiguration = app.getConfiguration().getSourceAt('settings/universis/janitor/speedLimit') || {
profiles: [],
paths: []
};
if (serviceConfiguration.extends) {
// get additional configuration
const configurationPath = app.getConfiguration().getConfigurationPath();
const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
TraceUtils.log(`/janitor#SpeedLimitService will try to extend service configuration from ${extendsPath}`);
serviceConfiguration = require(extendsPath);
}
const pathsArray = serviceConfiguration.paths || [];
const profilesArray = serviceConfiguration.profiles || [];
// create maps
const paths = new Map(pathsArray);
const profiles = new Map(profilesArray);
if (paths.size === 0) {
TraceUtils.warn('@universis/janitor#SpeedLimitService is being started but the collection of paths is empty.');
}
// get proxy address forwarding option
let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
if (typeof proxyAddressForwarding !== 'boolean') {
proxyAddressForwarding = false;
}
paths.forEach((value, path) => {
let profile;
// get profile
if (value.profile) {
profile = profiles.get(value.profile);
} else {
// or options defined inline
profile = value;
}
if (profile != null) {
const slowDownOptions = Object.assign({
windowMs: 5 * 60 * 1000, // 5 minutes
delayAfter: 20, // 20 requests
delayMs: 500, // 500 ms
maxDelayMs: 10000 // 10 seconds
}, profile, {
keyGenerator: (req) => {
let remoteAddress;
if (proxyAddressForwarding) {
// get proxy headers or remote address
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
} else {
// get remote address
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
}
return `${path}:${remoteAddress}`;
}
});
if (Array.isArray(slowDownOptions.randomDelayMs)) {
slowDownOptions.delayMs = () => {
const delayMs = Math.floor(Math.random() * (slowDownOptions.randomDelayMs[1] - slowDownOptions.randomDelayMs[0] + 1) + slowDownOptions.randomDelayMs[0]);
return delayMs;
};
}
if (Array.isArray(slowDownOptions.randomMaxDelayMs)) {
slowDownOptions.maxDelayMs = () => {
const maxDelayMs = Math.floor(Math.random() * (slowDownOptions.randomMaxDelayMs[1] - slowDownOptions.randomMaxDelayMs[0] + 1) + slowDownOptions.randomMaxDelayMs[0]);
return maxDelayMs;
};
}
if (typeof slowDownOptions.store === 'string') {
// load store
const store = slowDownOptions.store.split('#');
let StoreClass;
if (store.length === 2) {
const storeModule = require(store[0]);
if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
StoreClass = storeModule[store[1]];
slowDownOptions.store = new StoreClass(this, slowDownOptions);
} else {
throw new Error(`${store} cannot be found or is inaccessible`);
}
} else {
StoreClass = require(store[0]);
// create store
slowDownOptions.store = new StoreClass(this, slowDownOptions);
}
}
addRouter.use(path, slowDown(slowDownOptions));
}
});
if (addRouter.stack.length) {
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
}
} catch (err) {
TraceUtils.error('An error occurred while validating speed limit configuration.');
TraceUtils.error(err);
TraceUtils.warn('Speed limit service is inactive due to an error occured while loading configuration.');
}
});
}
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r);
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return ("string" === r ? String : Number)(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
let superLoadIncrementScript;
let superLoadGetScript;
function noLoadGetScript() {
//
}
function noLoadIncrementScript() {
//
}
if (RedisStore.prototype.loadIncrementScript.name === 'loadIncrementScript') {
// get super method for future use
superLoadIncrementScript = RedisStore.prototype.loadIncrementScript;
RedisStore.prototype.loadIncrementScript = noLoadIncrementScript;
}
if (RedisStore.prototype.loadGetScript.name === 'loadGetScript') {
// get super method
superLoadGetScript = RedisStore.prototype.loadGetScript;
RedisStore.prototype.loadGetScript = noLoadGetScript;
}
class RedisClientStore extends RedisStore {
/**
*
* @param {import('@themost/common').ApplicationService} service
* @param {{windowMs: number}} options
*/
constructor(service, options) {
super({
/**
* @param {...string} args
* @returns {Promise<*>}
*/
sendCommand: function () {
const args = Array.from(arguments);
const [command] = args.splice(0, 1);
const self = this;
if (command === 'SCRIPT') {
const connectOptions = service.getApplication().getConfiguration().getSourceAt('settings/redis/options') || {
host: '127.0.0.1',
port: 6379
};
const client = new Redis(connectOptions);
return client.call(command, args).catch((error) => {
if (error instanceof TypeError && error.message === 'Invalid argument type') {
TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
}
return Promise.reject(error);
}).finally(() => {
if (client.isOpen) {
client.disconnect().catch((errDisconnect) => {
TraceUtils.error(errDisconnect);
});
}
});
}
if (self.client == null) {
const connectOptions = service.getApplication().getConfiguration().getSourceAt('settings/redis/options') || {
host: '127.0.0.1',
port: 6379
};
self.client = new Redis(connectOptions);
}
if (self.client.isOpen) {
return self.client.call(command, args).catch((error) => {
if (error instanceof TypeError && error.message === 'Invalid argument type') {
TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
}
return Promise.reject(error);
});
}
// send load script commands once
return (() => {
if (self.incrementScriptSha == null) {
return this.postInit();
}
return Promise.resolve();
})().then(() => {
// send command
args[0] = self.incrementScriptSha;
return self.client.call(command, args).catch((error) => {
if (error instanceof TypeError && error.message === 'Invalid argument type') {
TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
}
return Promise.reject(error);
});
});
}
}); /**
* @type {import('redis').RedisClientType}
*/_defineProperty(this, "client", void 0);this.init(options);TraceUtils.debug('RedisClientStore: Starting up and loading increment and get scripts.');
void this.postInit().then(() => {
TraceUtils.debug('RedisClientStore: Successfully loaded increment and get scripts.');
}).catch((err) => {
TraceUtils.error('RedisClientStore: Failed to load increment and get scripts.');
TraceUtils.error(err);
});
}
async postInit() {
const [incrementScriptSha, getScriptSha] = await Promise.sequence([
() => superLoadIncrementScript.call(this),
() => superLoadGetScript.call(this)]
);
this.incrementScriptSha = incrementScriptSha;
this.getScriptSha = getScriptSha;
}
}
const HTTP_METHOD_REGEXP = /^\b(POST|PUT|PATCH|DELETE)\b$/i;
class ScopeString {
constructor(str) {
this.value = str;
}
toString() {
return this.value;
}
/**
* Splits a comma-separated or space-separated scope string e.g. "profile email" or "profile,email"
*
* Important note: https://www.rfc-editor.org/rfc/rfc6749#section-3.3 defines the regular expression of access token scopes
* which is a space separated string. Several OAuth2 servers use a comma-separated list instead.
*
* The operation will try to use both implementations by excluding comma ',' from access token regular expressions
* @returns {Array<string>}
*/
split() {
// the default regular expression includes comma /([\x21\x23-\x5B\x5D-\x7E]+)/g
// the modified regular expression excludes comma /x2C /([\x21\x23-\x2B\x2D-\x5B\x5D-\x7E]+)/g
const re = /([\x21\x23-\x2B\x2D-\x5B\x5D-\x7E]+)/g;
const results = [];
let match = re.exec(this.value);
while (match !== null) {
results.push(match[0]);
match = re.exec(this.value);
}
return results;
}
}
class ScopeAccessConfiguration extends ConfigurationStrategy {
/**
* @param {import('@themost/common').ConfigurationBase} configuration
*/
constructor(configuration) {
super(configuration);
let elements = [];
// define property
Object.defineProperty(this, 'elements', {
get: () => {
return elements;
},
enumerable: true
});
}
/**
* @param {Request} req
* @returns Promise<ScopeAccessConfigurationElement>
*/
verify(req) {
return new Promise((resolve, reject) => {
try {
// validate request context
Args.notNull(req.context, 'Context');
// validate request context user
Args.notNull(req.context.user, 'User');
if (req.context.user.authenticationScope && req.context.user.authenticationScope.length > 0) {
// get original url
let reqUrl = url.parse(req.originalUrl).pathname;
// get user context scopes as array e.g, ['students', 'students:read']
let reqScopes = new ScopeString(req.context.user.authenticationScope).split();
// get user access based on HTTP method e.g. GET -> read access
let reqAccess = HTTP_METHOD_REGEXP.test(req.method) ? 'write' : 'read';
// first phase: find element by resource and scope
let result = this.elements.find((x) => {
// filter element by access level
return new RegExp("^" + x.resource, 'i').test(reqUrl)
// and scopes
&& x.scope.find((y) => {
// search user scopes (validate wildcard scope)
return y === "*" || reqScopes.indexOf(y) >= 0;
});
});
// second phase: check access level
if (result == null) {
return resolve();
}
// if access is missing or access is not an array
if (Array.isArray(result.access) === false) {
// the requested access is not allowed because the access is not defined
return resolve();
}
// if the requested access is not in the access array
if (result.access.indexOf(reqAccess) < 0) {
// the requested access is not allowed because the access levels do not match
return resolve();
}
// otherwise, return result
return resolve(result);
}
return resolve();
}
catch (err) {
return reject(err);
}
});
}
}
/**
* @class
*/
class DefaultScopeAccessConfiguration extends ScopeAccessConfiguration {
/**
* @param {import('@themost/common').ConfigurationBase} configuration
*/
constructor(configuration) {
super(configuration);
let defaults = [];
// load scope access from configuration resource
try {
/**
* @type {Array<ScopeAccessConfigurationElement>}
*/
defaults = require(path.resolve(configuration.getConfigurationPath(), 'scope.access.json'));
}
catch (err) {
// if an error occurred other than module not found (there are no default access policies)
if (err.code !== 'MODULE_NOT_FOUND') {
// throw error
throw err;
}
// otherwise continue
}
this.elements.push.apply(this.elements, defaults);
}
}
class EnableScopeAccessConfiguration extends ApplicationService {
/**
* @param {import('@themost/express').ExpressDataApplication|import('@themost/common').ApplicationBase} app
*/
constructor(app) {
super(app);
// register scope access configuration
app.getConfiguration().useStrategy(ScopeAccessConfiguration, DefaultScopeAccessConfiguration);
}
}
/**
* @class
*/
class ExtendScopeAccessConfiguration extends ApplicationService {
/**
* @param {import('@themost/express').ExpressDataApplication|import('@themost/common').ApplicationBase} app
*/
constructor(app) {
super(app);
// Get the additional scope access extensions from the configuration
const scopeAccessExtensions = app.getConfiguration().settings.universis?.janitor?.scopeAccess.imports;
if (app && app.container && scopeAccessExtensions != null) {
app.container.subscribe((container) => {
if (container) {
const scopeAccess = app.getConfiguration().getStrategy(function ScopeAccessConfiguration() {});
if (scopeAccess != null) {
for (const scopeAccessExtension of scopeAccessExtensions) {
try {
const elements = require(path.resolve(app.getConfiguration().getConfigurationPath(), scopeAccessExtension));
if (elements) {
// add extra scope access elements
scopeAccess.elements.unshift(...elements);
}
}
catch (err) {
// if an error occurred other than module not found (there are no default access policies)
if (err.code !== 'MODULE_NOT_FOUND') {
// throw error
throw err;
}
}
}
}
}
});
}
}
}
function validateScope() {
return (req, res, next) => {
/**
* @type {ScopeAccessConfiguration}
*/
let scopeAccessConfiguration = req.context.getApplication().getConfiguration().getStrategy(ScopeAccessConfiguration);
if (typeof scopeAccessConfiguration === 'undefined') {
return next(new Error('Invalid application configuration. Scope access configuration strategy is missing or is in accessible.'));
}
scopeAccessConfiguration.verify(req).then((value) => {
if (value) {
return next();
}
return next(new HttpForbiddenError('Access denied due to authorization scopes.'));
}).catch((reason) => {
return next(reason);
});
};
}
function responseHander(resolve, reject) {
return function (err, response) {
if (err) {
/**
* @type {import('superagent').Response}
*/
const response = err.response;
if (response && response.headers['content-type'] === 'application/json') {
// get body
const clientError = response.body;
const error = new HttpError(response.status);
return reject(Object.assign(error, {
clientError
}));
}
return reject(err);
}
if (response.status === 204 && response.headers['content-type'] === 'application/json') {
return resolve(null);
}
return resolve(response.body);
};
}
/**
* @class
*/
class OAuth2ClientService extends ApplicationService {
/**
* @param {import('@themost/express').ExpressDataApplication} app
*/
constructor(app) {
super(app);
/**
* @name OAuth2ClientService#settings
* @type {{server_uri:string,token_uri?:string}}
*/
Object.defineProperty(this, 'settings', {
writable: false,
value: app.getConfiguration().getSourceAt('settings/auth'),
enumerable: false,
configurable: false
});
}
/**
* Gets keycloak server root
* @returns {string}
*/
getServer() {
return this.settings.server_uri;
}
/**
* Gets keycloak server root
* @returns {string}
*/
getAdminRoot() {
return this.settings.admin_uri;
}
// noinspection JSUnusedGlobalSymbols
/**
* Gets user's profile by calling OAuth2 server profile endpoint
* @param {ExpressDataContext} context
* @param {string} token
*/
getUserInfo(token) {
return new Promise((resolve, reject) => {
const userinfo_uri = this.settings.userinfo_uri ? new URL(this.settings.userinfo_uri, this.getServer()) : new URL('me', this.getServer());
return new Request('GET', userinfo_uri).
set({
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}).
query({
'access_token': token
}).end(responseHander(resolve, reject));
});
}
// noinspection JSUnusedGlobalSymbols
/**
* Gets the token info of the current context
* @param {ExpressDataContext} context
*/
getContextTokenInfo(context) {
if (context.user == null) {
return Promise.reject(new Error('Context user may not be null'));
}
if (context.user.authenticationType !== 'Bearer') {
return Promise.reject(new Error('Invalid context authentication type'));
}
if (context.user.authenticationToken == null) {
return Promise.reject(new Error('Context authentication data may not be null'));
}
return this.getTokenInfo(context, context.user.authenticationToken);
}
/**
* Gets token info by calling OAuth2 server endpoint
* @param {ExpressDataContext} _context
* @param {string} token
*/
getTokenInfo(_context, token) {
return new Promise((resolve, reject) => {
const introspection_uri = this.settings.introspection_uri ? new URL(this.settings.introspection_uri, this.getServer()) : new URL('tokeninfo', this.getServer());
return new Request('POST', introspection_uri).
auth(this.settings.client_id, this.settings.client_secret).
set('Accept', 'application/json').
type('form').
send({
'token_type_hint': 'access_token',
'token': token
}).end(responseHander(resolve, reject));
});
}
/**
* @param {AuthorizeUser} authorizeUser
*/
authorize(authorizeUser) {
const tokenURL = this.settings.token_uri ? new URL(this.settings.token_uri) : new URL('authorize', this.getServer());
return new Promise((resolve, reject) => {
return new Request('POST', tokenURL).
type('form').
send(authorizeUser).end(responseHander(resolve, reject));
});
}
/**
* Gets a user by name
* @param {*} user_id
* @param {AdminMethodOptions} options
*/
getUserById(user_id, options) {
return new Promise((resolve, reject) => {
return new Request('GET', new URL(`users/${user_id}`, this.getAdminRoot())).
set('Authorization', `Bearer ${options.access_token}`).
end(responseHander(resolve, reject));
});
}
/**
* Gets a user by name
* @param {string} username
* @param {AdminMethodOptions} options
*/
getUser(username, options) {
return new Promise((resolve, reject) => {
return new Request('GET', new URL('users', this.getAdminRoot())).
set('Authorization', `Bearer ${options.access_token}`).
query({
'$filter': `name eq '${username}'`
}).
end(responseHander(resolve, reject));
});
}
/**
* Gets a user by email address
* @param {string} email
* @param {AdminMethodOptions} options
*/
getUserByEmail(email, options) {
return new Promise((resolve, reject) => {
return new Request('GET', new URL('users', this.getAdminRoot())).
set('Authorization', `Bearer ${options.access_token}`).
query({
'$filter': `alternateName eq '${email}'`
}).
end(responseHander(resolve, reject));
});
}
/**
* Updates an existing user
* @param {*} user
* @param {AdminMethodOptions} options
*/
updateUser(user, options) {
return new Promise((resolve, reject) => {
if (user.id == null) {
return reject(new DataError('E_IDENTIFIER', 'User may not be empty at this context.', null, 'User', 'id'));
}
const request = new Request('PUT', new URL(`users/${user.id}`, this.getAdminRoot()));
return request.set('Authorization', `Bearer ${options.access_token}`).
set('Content-Type', 'application/json').
send(user).
end(responseHander(resolve, reject));
});
}
/**
* Creates a new user
* @param {*} user
* @param {AdminMethodOptions} options
*/
createUser(user, options) {
return new Promise((resolve, reject) => {
const request = new Request('POST', new URL('users', this.getAdminRoot()));
return request.set('Authorization', `Bearer ${options.access_token}`).
set('Content-Type', 'application/json').
send(Object.assign({}, user, {
$state: 1 // for create
})).
end(responseHander(resolve, reject));
});
}
/**
* Deletes a user
* @param {{id: any}} user
* @param {AdminMethodOptions} options
*/
deleteUser(user, options) {
return new Promise((resolve, reject) => {
if (user.id == null) {
return reject(new DataError('E_IDENTIFIER', 'User may not be empty at this context.', null, 'User', 'id'));
}
const request = new Request('DELETE', new URL(`users/${user.id}`, this.getAdminRoot()));
return request.set('Authorization', `Bearer ${options.access_token}`).
end(responseHander(resolve, reject));
});
}
/**
* @param {boolean=} force
* @returns {*}
*/
getWellKnownConfiguration(force) {
if (force) {
this.well_known_configuration = null;
}
if (this.well_known_configuration) {
return Promise.resolve(this.well_known_configuration);
}
return new Promise((resolve, reject) => {
const well_known_configuration_uri = this.settings.well_known_configuration_uri ? new URL(this.settings.well_known_configuration_uri, this.getServer()) : new URL('.well-known/openid-configuration', this.getServer());
return new Request('GET', well_known_configuration_uri).
end(responseHander(resolve, reject));
}).then((configuration) => {
this.well_known_configuration = configuration;
return configuration;
});
}
}
class HttpRemoteAddrForbiddenError extends HttpForbiddenError {
constructor() {
super('Access is denied due to remote address conflict. The client network has been changed or cannot be determined.');
this.statusCode = 403.6;
}
}
class RemoteAddressValidator extends ApplicationService {
constructor(app) {
super(app);
// get proxy address forwarding option
let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
if (typeof proxyAddressForwarding !== 'boolean') {
proxyAddressForwarding = false;
}
this.proxyAddressForwarding = proxyAddressForwarding;
// get token claim name
this.claim = app.getConfiguration().getSourceAt('settings/universis/janitor/remoteAddress/claim') || 'remoteAddress';
app.serviceRouter.subscribe((serviceRouter) => {
if (serviceRouter == null) {
return;
}
const addRouter = express.Router();
addRouter.use((req, res, next) => {
void this.validateRemoteAddress(req).then((value) => {
if (value === false) {
return next(new HttpRemoteAddrForbiddenError());
}
return next();
}).catch((err) => {
return next(err);
});
});
// insert router at the beginning of serviceRouter.stack
serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
});
}
/**
* Gets remote address from request
* @param {import('express').Request} req
* @returns
*/
getRemoteAddress(req) {
let remoteAddress;
if (this.proxyAddressForwarding) {
// get proxy headers or remote address
remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
} else {
remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
}
return remoteAddress;
}
/**
* Validates token remote address with request remote address
* @param {import('express').Request} req
* @returns {Promise<boolean>}
*/
async validateRemoteAddress(req) {
const authenticationToken = req.context?.user?.authenticationToken;
if (authenticationToken != null) {
const access_token = jwt.decode(authenticationToken);
const remoteAddress = access_token[this.claim];
if (remoteAddress == null) {
TraceUtils.warn(`Remote address validation failed. Expected a valid remote address claimed by using "${this.claim}" attribute but got none.`);
return false;
}
// get context remote address
const requestRemoteAddress = this.getRemoteAddress(req);
if (remoteAddress !== requestRemoteAddress) {
TraceUtils.warn(`Remote address validation failed. Expected remote address is ${remoteAddress || 'Uknown'} but request remote address is ${requestRemoteAddress}`);
return false;
}
return true;
}
TraceUtils.warn('Remote address validation cannot be completed because authentication token is not available.');
return false;
}
}
export { DefaultScopeAccessConfiguration, EnableScopeAccessConfiguration, ExtendScopeAccessConfiguration, HttpRemoteAddrForbiddenError, OAuth2ClientService, RateLimitService, RedisClientStore, RemoteAddressValidator, ScopeAccessConfiguration, ScopeString, SpeedLimitService, validateScope };
//# sourceMappingURL=index.esm.js.map