UNPKG

@universis/janitor

Version:

Universis api plugin for handling user authorization and rate limiting

897 lines (843 loc) 32.1 kB
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(`@universis/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(`@universis/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