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