@universis/janitor 1.6.3 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/dist/index.d.ts +213 -7
  3. package/dist/index.esm.js +896 -0
  4. package/dist/index.esm.js.map +1 -0
  5. package/dist/index.js +909 -9
  6. package/dist/index.js.map +1 -1
  7. package/package.json +40 -20
  8. package/src/HttpBearerStategy.js +134 -0
  9. package/src/HttpBearerStrategy.d.ts +4 -0
  10. package/src/RedisClientStore.js +1 -1
  11. package/src/ScopeAccessConfiguration.d.ts +23 -1
  12. package/src/ScopeAccessConfiguration.js +38 -8
  13. package/src/index.d.ts +1 -1
  14. package/src/index.js +1 -1
  15. package/.gitlab-ci.yml +0 -29
  16. package/dist/OAuth2ClientService.d.ts +0 -98
  17. package/dist/OAuth2ClientService.js +0 -251
  18. package/dist/OAuth2ClientService.js.map +0 -1
  19. package/dist/RateLimitService.d.ts +0 -4
  20. package/dist/RateLimitService.js +0 -102
  21. package/dist/RateLimitService.js.map +0 -1
  22. package/dist/RedisClientStore.d.ts +0 -5
  23. package/dist/RedisClientStore.js +0 -122
  24. package/dist/RedisClientStore.js.map +0 -1
  25. package/dist/RemoteAddressValidator.d.ts +0 -10
  26. package/dist/RemoteAddressValidator.js +0 -89
  27. package/dist/RemoteAddressValidator.js.map +0 -1
  28. package/dist/ScopeAccessConfiguration.d.ts +0 -65
  29. package/dist/ScopeAccessConfiguration.js +0 -160
  30. package/dist/ScopeAccessConfiguration.js.map +0 -1
  31. package/dist/SpeedLimitService.d.ts +0 -4
  32. package/dist/SpeedLimitService.js +0 -113
  33. package/dist/SpeedLimitService.js.map +0 -1
  34. package/dist/polyfills.js +0 -11
  35. package/dist/polyfills.js.map +0 -1
  36. package/dist/validateScope.d.ts +0 -2
  37. package/dist/validateScope.js +0 -23
  38. package/dist/validateScope.js.map +0 -1
  39. package/src/polyfills.js +0 -10
