@technomoron/api-server-base 1.1.13 → 2.0.0-beta.10
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.
- package/README.txt +25 -2
- package/dist/cjs/api-server-base.cjs +448 -111
- package/dist/cjs/api-server-base.d.ts +91 -34
- package/dist/cjs/auth-api/auth-module.d.ts +105 -0
- package/dist/cjs/auth-api/auth-module.js +1180 -0
- package/dist/cjs/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/cjs/auth-api/compat-auth-storage.js +128 -0
- package/dist/cjs/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/cjs/auth-api/mem-auth-store.js +141 -0
- package/dist/cjs/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
- package/dist/cjs/{auth-module.cjs → auth-api/module.js} +1 -1
- package/dist/cjs/auth-api/sql-auth-store.d.ts +77 -0
- package/dist/cjs/auth-api/sql-auth-store.js +172 -0
- package/dist/cjs/auth-api/storage.d.ts +38 -0
- package/dist/cjs/{auth-storage.cjs → auth-api/storage.js} +17 -7
- package/dist/cjs/auth-api/types.d.ts +34 -0
- package/dist/cjs/auth-api/types.js +2 -0
- package/dist/cjs/index.cjs +41 -7
- package/dist/cjs/index.d.ts +29 -5
- package/dist/cjs/oauth/base.d.ts +10 -0
- package/dist/cjs/oauth/base.js +6 -0
- package/dist/cjs/oauth/memory.d.ts +16 -0
- package/dist/cjs/oauth/memory.js +99 -0
- package/dist/cjs/oauth/models.d.ts +45 -0
- package/dist/cjs/oauth/models.js +58 -0
- package/dist/cjs/oauth/sequelize.d.ts +68 -0
- package/dist/cjs/oauth/sequelize.js +210 -0
- package/dist/cjs/oauth/types.d.ts +50 -0
- package/dist/cjs/oauth/types.js +3 -0
- package/dist/cjs/passkey/base.d.ts +16 -0
- package/dist/cjs/passkey/base.js +6 -0
- package/dist/cjs/passkey/memory.d.ts +27 -0
- package/dist/cjs/passkey/memory.js +86 -0
- package/dist/cjs/passkey/models.d.ts +25 -0
- package/dist/cjs/passkey/models.js +115 -0
- package/dist/cjs/passkey/sequelize.d.ts +55 -0
- package/dist/cjs/passkey/sequelize.js +220 -0
- package/dist/cjs/passkey/service.d.ts +20 -0
- package/dist/cjs/passkey/service.js +356 -0
- package/dist/cjs/passkey/types.d.ts +78 -0
- package/dist/cjs/passkey/types.js +2 -0
- package/dist/cjs/token/base.d.ts +38 -0
- package/dist/cjs/token/base.js +114 -0
- package/dist/cjs/token/memory.d.ts +19 -0
- package/dist/cjs/token/memory.js +149 -0
- package/dist/cjs/token/sequelize.d.ts +58 -0
- package/dist/cjs/token/sequelize.js +404 -0
- package/dist/cjs/token/types.d.ts +27 -0
- package/dist/cjs/token/types.js +2 -0
- package/dist/cjs/user/base.d.ts +26 -0
- package/dist/cjs/user/base.js +45 -0
- package/dist/cjs/user/memory.d.ts +35 -0
- package/dist/cjs/user/memory.js +173 -0
- package/dist/cjs/user/sequelize.d.ts +41 -0
- package/dist/cjs/user/sequelize.js +182 -0
- package/dist/cjs/user/types.d.ts +11 -0
- package/dist/cjs/user/types.js +2 -0
- package/dist/esm/api-server-base.d.ts +91 -34
- package/dist/esm/api-server-base.js +447 -110
- package/dist/esm/auth-api/auth-module.d.ts +105 -0
- package/dist/esm/auth-api/auth-module.js +1178 -0
- package/dist/esm/auth-api/compat-auth-storage.d.ts +57 -0
- package/dist/esm/auth-api/compat-auth-storage.js +124 -0
- package/dist/esm/auth-api/mem-auth-store.d.ts +68 -0
- package/dist/esm/auth-api/mem-auth-store.js +137 -0
- package/dist/esm/{auth-module.d.ts → auth-api/module.d.ts} +7 -7
- package/dist/esm/{auth-module.js → auth-api/module.js} +1 -1
- package/dist/esm/auth-api/sql-auth-store.d.ts +77 -0
- package/dist/esm/auth-api/sql-auth-store.js +168 -0
- package/dist/esm/auth-api/storage.d.ts +38 -0
- package/dist/esm/{auth-storage.js → auth-api/storage.js} +15 -5
- package/dist/esm/auth-api/types.d.ts +34 -0
- package/dist/esm/auth-api/types.js +1 -0
- package/dist/esm/index.d.ts +29 -5
- package/dist/esm/index.js +19 -2
- package/dist/esm/oauth/base.d.ts +10 -0
- package/dist/esm/oauth/base.js +2 -0
- package/dist/esm/oauth/memory.d.ts +16 -0
- package/dist/esm/oauth/memory.js +92 -0
- package/dist/esm/oauth/models.d.ts +45 -0
- package/dist/esm/oauth/models.js +51 -0
- package/dist/esm/oauth/sequelize.d.ts +68 -0
- package/dist/esm/oauth/sequelize.js +199 -0
- package/dist/esm/oauth/types.d.ts +50 -0
- package/dist/esm/oauth/types.js +2 -0
- package/dist/esm/passkey/base.d.ts +16 -0
- package/dist/esm/passkey/base.js +2 -0
- package/dist/esm/passkey/memory.d.ts +27 -0
- package/dist/esm/passkey/memory.js +82 -0
- package/dist/esm/passkey/models.d.ts +25 -0
- package/dist/esm/passkey/models.js +108 -0
- package/dist/esm/passkey/sequelize.d.ts +55 -0
- package/dist/esm/passkey/sequelize.js +216 -0
- package/dist/esm/passkey/service.d.ts +20 -0
- package/dist/esm/passkey/service.js +319 -0
- package/dist/esm/passkey/types.d.ts +78 -0
- package/dist/esm/passkey/types.js +1 -0
- package/dist/esm/token/base.d.ts +38 -0
- package/dist/esm/token/base.js +107 -0
- package/dist/esm/token/memory.d.ts +19 -0
- package/dist/esm/token/memory.js +145 -0
- package/dist/esm/token/sequelize.d.ts +58 -0
- package/dist/esm/token/sequelize.js +400 -0
- package/dist/esm/token/types.d.ts +27 -0
- package/dist/esm/token/types.js +1 -0
- package/dist/esm/user/base.d.ts +26 -0
- package/dist/esm/user/base.js +38 -0
- package/dist/esm/user/memory.d.ts +35 -0
- package/dist/esm/user/memory.js +169 -0
- package/dist/esm/user/sequelize.d.ts +41 -0
- package/dist/esm/user/sequelize.js +176 -0
- package/dist/esm/user/types.d.ts +11 -0
- package/dist/esm/user/types.js +1 -0
- package/package.json +13 -3
- package/dist/cjs/auth-storage.d.ts +0 -133
- package/dist/esm/auth-storage.d.ts +0 -133
|
@@ -10,13 +10,34 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.ApiServer = exports.ApiError = exports.ApiModule = void 0;
|
|
13
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
14
|
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
|
14
15
|
const cors_1 = __importDefault(require("cors"));
|
|
15
16
|
const express_1 = __importDefault(require("express"));
|
|
16
|
-
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
17
17
|
const multer_1 = __importDefault(require("multer"));
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
const module_js_1 = require("./auth-api/module.js");
|
|
19
|
+
const storage_js_1 = require("./auth-api/storage.js");
|
|
20
|
+
const base_js_1 = require("./token/base.js");
|
|
21
|
+
class JwtHelperStore extends base_js_1.TokenStore {
|
|
22
|
+
async save() {
|
|
23
|
+
throw new Error('Token store is not configured');
|
|
24
|
+
}
|
|
25
|
+
async get() {
|
|
26
|
+
throw new Error('Token store is not configured');
|
|
27
|
+
}
|
|
28
|
+
async delete() {
|
|
29
|
+
throw new Error('Token store is not configured');
|
|
30
|
+
}
|
|
31
|
+
async update() {
|
|
32
|
+
throw new Error('Token store is not configured');
|
|
33
|
+
}
|
|
34
|
+
async list() {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
async close() {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
20
41
|
var api_module_js_1 = require("./api-module.cjs");
|
|
21
42
|
Object.defineProperty(exports, "ApiModule", { enumerable: true, get: function () { return api_module_js_1.ApiModule; } });
|
|
22
43
|
function guess_exception_text(error, defMsg = 'Unknown Error') {
|
|
@@ -331,19 +352,38 @@ function fillConfig(config) {
|
|
|
331
352
|
devMode: config.devMode ?? false,
|
|
332
353
|
hydrateGetBody: config.hydrateGetBody ?? true,
|
|
333
354
|
validateTokens: config.validateTokens ?? false,
|
|
355
|
+
refreshMaybe: config.refreshMaybe ?? false,
|
|
334
356
|
apiVersion: config.apiVersion ?? '',
|
|
335
|
-
minClientVersion: config.minClientVersion ?? ''
|
|
357
|
+
minClientVersion: config.minClientVersion ?? '',
|
|
358
|
+
tokenStore: config.tokenStore,
|
|
359
|
+
authStores: config.authStores
|
|
336
360
|
};
|
|
337
361
|
}
|
|
338
362
|
class ApiServer {
|
|
339
363
|
constructor(config = {}) {
|
|
340
364
|
this.currReq = null;
|
|
341
365
|
this.apiNotFoundHandler = null;
|
|
366
|
+
this.tokenStoreAdapter = null;
|
|
367
|
+
this.userStoreAdapter = null;
|
|
368
|
+
this.passkeyServiceAdapter = null;
|
|
369
|
+
this.oauthStoreAdapter = null;
|
|
370
|
+
this.canImpersonateAdapter = null;
|
|
342
371
|
this.config = fillConfig(config);
|
|
343
372
|
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
344
373
|
this.startedAt = Date.now();
|
|
345
|
-
this.storageAdapter =
|
|
346
|
-
this.moduleAdapter =
|
|
374
|
+
this.storageAdapter = storage_js_1.nullAuthAdapter;
|
|
375
|
+
this.moduleAdapter = module_js_1.nullAuthModule;
|
|
376
|
+
this.jwtHelper = new JwtHelperStore();
|
|
377
|
+
this.tokenStoreAdapter = this.config.tokenStore ?? null;
|
|
378
|
+
if (this.config.authStores) {
|
|
379
|
+
const { userStore, tokenStore, passkeyService, oauthStore, canImpersonate } = this.config.authStores;
|
|
380
|
+
this.userStoreAdapter = userStore;
|
|
381
|
+
this.tokenStoreAdapter = tokenStore;
|
|
382
|
+
this.passkeyServiceAdapter = passkeyService ?? null;
|
|
383
|
+
this.oauthStoreAdapter = oauthStore ?? null;
|
|
384
|
+
this.canImpersonateAdapter = canImpersonate ?? null;
|
|
385
|
+
this.storageAdapter = this;
|
|
386
|
+
}
|
|
347
387
|
this.app = (0, express_1.default)();
|
|
348
388
|
if (config.uploadPath) {
|
|
349
389
|
const upload = (0, multer_1.default)({ dest: config.uploadPath });
|
|
@@ -380,72 +420,149 @@ class ApiServer {
|
|
|
380
420
|
getAuthModule() {
|
|
381
421
|
return this.moduleAdapter;
|
|
382
422
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
return {
|
|
389
|
-
success: true,
|
|
390
|
-
token
|
|
391
|
-
};
|
|
423
|
+
setTokenStore(store) {
|
|
424
|
+
this.tokenStoreAdapter = store;
|
|
425
|
+
// If using direct stores, expose self as the auth storage.
|
|
426
|
+
if (this.userStoreAdapter) {
|
|
427
|
+
this.storageAdapter = this;
|
|
392
428
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
429
|
+
return this;
|
|
430
|
+
}
|
|
431
|
+
getTokenStore() {
|
|
432
|
+
return this.tokenStoreAdapter;
|
|
433
|
+
}
|
|
434
|
+
ensureUserStore() {
|
|
435
|
+
if (!this.userStoreAdapter) {
|
|
436
|
+
throw new Error('User store is not configured');
|
|
398
437
|
}
|
|
438
|
+
return this.userStoreAdapter;
|
|
399
439
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const data = jsonwebtoken_1.default.verify(token, secret, options);
|
|
404
|
-
return {
|
|
405
|
-
success: true,
|
|
406
|
-
data
|
|
407
|
-
};
|
|
440
|
+
ensureTokenStore() {
|
|
441
|
+
if (!this.tokenStoreAdapter) {
|
|
442
|
+
throw new Error('Token store is not configured');
|
|
408
443
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
error: 'Token expired'
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
else {
|
|
418
|
-
return {
|
|
419
|
-
success: false,
|
|
420
|
-
expired: false,
|
|
421
|
-
error: error instanceof Error ? error.message : String(error)
|
|
422
|
-
};
|
|
423
|
-
}
|
|
444
|
+
return this.tokenStoreAdapter;
|
|
445
|
+
}
|
|
446
|
+
ensurePasskeyService() {
|
|
447
|
+
if (!this.passkeyServiceAdapter) {
|
|
448
|
+
throw new Error('Passkey service is not configured');
|
|
424
449
|
}
|
|
450
|
+
return this.passkeyServiceAdapter;
|
|
425
451
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
};
|
|
436
|
-
}
|
|
437
|
-
return {
|
|
438
|
-
success: true,
|
|
439
|
-
data
|
|
440
|
-
};
|
|
452
|
+
async listUserCredentials(userId) {
|
|
453
|
+
return this.ensurePasskeyService().listUserCredentials(userId);
|
|
454
|
+
}
|
|
455
|
+
async deletePasskeyCredential(credentialId) {
|
|
456
|
+
return this.ensurePasskeyService().deleteCredential(credentialId);
|
|
457
|
+
}
|
|
458
|
+
ensureOAuthStore() {
|
|
459
|
+
if (!this.oauthStoreAdapter) {
|
|
460
|
+
throw new Error('OAuth store is not configured');
|
|
441
461
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
462
|
+
return this.oauthStoreAdapter;
|
|
463
|
+
}
|
|
464
|
+
// AuthAdapter-compatible helpers (used by AuthModule)
|
|
465
|
+
async getUser(identifier) {
|
|
466
|
+
return this.userStoreAdapter ? this.userStoreAdapter.findUser(identifier) : null;
|
|
467
|
+
}
|
|
468
|
+
getUserPasswordHash(user) {
|
|
469
|
+
return this.ensureUserStore().getPasswordHash(user) ?? '';
|
|
470
|
+
}
|
|
471
|
+
getUserId(user) {
|
|
472
|
+
return this.ensureUserStore().getUserId(user);
|
|
473
|
+
}
|
|
474
|
+
filterUser(user) {
|
|
475
|
+
return this.ensureUserStore().toPublic(user);
|
|
476
|
+
}
|
|
477
|
+
async verifyPassword(password, hash) {
|
|
478
|
+
return this.ensureUserStore().verifyPassword(password, hash);
|
|
479
|
+
}
|
|
480
|
+
async storeToken(data) {
|
|
481
|
+
if (this.tokenStoreAdapter) {
|
|
482
|
+
return this.tokenStoreAdapter.save(data);
|
|
448
483
|
}
|
|
484
|
+
if (typeof this.storageAdapter.storeToken === 'function') {
|
|
485
|
+
return this.storageAdapter.storeToken(data);
|
|
486
|
+
}
|
|
487
|
+
throw new Error('Token store is not configured');
|
|
488
|
+
}
|
|
489
|
+
async getToken(query, opts) {
|
|
490
|
+
const normalized = {
|
|
491
|
+
...query,
|
|
492
|
+
userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
|
|
493
|
+
ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
|
|
494
|
+
};
|
|
495
|
+
if (this.tokenStoreAdapter) {
|
|
496
|
+
return this.tokenStoreAdapter.get(normalized, opts);
|
|
497
|
+
}
|
|
498
|
+
if (typeof this.storageAdapter.getToken === 'function') {
|
|
499
|
+
return this.storageAdapter.getToken(normalized, opts);
|
|
500
|
+
}
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
async deleteToken(query) {
|
|
504
|
+
const normalized = {
|
|
505
|
+
...query,
|
|
506
|
+
userId: query.userId !== undefined && query.userId !== null ? String(query.userId) : undefined,
|
|
507
|
+
ruid: query.ruid !== undefined && query.ruid !== null ? String(query.ruid) : undefined
|
|
508
|
+
};
|
|
509
|
+
if (this.tokenStoreAdapter) {
|
|
510
|
+
return this.tokenStoreAdapter.delete(normalized);
|
|
511
|
+
}
|
|
512
|
+
if (typeof this.storageAdapter.deleteToken === 'function') {
|
|
513
|
+
return this.storageAdapter.deleteToken(normalized);
|
|
514
|
+
}
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
async createPasskeyChallenge(params) {
|
|
518
|
+
return this.ensurePasskeyService().createChallenge(params);
|
|
519
|
+
}
|
|
520
|
+
async verifyPasskeyResponse(params) {
|
|
521
|
+
return this.ensurePasskeyService().verifyResponse(params);
|
|
522
|
+
}
|
|
523
|
+
async getClient(clientId) {
|
|
524
|
+
return this.oauthStoreAdapter ? this.oauthStoreAdapter.getClient(clientId) : null;
|
|
525
|
+
}
|
|
526
|
+
async verifyClientSecret(client, clientSecret) {
|
|
527
|
+
return this.ensureOAuthStore().verifyClientSecret(client.clientId, clientSecret);
|
|
528
|
+
}
|
|
529
|
+
async createAuthCode(request) {
|
|
530
|
+
const expiresAt = new Date(Date.now() + (request.expiresInSeconds ?? 300) * 1000);
|
|
531
|
+
const code = request.code ?? (0, node_crypto_1.randomUUID)();
|
|
532
|
+
await this.ensureOAuthStore().createAuthCode({ ...request, code, expiresAt });
|
|
533
|
+
return {
|
|
534
|
+
code,
|
|
535
|
+
clientId: request.clientId,
|
|
536
|
+
userId: request.userId,
|
|
537
|
+
redirectUri: request.redirectUri,
|
|
538
|
+
scope: request.scope ?? [],
|
|
539
|
+
codeChallenge: request.codeChallenge,
|
|
540
|
+
codeChallengeMethod: request.codeChallengeMethod,
|
|
541
|
+
expiresAt,
|
|
542
|
+
metadata: request.metadata
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async consumeAuthCode(code, clientId) {
|
|
546
|
+
const consumed = await this.ensureOAuthStore().consumeAuthCode(code);
|
|
547
|
+
if (!consumed || consumed.clientId !== clientId) {
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
return consumed;
|
|
551
|
+
}
|
|
552
|
+
async canImpersonate(params) {
|
|
553
|
+
if (this.canImpersonateAdapter) {
|
|
554
|
+
return !!(await this.canImpersonateAdapter(params));
|
|
555
|
+
}
|
|
556
|
+
return params.realUserId === params.effectiveUserId;
|
|
557
|
+
}
|
|
558
|
+
jwtSign(payload, secret, expiresInSeconds, options) {
|
|
559
|
+
return (this.tokenStoreAdapter ?? this.jwtHelper).jwtSign(payload, secret, expiresInSeconds, options);
|
|
560
|
+
}
|
|
561
|
+
jwtVerify(token, secret, options) {
|
|
562
|
+
return (this.tokenStoreAdapter ?? this.jwtHelper).jwtVerify(token, secret, options);
|
|
563
|
+
}
|
|
564
|
+
jwtDecode(token, options) {
|
|
565
|
+
return (this.tokenStoreAdapter ?? this.jwtHelper).jwtDecode(token, options);
|
|
449
566
|
}
|
|
450
567
|
async getApiKey(token) {
|
|
451
568
|
void token;
|
|
@@ -463,16 +580,13 @@ class ApiServer {
|
|
|
463
580
|
return this.storageAdapter.verifyPassword(params.password, hash);
|
|
464
581
|
}
|
|
465
582
|
async updateToken(updates) {
|
|
466
|
-
if (
|
|
467
|
-
return
|
|
583
|
+
if (this.tokenStoreAdapter) {
|
|
584
|
+
return this.tokenStoreAdapter.update(updates);
|
|
468
585
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
clientId: updates.clientId,
|
|
474
|
-
scope: updates.scope
|
|
475
|
-
});
|
|
586
|
+
if (typeof this.storageAdapter.updateToken === 'function') {
|
|
587
|
+
return this.storageAdapter.updateToken(updates);
|
|
588
|
+
}
|
|
589
|
+
return false;
|
|
476
590
|
}
|
|
477
591
|
guessExceptionText(error, defMsg = 'Unkown Error') {
|
|
478
592
|
return guess_exception_text(error, defMsg);
|
|
@@ -593,16 +707,117 @@ class ApiServer {
|
|
|
593
707
|
}
|
|
594
708
|
async verifyJWT(token) {
|
|
595
709
|
if (!this.config.accessSecret) {
|
|
596
|
-
return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set' };
|
|
710
|
+
return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
|
|
597
711
|
}
|
|
598
712
|
const result = this.jwtVerify(token, this.config.accessSecret);
|
|
599
713
|
if (!result.success) {
|
|
600
|
-
return { tokenData: undefined, error: result.error };
|
|
714
|
+
return { tokenData: undefined, error: result.error, expired: result.expired };
|
|
601
715
|
}
|
|
602
716
|
if (!result.data.uid) {
|
|
603
|
-
return { tokenData: undefined, error: 'Missing/bad userid in token' };
|
|
717
|
+
return { tokenData: undefined, error: 'Missing/bad userid in token', expired: false };
|
|
718
|
+
}
|
|
719
|
+
return { tokenData: result.data, error: undefined, expired: false };
|
|
720
|
+
}
|
|
721
|
+
jwtCookieOptions(apiReq) {
|
|
722
|
+
const conf = this.config;
|
|
723
|
+
const forwarded = apiReq.req.headers['x-forwarded-proto'];
|
|
724
|
+
const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
|
|
725
|
+
const origin = typeof referer === 'string' ? referer : '';
|
|
726
|
+
const isHttps = forwarded === 'https' || apiReq.req.protocol === 'https';
|
|
727
|
+
const isLocalhost = origin.includes('localhost');
|
|
728
|
+
const options = {
|
|
729
|
+
httpOnly: true,
|
|
730
|
+
secure: true,
|
|
731
|
+
sameSite: 'strict',
|
|
732
|
+
domain: conf.cookieDomain || undefined,
|
|
733
|
+
path: '/',
|
|
734
|
+
maxAge: undefined
|
|
735
|
+
};
|
|
736
|
+
if (conf.devMode) {
|
|
737
|
+
options.secure = isHttps;
|
|
738
|
+
options.httpOnly = false;
|
|
739
|
+
options.sameSite = 'lax';
|
|
740
|
+
if (isLocalhost) {
|
|
741
|
+
options.domain = undefined;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return options;
|
|
745
|
+
}
|
|
746
|
+
setAccessCookie(apiReq, accessToken, sessionCookie) {
|
|
747
|
+
const conf = this.config;
|
|
748
|
+
const options = this.jwtCookieOptions(apiReq);
|
|
749
|
+
const accessMaxAge = Math.max(1, conf.accessExpiry) * 1000;
|
|
750
|
+
const accessOptions = sessionCookie ? options : { ...options, maxAge: accessMaxAge };
|
|
751
|
+
apiReq.res.cookie(conf.accessCookie, accessToken, accessOptions);
|
|
752
|
+
}
|
|
753
|
+
async tryRefreshAccessToken(apiReq) {
|
|
754
|
+
const conf = this.config;
|
|
755
|
+
if (!conf.refreshSecret || !conf.accessSecret) {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
const rawRefresh = apiReq.req.cookies?.[conf.refreshCookie];
|
|
759
|
+
if (typeof rawRefresh !== 'string') {
|
|
760
|
+
return null;
|
|
604
761
|
}
|
|
605
|
-
|
|
762
|
+
const refreshToken = rawRefresh.trim();
|
|
763
|
+
if (!refreshToken) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
const verify = this.jwtVerify(refreshToken, conf.refreshSecret);
|
|
767
|
+
if (!verify.success || !verify.data) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
let stored = null;
|
|
771
|
+
try {
|
|
772
|
+
stored = await this.storageAdapter.getToken({ refreshToken });
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
if (!stored) {
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
const storedUid = String(stored.userId);
|
|
781
|
+
const verifyUid = verify.data.uid === undefined || verify.data.uid === null ? null : String(verify.data.uid);
|
|
782
|
+
if (verifyUid && verifyUid !== storedUid) {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
const claims = verify.data;
|
|
786
|
+
const { exp: _exp, iat: _iat, nbf: _nbf, ...payload } = claims;
|
|
787
|
+
void _exp;
|
|
788
|
+
void _iat;
|
|
789
|
+
void _nbf;
|
|
790
|
+
// Ensure we never embed token secrets into refreshed access tokens.
|
|
791
|
+
delete payload.accessToken;
|
|
792
|
+
delete payload.refreshToken;
|
|
793
|
+
delete payload.userId;
|
|
794
|
+
delete payload.expires;
|
|
795
|
+
delete payload.issuedAt;
|
|
796
|
+
delete payload.lastSeenAt;
|
|
797
|
+
delete payload.status;
|
|
798
|
+
const access = this.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
|
|
799
|
+
if (!access.success || !access.token) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
const updated = await this.updateToken({
|
|
803
|
+
refreshToken,
|
|
804
|
+
accessToken: access.token,
|
|
805
|
+
lastSeenAt: new Date()
|
|
806
|
+
});
|
|
807
|
+
if (!updated) {
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
this.setAccessCookie(apiReq, access.token, stored.sessionCookie ?? false);
|
|
811
|
+
if (apiReq.req.cookies) {
|
|
812
|
+
apiReq.req.cookies[conf.accessCookie] = access.token;
|
|
813
|
+
}
|
|
814
|
+
const verifiedAccess = await this.verifyJWT(access.token);
|
|
815
|
+
if (!verifiedAccess.tokenData) {
|
|
816
|
+
return null;
|
|
817
|
+
}
|
|
818
|
+
const refreshedStored = { ...stored, accessToken: access.token };
|
|
819
|
+
apiReq.authToken = refreshedStored;
|
|
820
|
+
return { token: access.token, tokenData: verifiedAccess.tokenData, stored: refreshedStored };
|
|
606
821
|
}
|
|
607
822
|
async authenticate(apiReq, authType) {
|
|
608
823
|
if (authType === 'none') {
|
|
@@ -612,6 +827,7 @@ class ApiServer {
|
|
|
612
827
|
let token = null;
|
|
613
828
|
const authHeader = apiReq.req.headers.authorization;
|
|
614
829
|
const requiresAuthToken = this.requiresAuthToken(authType);
|
|
830
|
+
const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
|
|
615
831
|
const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, authHeader);
|
|
616
832
|
if (apiKeyAuth) {
|
|
617
833
|
return apiKeyAuth;
|
|
@@ -620,32 +836,84 @@ class ApiServer {
|
|
|
620
836
|
token = authHeader.slice(7).trim();
|
|
621
837
|
}
|
|
622
838
|
if (!token) {
|
|
623
|
-
const access = apiReq.req.cookies?.
|
|
839
|
+
const access = apiReq.req.cookies?.[this.config.accessCookie];
|
|
624
840
|
if (access) {
|
|
625
841
|
token = access;
|
|
626
842
|
}
|
|
627
843
|
}
|
|
844
|
+
let tokenData;
|
|
845
|
+
let error;
|
|
846
|
+
let expired = false;
|
|
628
847
|
if (!token || token === null) {
|
|
629
|
-
if (
|
|
630
|
-
|
|
848
|
+
if (authType === 'maybe') {
|
|
849
|
+
if (!this.config.refreshMaybe) {
|
|
850
|
+
return null;
|
|
851
|
+
}
|
|
852
|
+
const refreshed = await this.tryRefreshAccessToken(apiReq);
|
|
853
|
+
if (!refreshed) {
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
token = refreshed.token;
|
|
857
|
+
tokenData = refreshed.tokenData;
|
|
858
|
+
error = undefined;
|
|
859
|
+
expired = false;
|
|
860
|
+
}
|
|
861
|
+
else if (requiresAuthToken) {
|
|
862
|
+
const refreshed = await this.tryRefreshAccessToken(apiReq);
|
|
863
|
+
if (!refreshed) {
|
|
864
|
+
throw new ApiError({ code: 401, message: 'Authorization token is required (Bearer/cookie)' });
|
|
865
|
+
}
|
|
866
|
+
token = refreshed.token;
|
|
867
|
+
tokenData = refreshed.tokenData;
|
|
868
|
+
error = undefined;
|
|
869
|
+
expired = false;
|
|
631
870
|
}
|
|
632
871
|
}
|
|
633
872
|
if (!token) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
873
|
+
throw new ApiError({ code: 401, message: 'Unauthorized Access - requires authentication' });
|
|
874
|
+
}
|
|
875
|
+
if (!tokenData) {
|
|
876
|
+
const verified = await this.verifyJWT(token);
|
|
877
|
+
tokenData = verified.tokenData;
|
|
878
|
+
error = verified.error;
|
|
879
|
+
expired = verified.expired ?? false;
|
|
880
|
+
}
|
|
881
|
+
if (!tokenData && allowRefresh && expired) {
|
|
882
|
+
const refreshed = await this.tryRefreshAccessToken(apiReq);
|
|
883
|
+
if (refreshed) {
|
|
884
|
+
token = refreshed.token;
|
|
885
|
+
tokenData = refreshed.tokenData;
|
|
886
|
+
error = undefined;
|
|
639
887
|
}
|
|
640
888
|
}
|
|
641
|
-
const { tokenData, error } = await this.verifyJWT(token);
|
|
642
889
|
if (!tokenData) {
|
|
643
890
|
throw new ApiError({ code: 401, message: 'Unathorized Access - ' + error });
|
|
644
891
|
}
|
|
645
892
|
const effectiveUserId = this.extractTokenUserId(tokenData);
|
|
646
893
|
apiReq.realUid = this.resolveRealUserId(tokenData, effectiveUserId);
|
|
647
894
|
if (this.shouldValidateStoredToken(authType)) {
|
|
648
|
-
|
|
895
|
+
try {
|
|
896
|
+
await this.assertStoredAccessToken(apiReq, token, tokenData);
|
|
897
|
+
}
|
|
898
|
+
catch (error) {
|
|
899
|
+
if (allowRefresh &&
|
|
900
|
+
error instanceof ApiError &&
|
|
901
|
+
error.code === 401 &&
|
|
902
|
+
error.message === 'Authorization token is no longer valid') {
|
|
903
|
+
const refreshed = await this.tryRefreshAccessToken(apiReq);
|
|
904
|
+
if (!refreshed) {
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
907
|
+
token = refreshed.token;
|
|
908
|
+
tokenData = refreshed.tokenData;
|
|
909
|
+
const refreshedUserId = this.extractTokenUserId(tokenData);
|
|
910
|
+
apiReq.realUid = this.resolveRealUserId(tokenData, refreshedUserId);
|
|
911
|
+
await this.assertStoredAccessToken(apiReq, token, tokenData);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
649
917
|
}
|
|
650
918
|
apiReq.token = token;
|
|
651
919
|
return tokenData;
|
|
@@ -686,7 +954,10 @@ class ApiServer {
|
|
|
686
954
|
return this.config.validateTokens || authType === 'strict';
|
|
687
955
|
}
|
|
688
956
|
async assertStoredAccessToken(apiReq, token, tokenData) {
|
|
689
|
-
const userId = this.extractTokenUserId(tokenData);
|
|
957
|
+
const userId = String(this.extractTokenUserId(tokenData));
|
|
958
|
+
if (apiReq.authToken && apiReq.authToken.accessToken === token && String(apiReq.authToken.userId) === userId) {
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
690
961
|
const stored = await this.storageAdapter.getToken({
|
|
691
962
|
accessToken: token,
|
|
692
963
|
userId
|
|
@@ -726,32 +997,98 @@ class ApiServer {
|
|
|
726
997
|
}
|
|
727
998
|
return rawReal;
|
|
728
999
|
}
|
|
729
|
-
|
|
1000
|
+
useExpress(pathOrHandler, ...handlers) {
|
|
1001
|
+
if (typeof pathOrHandler === 'string') {
|
|
1002
|
+
this.app.use(pathOrHandler, ...handlers);
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
this.app.use(pathOrHandler, ...handlers);
|
|
1006
|
+
}
|
|
1007
|
+
this.ensureApiNotFoundOrdering();
|
|
1008
|
+
return this;
|
|
1009
|
+
}
|
|
1010
|
+
createApiRequest(req, res) {
|
|
1011
|
+
const apiReq = {
|
|
1012
|
+
server: this,
|
|
1013
|
+
req,
|
|
1014
|
+
res,
|
|
1015
|
+
token: '',
|
|
1016
|
+
tokenData: null,
|
|
1017
|
+
realUid: null,
|
|
1018
|
+
getClientInfo: () => ensureClientInfo(apiReq),
|
|
1019
|
+
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
1020
|
+
getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
|
|
1021
|
+
getRealUid: () => apiReq.realUid ?? null,
|
|
1022
|
+
isImpersonating: () => {
|
|
1023
|
+
const realUid = apiReq.realUid;
|
|
1024
|
+
const tokenUid = apiReq.tokenData?.uid;
|
|
1025
|
+
if (realUid === null || realUid === undefined) {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
if (tokenUid === null || tokenUid === undefined) {
|
|
1029
|
+
return false;
|
|
1030
|
+
}
|
|
1031
|
+
return realUid !== tokenUid;
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
return apiReq;
|
|
1035
|
+
}
|
|
1036
|
+
expressAuth(auth) {
|
|
730
1037
|
return async (req, res, next) => {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
realUid: null,
|
|
739
|
-
getClientInfo: () => ensureClientInfo(apiReq),
|
|
740
|
-
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
741
|
-
getClientIpChain: () => ensureClientInfo(apiReq).ipchain,
|
|
742
|
-
getRealUid: () => apiReq.realUid ?? null,
|
|
743
|
-
isImpersonating: () => {
|
|
744
|
-
const realUid = apiReq.realUid;
|
|
745
|
-
const tokenUid = apiReq.tokenData?.uid;
|
|
746
|
-
if (realUid === null || realUid === undefined) {
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
if (tokenUid === null || tokenUid === undefined) {
|
|
750
|
-
return false;
|
|
751
|
-
}
|
|
752
|
-
return realUid !== tokenUid;
|
|
1038
|
+
const apiReq = this.createApiRequest(req, res);
|
|
1039
|
+
req.apiReq = apiReq;
|
|
1040
|
+
res.locals.apiReq = apiReq;
|
|
1041
|
+
this.currReq = apiReq;
|
|
1042
|
+
try {
|
|
1043
|
+
if (this.config.hydrateGetBody) {
|
|
1044
|
+
hydrateGetBody(req);
|
|
753
1045
|
}
|
|
1046
|
+
if (this.config.debug) {
|
|
1047
|
+
this.dumpRequest(apiReq);
|
|
1048
|
+
}
|
|
1049
|
+
apiReq.tokenData = await this.authenticate(apiReq, auth.type);
|
|
1050
|
+
await this.authorize(apiReq, auth.req);
|
|
1051
|
+
next();
|
|
1052
|
+
}
|
|
1053
|
+
catch (error) {
|
|
1054
|
+
next(error);
|
|
1055
|
+
}
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
expressErrorHandler() {
|
|
1059
|
+
return (error, _req, res, next) => {
|
|
1060
|
+
void _req;
|
|
1061
|
+
if (res.headersSent) {
|
|
1062
|
+
next(error);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (error instanceof ApiError || isApiErrorLike(error)) {
|
|
1066
|
+
const apiError = error;
|
|
1067
|
+
const normalizedErrors = apiError.errors && typeof apiError.errors === 'object' && !Array.isArray(apiError.errors)
|
|
1068
|
+
? apiError.errors
|
|
1069
|
+
: {};
|
|
1070
|
+
const errorPayload = {
|
|
1071
|
+
code: apiError.code,
|
|
1072
|
+
message: apiError.message,
|
|
1073
|
+
data: apiError.data ?? null,
|
|
1074
|
+
errors: normalizedErrors
|
|
1075
|
+
};
|
|
1076
|
+
res.status(apiError.code).json(errorPayload);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
const errorPayload = {
|
|
1080
|
+
code: 500,
|
|
1081
|
+
message: this.guessExceptionText(error),
|
|
1082
|
+
data: null,
|
|
1083
|
+
errors: {}
|
|
754
1084
|
};
|
|
1085
|
+
res.status(500).json(errorPayload);
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
handle_request(handler, auth) {
|
|
1089
|
+
return async (req, res, next) => {
|
|
1090
|
+
void next;
|
|
1091
|
+
const apiReq = this.createApiRequest(req, res);
|
|
755
1092
|
this.currReq = apiReq;
|
|
756
1093
|
try {
|
|
757
1094
|
if (this.config.hydrateGetBody) {
|