@@ -0,0 +1,896 @@
1
+ import 'core-js/stable/string/replace-all';
2
+ import { ApplicationService, TraceUtils, ConfigurationStrategy, Args, HttpForbiddenError, DataError, HttpError } from '@themost/common';
3
+ import { rateLimit } from 'express-rate-limit';
4
+ import express from 'express';
5
+ import path from 'path';
6
+ import slowDown from 'express-slow-down';
7
+ import { RedisStore } from 'rate-limit-redis';
8
+ import { Redis } from 'ioredis';
9
+ import '@themost/promise-sequence';
10
+ import url, { URL } from 'url';
11
+ import { Request } from 'superagent';
12
+ import jwt from 'jsonwebtoken';
13
+
14
+ class RateLimitService extends ApplicationService {
15
+ /**
16
+ * @param {import('@themost/express').ExpressDataApplication} app
17
+ */
18
+ constructor(app) {
19
+ super(app);
20
+ app.serviceRouter.subscribe((serviceRouter) => {
21
+ if (serviceRouter == null) {
22
+ return;
23
+ }
24
+ try {
25
+ const addRouter = express.Router();
26
+ let serviceConfiguration = app.getConfiguration().getSourceAt('settings/universis/janitor/rateLimit') || {
27
+ profiles: [],
28
+ paths: []
29
+ };
30
+ if (serviceConfiguration.extends) {
31
+ // get additional configuration
32
+ const configurationPath = app.getConfiguration().getConfigurationPath();
33
+ const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
34
+ TraceUtils.log(`@universis/janitor#RateLimitService will try to extend service configuration from ${extendsPath}`);
35
+ serviceConfiguration = require(extendsPath);
36
+ }
37
+ const pathsArray = serviceConfiguration.paths || [];
38
+ const profilesArray = serviceConfiguration.profiles || [];
39
+ // create maps
40
+ const paths = new Map(pathsArray);
41
+ const profiles = new Map(profilesArray);
42
+ if (paths.size === 0) {
43
+ TraceUtils.warn('@universis/janitor#RateLimitService is being started but the collection of paths is empty.');
44
+ }
45
+ // get proxy address forwarding option
46
+ let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
47
+ if (typeof proxyAddressForwarding !== 'boolean') {
48
+ proxyAddressForwarding = false;
49
+ }
50
+ paths.forEach((value, path) => {
51
+ let profile;
52
+ // get profile
53
+ if (value.profile) {
54
+ profile = profiles.get(value.profile);
55
+ } else {
56
+ // or options defined inline
57
+ profile = value;
58
+ }
59
+ if (profile != null) {
60
+ const rateLimitOptions = Object.assign({
61
+ windowMs: 5 * 60 * 1000, // 5 minutes
62
+ limit: 50, // 50 requests
63
+ legacyHeaders: true // send headers
64
+ }, profile, {
65
+ keyGenerator: (req) => {
66
+ let remoteAddress;
67
+ if (proxyAddressForwarding) {
68
+ // get proxy headers or remote address
69
+ remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
70
+ } else {
71
+ // get remote address
72
+ remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
73
+ }
74
+ return `${path}:${remoteAddress}`;
75
+ }
76
+ });
77
+ if (typeof rateLimitOptions.store === 'string') {
78
+ // load store
79
+ const store = rateLimitOptions.store.split('#');
80
+ let StoreClass;
81
+ if (store.length === 2) {
82
+ const storeModule = require(store[0]);
83
+ if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
84
+ StoreClass = storeModule[store[1]];
85
+ rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
86
+ } else {
87
+ throw new Error(`${store} cannot be found or is inaccessible`);
88
+ }
89
+ } else {
90
+ StoreClass = require(store[0]);
91
+ // create store
92
+ rateLimitOptions.store = new StoreClass(this, rateLimitOptions);
93
+ }
94
+ }
95
+ addRouter.use(path, rateLimit(rateLimitOptions));
96
+ }
97
+ });
98
+ if (addRouter.stack.length) {
99
+ serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
100
+ }
101
+ } catch (err) {
102
+ TraceUtils.error('An error occurred while validating rate limit configuration.');
103
+ TraceUtils.error(err);
104
+ TraceUtils.warn('Rate limit service is inactive due to an error occured while loading configuration.');
105
+ }
106
+ });
107
+ }
108
+
109
+ }
110
+
111
+ class SpeedLimitService extends ApplicationService {
112
+ constructor(app) {
113
+ super(app);
114
+
115
+ app.serviceRouter.subscribe((serviceRouter) => {
116
+ if (serviceRouter == null) {
117
+ return;
118
+ }
119
+ try {
120
+ const addRouter = express.Router();
121
+ let serviceConfiguration = app.getConfiguration().getSourceAt('settings/universis/janitor/speedLimit') || {
122
+ profiles: [],
123
+ paths: []
124
+ };
125
+ if (serviceConfiguration.extends) {
126
+ // get additional configuration
127
+ const configurationPath = app.getConfiguration().getConfigurationPath();
128
+ const extendsPath = path.resolve(configurationPath, serviceConfiguration.extends);
129
+ TraceUtils.log(`@universis/janitor#SpeedLimitService will try to extend service configuration from ${extendsPath}`);
130
+ serviceConfiguration = require(extendsPath);
131
+ }
132
+ const pathsArray = serviceConfiguration.paths || [];
133
+ const profilesArray = serviceConfiguration.profiles || [];
134
+ // create maps
135
+ const paths = new Map(pathsArray);
136
+ const profiles = new Map(profilesArray);
137
+ if (paths.size === 0) {
138
+ TraceUtils.warn('@universis/janitor#SpeedLimitService is being started but the collection of paths is empty.');
139
+ }
140
+ // get proxy address forwarding option
141
+ let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
142
+ if (typeof proxyAddressForwarding !== 'boolean') {
143
+ proxyAddressForwarding = false;
144
+ }
145
+ paths.forEach((value, path) => {
146
+ let profile;
147
+ // get profile
148
+ if (value.profile) {
149
+ profile = profiles.get(value.profile);
150
+ } else {
151
+ // or options defined inline
152
+ profile = value;
153
+ }
154
+ if (profile != null) {
155
+ const slowDownOptions = Object.assign({
156
+ windowMs: 5 * 60 * 1000, // 5 minutes
157
+ delayAfter: 20, // 20 requests
158
+ delayMs: 500, // 500 ms
159
+ maxDelayMs: 10000 // 10 seconds
160
+ }, profile, {
161
+ keyGenerator: (req) => {
162
+ let remoteAddress;
163
+ if (proxyAddressForwarding) {
164
+ // get proxy headers or remote address
165
+ remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
166
+ } else {
167
+ // get remote address
168
+ remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
169
+ }
170
+ return `${path}:${remoteAddress}`;
171
+ }
172
+ });
173
+ if (Array.isArray(slowDownOptions.randomDelayMs)) {
174
+ slowDownOptions.delayMs = () => {
175
+ const delayMs = Math.floor(Math.random() * (slowDownOptions.randomDelayMs[1] - slowDownOptions.randomDelayMs[0] + 1) + slowDownOptions.randomDelayMs[0]);
176
+ return delayMs;
177
+ };
178
+ }
179
+ if (Array.isArray(slowDownOptions.randomMaxDelayMs)) {
180
+ slowDownOptions.maxDelayMs = () => {
181
+ const maxDelayMs = Math.floor(Math.random() * (slowDownOptions.randomMaxDelayMs[1] - slowDownOptions.randomMaxDelayMs[0] + 1) + slowDownOptions.randomMaxDelayMs[0]);
182
+ return maxDelayMs;
183
+ };
184
+ }
185
+ if (typeof slowDownOptions.store === 'string') {
186
+ // load store
187
+ const store = slowDownOptions.store.split('#');
188
+ let StoreClass;
189
+ if (store.length === 2) {
190
+ const storeModule = require(store[0]);
191
+ if (Object.prototype.hasOwnProperty.call(storeModule, store[1])) {
192
+ StoreClass = storeModule[store[1]];
193
+ slowDownOptions.store = new StoreClass(this, slowDownOptions);
194
+ } else {
195
+ throw new Error(`${store} cannot be found or is inaccessible`);
196
+ }
197
+ } else {
198
+ StoreClass = require(store[0]);
199
+ // create store
200
+ slowDownOptions.store = new StoreClass(this, slowDownOptions);
201
+ }
202
+ }
203
+ addRouter.use(path, slowDown(slowDownOptions));
204
+ }
205
+ });
206
+ if (addRouter.stack.length) {
207
+ serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
208
+ }
209
+ } catch (err) {
210
+ TraceUtils.error('An error occurred while validating speed limit configuration.');
211
+ TraceUtils.error(err);
212
+ TraceUtils.warn('Speed limit service is inactive due to an error occured while loading configuration.');
213
+ }
214
+ });
215
+ }
216
+
217
+ }
218
+
219
+ function _defineProperty(e, r, t) {
220
+ return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
221
+ value: t,
222
+ enumerable: true,
223
+ configurable: true,
224
+ writable: true
225
+ }) : e[r] = t, e;
226
+ }
227
+ function _toPrimitive(t, r) {
228
+ if ("object" != typeof t || !t) return t;
229
+ var e = t[Symbol.toPrimitive];
230
+ if (void 0 !== e) {
231
+ var i = e.call(t, r);
232
+ if ("object" != typeof i) return i;
233
+ throw new TypeError("@@toPrimitive must return a primitive value.");
234
+ }
235
+ return ("string" === r ? String : Number)(t);
236
+ }
237
+ function _toPropertyKey(t) {
238
+ var i = _toPrimitive(t, "string");
239
+ return "symbol" == typeof i ? i : i + "";
240
+ }
241
+
242
+ let superLoadIncrementScript;
243
+ let superLoadGetScript;
244
+
245
+ function noLoadGetScript() {
246
+
247
+ //
248
+ }
249
+ function noLoadIncrementScript() {
250
+
251
+ //
252
+ }
253
+ if (RedisStore.prototype.loadIncrementScript.name === 'loadIncrementScript') {
254
+ // get super method for future use
255
+ superLoadIncrementScript = RedisStore.prototype.loadIncrementScript;
256
+ RedisStore.prototype.loadIncrementScript = noLoadIncrementScript;
257
+ }
258
+
259
+ if (RedisStore.prototype.loadGetScript.name === 'loadGetScript') {
260
+ // get super method
261
+ superLoadGetScript = RedisStore.prototype.loadGetScript;
262
+ RedisStore.prototype.loadGetScript = noLoadGetScript;
263
+ }
264
+
265
+ class RedisClientStore extends RedisStore {
266
+
267
+
268
+
269
+
270
+
271
+
272
+ /**
273
+ *
274
+ * @param {import('@themost/common').ApplicationService} service
275
+ * @param {{windowMs: number}} options
276
+ */
277
+ constructor(service, options) {
278
+ super({
279
+ /**
280
+ * @param {...string} args
281
+ * @returns {Promise<*>}
282
+ */
283
+ sendCommand: function () {
284
+ const args = Array.from(arguments);
285
+ const [command] = args.splice(0, 1);
286
+ const self = this;
287
+ if (command === 'SCRIPT') {
288
+ const connectOptions = service.getApplication().getConfiguration().getSourceAt('settings/redis/options') || {
289
+ host: '127.0.0.1',
290
+ port: 6379
291
+ };
292
+ const client = new Redis(connectOptions);
293
+ return client.call(command, args).catch((error) => {
294
+ if (error instanceof TypeError && error.message === 'Invalid argument type') {
295
+ TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
296
+ }
297
+ return Promise.reject(error);
298
+ }).finally(() => {
299
+ if (client.isOpen) {
300
+ client.disconnect().catch((errDisconnect) => {
301
+ TraceUtils.error(errDisconnect);
302
+ });
303
+ }
304
+ });
305
+ }
306
+ if (self.client == null) {
307
+ const connectOptions = service.getApplication().getConfiguration().getSourceAt('settings/redis/options') || {
308
+ host: '127.0.0.1',
309
+ port: 6379
310
+ };
311
+ self.client = new Redis(connectOptions);
312
+ }
313
+ if (self.client.isOpen) {
314
+ return self.client.call(command, args).catch((error) => {
315
+ if (error instanceof TypeError && error.message === 'Invalid argument type') {
316
+ TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
317
+ }
318
+ return Promise.reject(error);
319
+ });
320
+ }
321
+ // send load script commands once
322
+ return (() => {
323
+ if (self.incrementScriptSha == null) {
324
+ return this.postInit();
325
+ }
326
+ return Promise.resolve();
327
+ })().then(() => {
328
+ // send command
329
+ args[0] = self.incrementScriptSha;
330
+ return self.client.call(command, args).catch((error) => {
331
+ if (error instanceof TypeError && error.message === 'Invalid argument type') {
332
+ TraceUtils.warn('RedisClientStore: Invalid argument type: ' + JSON.stringify(args));
333
+ }
334
+ return Promise.reject(error);
335
+ });
336
+ });
337
+ }
338
+ }); /**
339
+ * @type {import('redis').RedisClientType}
340
+ */_defineProperty(this, "client", void 0);this.init(options);TraceUtils.debug('RedisClientStore: Starting up and loading increment and get scripts.');
341
+ void this.postInit().then(() => {
342
+ TraceUtils.debug('RedisClientStore: Successfully loaded increment and get scripts.');
343
+ }).catch((err) => {
344
+ TraceUtils.error('RedisClientStore: Failed to load increment and get scripts.');
345
+ TraceUtils.error(err);
346
+ });
347
+ }
348
+ async postInit() {
349
+ const [incrementScriptSha, getScriptSha] = await Promise.sequence([
350
+ () => superLoadIncrementScript.call(this),
351
+ () => superLoadGetScript.call(this)]
352
+ );
353
+ this.incrementScriptSha = incrementScriptSha;
354
+ this.getScriptSha = getScriptSha;
355
+ }
356
+
357
+ }
358
+
359
+ const HTTP_METHOD_REGEXP = /^\b(POST|PUT|PATCH|DELETE)\b$/i;
360
+
361
+ class ScopeString {
362
+ constructor(str) {
363
+ this.value = str;
364
+ }
365
+ toString() {
366
+ return this.value;
367
+ }
368
+ /**
369
+ * Splits a comma-separated or space-separated scope string e.g. "profile email" or "profile,email"
370
+ *
371
+ * Important note: https://www.rfc-editor.org/rfc/rfc6749#section-3.3 defines the regular expression of access token scopes
372
+ * which is a space separated string. Several OAuth2 servers use a comma-separated list instead.
373
+ *
374
+ * The operation will try to use both implementations by excluding comma ',' from access token regular expressions
375
+ * @returns {Array<string>}
376
+ */
377
+ split() {
378
+ // the default regular expression includes comma /([\x21\x23-\x5B\x5D-\x7E]+)/g
379
+ // the modified regular expression excludes comma /x2C /([\x21\x23-\x2B\x2D-\x5B\x5D-\x7E]+)/g
380
+ const re = /([\x21\x23-\x2B\x2D-\x5B\x5D-\x7E]+)/g;
381
+ const results = [];
382
+ let match = re.exec(this.value);
383
+ while (match !== null) {
384
+ results.push(match[0]);
385
+ match = re.exec(this.value);
386
+ }
387
+ return results;
388
+ }
389
+ }
390
+
391
+ class ScopeAccessConfiguration extends ConfigurationStrategy {
392
+ /**
393
+ * @param {import('@themost/common').ConfigurationBase} configuration
394
+ */
395
+ constructor(configuration) {
396
+ super(configuration);
397
+ let elements = [];
398
+ // define property
399
+ Object.defineProperty(this, 'elements', {
400
+ get: () => {
401
+ return elements;
402
+ },
403
+ enumerable: true
404
+ });
405
+ }
406
+
407
+ /**
408
+ * @param {Request} req
409
+ * @returns Promise<ScopeAccessConfigurationElement>
410
+ */
411
+ verify(req) {
412
+ return new Promise((resolve, reject) => {
413
+ try {
414
+ // validate request context
415
+ Args.notNull(req.context, 'Context');
416
+ // validate request context user
417
+ Args.notNull(req.context.user, 'User');
418
+ if (req.context.user.authenticationScope && req.context.user.authenticationScope.length > 0) {
419
+ // get original url
420
+ let reqUrl = url.parse(req.originalUrl).pathname;
421
+ // get user context scopes as array e.g, ['students', 'students:read']
422
+ let reqScopes = new ScopeString(req.context.user.authenticationScope).split();
423
+ // get user access based on HTTP method e.g. GET -> read access
424
+ let reqAccess = HTTP_METHOD_REGEXP.test(req.method) ? 'write' : 'read';
425
+ // first phase: find element by resource and scope
426
+ let result = this.elements.find((x) => {
427
+ // filter element by access level
428
+ return new RegExp("^" + x.resource, 'i').test(reqUrl)
429
+ // and scopes
430
+ && x.scope.find((y) => {
431
+ // search user scopes (validate wildcard scope)
432
+ return y === "*" || reqScopes.indexOf(y) >= 0;
433
+ });
434
+ });
435
+ // second phase: check access level
436
+ if (result == null) {
437
+ return resolve();
438
+ }
439
+ // if access is missing or access is not an array
440
+ if (Array.isArray(result.access) === false) {
441
+ // the requested access is not allowed because the access is not defined
442
+ return resolve();
443
+ }
444
+ // if the requested access is not in the access array
445
+ if (result.access.indexOf(reqAccess) < 0) {
446
+ // the requested access is not allowed because the access levels do not match
447
+ return resolve();
448
+ }
449
+ // otherwise, return result
450
+ return resolve(result);
451
+ }
452
+ return resolve();
453
+ }
454
+ catch (err) {
455
+ return reject(err);
456
+ }
457
+
458
+ });
459
+ }
460
+
461
+ }
462
+
463
+ /**
464
+ * @class
465
+ */
466
+ class DefaultScopeAccessConfiguration extends ScopeAccessConfiguration {
467
+ /**
468
+ * @param {import('@themost/common').ConfigurationBase} configuration
469
+ */
470
+ constructor(configuration) {
471
+ super(configuration);
472
+ let defaults = [];
473
+ // load scope access from configuration resource
474
+ try {
475
+ /**
476
+ * @type {Array<ScopeAccessConfigurationElement>}
477
+ */
478
+ defaults = require(path.resolve(configuration.getConfigurationPath(), 'scope.access.json'));
479
+ }
480
+ catch (err) {
481
+ // if an error occurred other than module not found (there are no default access policies)
482
+ if (err.code !== 'MODULE_NOT_FOUND') {
483
+ // throw error
484
+ throw err;
485
+ }
486
+ // otherwise continue
487
+ }
488
+ this.elements.push.apply(this.elements, defaults);
489
+ }
490
+ }
491
+
492
+ class EnableScopeAccessConfiguration extends ApplicationService {
493
+ /**
494
+ * @param {import('@themost/express').ExpressDataApplication|import('@themost/common').ApplicationBase} app
495
+ */
496
+ constructor(app) {
497
+ super(app);
498
+ // register scope access configuration
499
+ app.getConfiguration().useStrategy(ScopeAccessConfiguration, DefaultScopeAccessConfiguration);
500
+ }
501
+ }
502
+
503
+
504
+ /**
505
+ * @class
506
+ */
507
+ class ExtendScopeAccessConfiguration extends ApplicationService {
508
+ /**
509
+ * @param {import('@themost/express').ExpressDataApplication|import('@themost/common').ApplicationBase} app
510
+ */
511
+ constructor(app) {
512
+ super(app);
513
+ // Get the additional scope access extensions from the configuration
514
+ const scopeAccessExtensions = app.getConfiguration().settings.universis?.janitor?.scopeAccess.imports;
515
+ if (app && app.container && scopeAccessExtensions != null) {
516
+ app.container.subscribe((container) => {
517
+ if (container) {
518
+ const scopeAccess = app.getConfiguration().getStrategy(function ScopeAccessConfiguration() {});
519
+ if (scopeAccess != null) {
520
+ for (const scopeAccessExtension of scopeAccessExtensions) {
521
+ try {
522
+ const elements = require(path.resolve(app.getConfiguration().getConfigurationPath(), scopeAccessExtension));
523
+ if (elements) {
524
+ // add extra scope access elements
525
+ scopeAccess.elements.unshift(...elements);
526
+ }
527
+ }
528
+ catch (err) {
529
+ // if an error occurred other than module not found (there are no default access policies)
530
+ if (err.code !== 'MODULE_NOT_FOUND') {
531
+ // throw error
532
+ throw err;
533
+ }
534
+ }
535
+ }
536
+ }
537
+ }
538
+ });
539
+ }
540
+ }
541
+ }
542
+
543
+ function validateScope() {
544
+ return (req, res, next) => {
545
+ /**
546
+ * @type {ScopeAccessConfiguration}
547
+ */
548
+ let scopeAccessConfiguration = req.context.getApplication().getConfiguration().getStrategy(ScopeAccessConfiguration);
549
+ if (typeof scopeAccessConfiguration === 'undefined') {
550
+ return next(new Error('Invalid application configuration. Scope access configuration strategy is missing or is in accessible.'));
551
+ }
552
+ scopeAccessConfiguration.verify(req).then((value) => {
553
+ if (value) {
554
+ return next();
555
+ }
556
+ return next(new HttpForbiddenError('Access denied due to authorization scopes.'));
557
+ }).catch((reason) => {
558
+ return next(reason);
559
+ });
560
+ };
561
+ }
562
+
563
+ function responseHander(resolve, reject) {
564
+ return function (err, response) {
565
+ if (err) {
566
+ /**
567
+ * @type {import('superagent').Response}
568
+ */
569
+ const response = err.response;
570
+ if (response && response.headers['content-type'] === 'application/json') {
571
+ // get body
572
+ const clientError = response.body;
573
+ const error = new HttpError(response.status);
574
+ return reject(Object.assign(error, {
575
+ clientError
576
+ }));
577
+ }
578
+ return reject(err);
579
+ }
580
+ if (response.status === 204 && response.headers['content-type'] === 'application/json') {
581
+ return resolve(null);
582
+ }
583
+ return resolve(response.body);
584
+ };
585
+ }
586
+
587
+ /**
588
+ * @class
589
+ */
590
+ class OAuth2ClientService extends ApplicationService {
591
+ /**
592
+ * @param {import('@themost/express').ExpressDataApplication} app
593
+ */
594
+ constructor(app) {
595
+ super(app);
596
+ /**
597
+ * @name OAuth2ClientService#settings
598
+ * @type {{server_uri:string,token_uri?:string}}
599
+ */
600
+ Object.defineProperty(this, 'settings', {
601
+ writable: false,
602
+ value: app.getConfiguration().getSourceAt('settings/auth'),
603
+ enumerable: false,
604
+ configurable: false
605
+ });
606
+ }
607
+
608
+ /**
609
+ * Gets keycloak server root
610
+ * @returns {string}
611
+ */
612
+ getServer() {
613
+ return this.settings.server_uri;
614
+ }
615
+
616
+ /**
617
+ * Gets keycloak server root
618
+ * @returns {string}
619
+ */
620
+ getAdminRoot() {
621
+ return this.settings.admin_uri;
622
+ }
623
+
624
+ // noinspection JSUnusedGlobalSymbols
625
+ /**
626
+ * Gets user's profile by calling OAuth2 server profile endpoint
627
+ * @param {ExpressDataContext} context
628
+ * @param {string} token
629
+ */
630
+ getUserInfo(token) {
631
+ return new Promise((resolve, reject) => {
632
+ const userinfo_uri = this.settings.userinfo_uri ? new URL(this.settings.userinfo_uri, this.getServer()) : new URL('me', this.getServer());
633
+ return new Request('GET', userinfo_uri).
634
+ set({
635
+ 'Authorization': `Bearer ${token}`,
636
+ 'Accept': 'application/json'
637
+ }).
638
+ query({
639
+ 'access_token': token
640
+ }).end(responseHander(resolve, reject));
641
+ });
642
+ }
643
+
644
+ // noinspection JSUnusedGlobalSymbols
645
+ /**
646
+ * Gets the token info of the current context
647
+ * @param {ExpressDataContext} context
648
+ */
649
+ getContextTokenInfo(context) {
650
+ if (context.user == null) {
651
+ return Promise.reject(new Error('Context user may not be null'));
652
+ }
653
+ if (context.user.authenticationType !== 'Bearer') {
654
+ return Promise.reject(new Error('Invalid context authentication type'));
655
+ }
656
+ if (context.user.authenticationToken == null) {
657
+ return Promise.reject(new Error('Context authentication data may not be null'));
658
+ }
659
+ return this.getTokenInfo(context, context.user.authenticationToken);
660
+ }
661
+ /**
662
+ * Gets token info by calling OAuth2 server endpoint
663
+ * @param {ExpressDataContext} _context
664
+ * @param {string} token
665
+ */
666
+ getTokenInfo(_context, token) {
667
+ return new Promise((resolve, reject) => {
668
+ const introspection_uri = this.settings.introspection_uri ? new URL(this.settings.introspection_uri, this.getServer()) : new URL('tokeninfo', this.getServer());
669
+ return new Request('POST', introspection_uri).
670
+ auth(this.settings.client_id, this.settings.client_secret).
671
+ set('Accept', 'application/json').
672
+ type('form').
673
+ send({
674
+ 'token_type_hint': 'access_token',
675
+ 'token': token
676
+ }).end(responseHander(resolve, reject));
677
+ });
678
+ }
679
+
680
+ /**
681
+ * @param {AuthorizeUser} authorizeUser
682
+ */
683
+ authorize(authorizeUser) {
684
+ const tokenURL = this.settings.token_uri ? new URL(this.settings.token_uri) : new URL('authorize', this.getServer());
685
+ return new Promise((resolve, reject) => {
686
+ return new Request('POST', tokenURL).
687
+ type('form').
688
+ send(authorizeUser).end(responseHander(resolve, reject));
689
+ });
690
+ }
691
+
692
+ /**
693
+ * Gets a user by name
694
+ * @param {*} user_id
695
+ * @param {AdminMethodOptions} options
696
+ */
697
+ getUserById(user_id, options) {
698
+ return new Promise((resolve, reject) => {
699
+ return new Request('GET', new URL(`users/${user_id}`, this.getAdminRoot())).
700
+ set('Authorization', `Bearer ${options.access_token}`).
701
+ end(responseHander(resolve, reject));
702
+ });
703
+ }
704
+
705
+ /**
706
+ * Gets a user by name
707
+ * @param {string} username
708
+ * @param {AdminMethodOptions} options
709
+ */
710
+ getUser(username, options) {
711
+ return new Promise((resolve, reject) => {
712
+ return new Request('GET', new URL('users', this.getAdminRoot())).
713
+ set('Authorization', `Bearer ${options.access_token}`).
714
+ query({
715
+ '$filter': `name eq '${username}'`
716
+ }).
717
+ end(responseHander(resolve, reject));
718
+ });
719
+ }
720
+
721
+ /**
722
+ * Gets a user by email address
723
+ * @param {string} email
724
+ * @param {AdminMethodOptions} options
725
+ */
726
+ getUserByEmail(email, options) {
727
+ return new Promise((resolve, reject) => {
728
+ return new Request('GET', new URL('users', this.getAdminRoot())).
729
+ set('Authorization', `Bearer ${options.access_token}`).
730
+ query({
731
+ '$filter': `alternateName eq '${email}'`
732
+ }).
733
+ end(responseHander(resolve, reject));
734
+ });
735
+ }
736
+
737
+ /**
738
+ * Updates an existing user
739
+ * @param {*} user
740
+ * @param {AdminMethodOptions} options
741
+ */
742
+ updateUser(user, options) {
743
+ return new Promise((resolve, reject) => {
744
+ if (user.id == null) {
745
+ return reject(new DataError('E_IDENTIFIER', 'User may not be empty at this context.', null, 'User', 'id'));
746
+ }
747
+ const request = new Request('PUT', new URL(`users/${user.id}`, this.getAdminRoot()));
748
+ return request.set('Authorization', `Bearer ${options.access_token}`).
749
+ set('Content-Type', 'application/json').
750
+ send(user).
751
+ end(responseHander(resolve, reject));
752
+ });
753
+ }
754
+
755
+ /**
756
+ * Creates a new user
757
+ * @param {*} user
758
+ * @param {AdminMethodOptions} options
759
+ */
760
+ createUser(user, options) {
761
+ return new Promise((resolve, reject) => {
762
+ const request = new Request('POST', new URL('users', this.getAdminRoot()));
763
+ return request.set('Authorization', `Bearer ${options.access_token}`).
764
+ set('Content-Type', 'application/json').
765
+ send(Object.assign({}, user, {
766
+ $state: 1 // for create
767
+ })).
768
+ end(responseHander(resolve, reject));
769
+ });
770
+ }
771
+
772
+ /**
773
+ * Deletes a user
774
+ * @param {{id: any}} user
775
+ * @param {AdminMethodOptions} options
776
+ */
777
+ deleteUser(user, options) {
778
+ return new Promise((resolve, reject) => {
779
+ if (user.id == null) {
780
+ return reject(new DataError('E_IDENTIFIER', 'User may not be empty at this context.', null, 'User', 'id'));
781
+ }
782
+ const request = new Request('DELETE', new URL(`users/${user.id}`, this.getAdminRoot()));
783
+ return request.set('Authorization', `Bearer ${options.access_token}`).
784
+ end(responseHander(resolve, reject));
785
+ });
786
+ }
787
+
788
+ /**
789
+ * @param {boolean=} force
790
+ * @returns {*}
791
+ */
792
+ getWellKnownConfiguration(force) {
793
+ if (force) {
794
+ this.well_known_configuration = null;
795
+ }
796
+ if (this.well_known_configuration) {
797
+ return Promise.resolve(this.well_known_configuration);
798
+ }
799
+ return new Promise((resolve, reject) => {
800
+ 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());
801
+ return new Request('GET', well_known_configuration_uri).
802
+ end(responseHander(resolve, reject));
803
+ }).then((configuration) => {
804
+ this.well_known_configuration = configuration;
805
+ return configuration;
806
+ });
807
+ }
808
+ }
809
+
810
+ class HttpRemoteAddrForbiddenError extends HttpForbiddenError {
811
+ constructor() {
812
+ super('Access is denied due to remote address conflict. The client network has been changed or cannot be determined.');
813
+ this.statusCode = 403.6;
814
+ }
815
+
816
+ }
817
+
818
+ class RemoteAddressValidator extends ApplicationService {
819
+ constructor(app) {
820
+ super(app);
821
+
822
+ // get proxy address forwarding option
823
+ let proxyAddressForwarding = app.getConfiguration().getSourceAt('settings/universis/api/proxyAddressForwarding');
824
+ if (typeof proxyAddressForwarding !== 'boolean') {
825
+ proxyAddressForwarding = false;
826
+ }
827
+ this.proxyAddressForwarding = proxyAddressForwarding;
828
+ // get token claim name
829
+ this.claim = app.getConfiguration().getSourceAt('settings/universis/janitor/remoteAddress/claim') || 'remoteAddress';
830
+
831
+ app.serviceRouter.subscribe((serviceRouter) => {
832
+ if (serviceRouter == null) {
833
+ return;
834
+ }
835
+ const addRouter = express.Router();
836
+ addRouter.use((req, res, next) => {
837
+ void this.validateRemoteAddress(req).then((value) => {
838
+ if (value === false) {
839
+ return next(new HttpRemoteAddrForbiddenError());
840
+ }
841
+ return next();
842
+ }).catch((err) => {
843
+ return next(err);
844
+ });
845
+ });
846
+ // insert router at the beginning of serviceRouter.stack
847
+ serviceRouter.stack.unshift.apply(serviceRouter.stack, addRouter.stack);
848
+ });
849
+ }
850
+
851
+ /**
852
+ * Gets remote address from request
853
+ * @param {import('express').Request} req
854
+ * @returns
855
+ */
856
+ getRemoteAddress(req) {
857
+ let remoteAddress;
858
+ if (this.proxyAddressForwarding) {
859
+ // get proxy headers or remote address
860
+ remoteAddress = req.headers['x-real-ip'] || req.headers['x-forwarded-for'] || (req.connection ? req.connection.remoteAddress : req.socket.remoteAddress);
861
+ } else {
862
+ remoteAddress = req.connection ? req.connection.remoteAddress : req.socket.remoteAddress;
863
+ }
864
+ return remoteAddress;
865
+ }
866
+
867
+ /**
868
+ * Validates token remote address with request remote address
869
+ * @param {import('express').Request} req
870
+ * @returns {Promise<boolean>}
871
+ */
872
+ async validateRemoteAddress(req) {
873
+ const authenticationToken = req.context?.user?.authenticationToken;
874
+ if (authenticationToken != null) {
875
+ const access_token = jwt.decode(authenticationToken);
876
+ const remoteAddress = access_token[this.claim];
877
+ if (remoteAddress == null) {
878
+ TraceUtils.warn(`Remote address validation failed. Expected a valid remote address claimed by using "${this.claim}" attribute but got none.`);
879
+ return false;
880
+ }
881
+ // get context remote address
882
+ const requestRemoteAddress = this.getRemoteAddress(req);
883
+ if (remoteAddress !== requestRemoteAddress) {
884
+ TraceUtils.warn(`Remote address validation failed. Expected remote address is ${remoteAddress || 'Uknown'} but request remote address is ${requestRemoteAddress}`);
885
+ return false;
886
+ }
887
+ return true;
888
+ }
889
+ TraceUtils.warn('Remote address validation cannot be completed because authentication token is not available.');
890
+ return false;
891
+ }
892
+
893
+ }
894
+
895
+ export { DefaultScopeAccessConfiguration, EnableScopeAccessConfiguration, ExtendScopeAccessConfiguration, HttpRemoteAddrForbiddenError, OAuth2ClientService, RateLimitService, RedisClientStore, RemoteAddressValidator, ScopeAccessConfiguration, ScopeString, SpeedLimitService, validateScope };
896
+ //# sourceMappingURL=index.esm.js.map