@technomoron/api-server-base 2.0.0-beta.18 → 2.0.0-beta.19
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/dist/cjs/api-module.cjs +9 -0
- package/dist/cjs/api-module.d.ts +4 -2
- package/dist/cjs/api-server-base.cjs +59 -37
- package/dist/cjs/api-server-base.d.ts +5 -0
- package/dist/cjs/auth-api/auth-module.d.ts +12 -1
- package/dist/cjs/auth-api/auth-module.js +42 -34
- package/dist/cjs/auth-api/mem-auth-store.js +2 -24
- package/dist/cjs/auth-api/sql-auth-store.js +4 -32
- package/dist/cjs/auth-api/user-id.d.ts +4 -0
- package/dist/cjs/auth-api/user-id.js +31 -0
- package/dist/cjs/auth-cookie-options.d.ts +11 -0
- package/dist/cjs/auth-cookie-options.js +57 -0
- package/dist/cjs/oauth/memory.js +2 -9
- package/dist/cjs/oauth/models.js +4 -15
- package/dist/cjs/oauth/sequelize.js +6 -22
- package/dist/cjs/passkey/config.d.ts +2 -0
- package/dist/cjs/passkey/config.js +26 -0
- package/dist/cjs/passkey/memory.js +2 -9
- package/dist/cjs/passkey/models.js +4 -15
- package/dist/cjs/passkey/sequelize.js +6 -22
- package/dist/cjs/sequelize-utils.d.ts +3 -0
- package/dist/cjs/sequelize-utils.js +17 -0
- package/dist/cjs/token/memory.d.ts +4 -0
- package/dist/cjs/token/memory.js +90 -25
- package/dist/cjs/token/sequelize.js +16 -22
- package/dist/cjs/token/types.d.ts +7 -0
- package/dist/cjs/user/memory.js +2 -9
- package/dist/cjs/user/sequelize.js +6 -22
- package/dist/esm/api-module.d.ts +4 -2
- package/dist/esm/api-module.js +9 -0
- package/dist/esm/api-server-base.d.ts +5 -0
- package/dist/esm/api-server-base.js +59 -37
- package/dist/esm/auth-api/auth-module.d.ts +12 -1
- package/dist/esm/auth-api/auth-module.js +42 -34
- package/dist/esm/auth-api/mem-auth-store.js +1 -23
- package/dist/esm/auth-api/sql-auth-store.js +2 -30
- package/dist/esm/auth-api/user-id.d.ts +4 -0
- package/dist/esm/auth-api/user-id.js +26 -0
- package/dist/esm/auth-cookie-options.d.ts +11 -0
- package/dist/esm/auth-cookie-options.js +54 -0
- package/dist/esm/oauth/memory.js +2 -9
- package/dist/esm/oauth/models.js +1 -12
- package/dist/esm/oauth/sequelize.js +3 -19
- package/dist/esm/passkey/config.d.ts +2 -0
- package/dist/esm/passkey/config.js +23 -0
- package/dist/esm/passkey/memory.js +2 -9
- package/dist/esm/passkey/models.js +1 -12
- package/dist/esm/passkey/sequelize.js +3 -19
- package/dist/esm/sequelize-utils.d.ts +3 -0
- package/dist/esm/sequelize-utils.js +12 -0
- package/dist/esm/token/memory.d.ts +4 -0
- package/dist/esm/token/memory.js +90 -25
- package/dist/esm/token/sequelize.js +12 -18
- package/dist/esm/token/types.d.ts +7 -0
- package/dist/esm/user/memory.js +2 -9
- package/dist/esm/user/sequelize.js +3 -19
- package/docs/swagger/openapi.json +1 -1
- package/package.json +1 -1
package/dist/cjs/api-module.cjs
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiModule = void 0;
|
|
4
4
|
class ApiModule {
|
|
5
|
+
get server() {
|
|
6
|
+
if (this._server === undefined) {
|
|
7
|
+
throw new Error('ApiModule.server is not set. Mount the module with ApiServer.api(...) before using it.');
|
|
8
|
+
}
|
|
9
|
+
return this._server;
|
|
10
|
+
}
|
|
11
|
+
set server(value) {
|
|
12
|
+
this._server = value;
|
|
13
|
+
}
|
|
5
14
|
constructor(opts = {}) {
|
|
6
15
|
this.mountpath = '';
|
|
7
16
|
this.namespace = opts.namespace ?? this.constructor.defaultNamespace ?? '';
|
package/dist/cjs/api-module.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface ApiKey {
|
|
|
7
7
|
uid: unknown;
|
|
8
8
|
}
|
|
9
9
|
export type ApiRoute = {
|
|
10
|
-
method: 'get' | 'post' | 'put' | 'delete';
|
|
10
|
+
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
11
11
|
path: string;
|
|
12
12
|
handler: ApiHandler;
|
|
13
13
|
auth: {
|
|
@@ -16,7 +16,9 @@ export type ApiRoute = {
|
|
|
16
16
|
};
|
|
17
17
|
};
|
|
18
18
|
export declare class ApiModule<T = unknown> {
|
|
19
|
-
|
|
19
|
+
private _server?;
|
|
20
|
+
get server(): T;
|
|
21
|
+
set server(value: T);
|
|
20
22
|
namespace: string;
|
|
21
23
|
mountpath: string;
|
|
22
24
|
static defaultNamespace: string;
|
|
@@ -20,6 +20,7 @@ const express_1 = __importDefault(require("express"));
|
|
|
20
20
|
const multer_1 = __importDefault(require("multer"));
|
|
21
21
|
const module_js_1 = require("./auth-api/module.js");
|
|
22
22
|
const storage_js_1 = require("./auth-api/storage.js");
|
|
23
|
+
const auth_cookie_options_js_1 = require("./auth-cookie-options.js");
|
|
23
24
|
const base_js_1 = require("./token/base.js");
|
|
24
25
|
class JwtHelperStore extends base_js_1.TokenStore {
|
|
25
26
|
async save() {
|
|
@@ -77,6 +78,7 @@ function hydrateGetBody(req) {
|
|
|
77
78
|
req.body = { ...query };
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
81
|
+
// Keep explicit body fields authoritative when both query and body provide the same key.
|
|
80
82
|
req.body = { ...query, ...body };
|
|
81
83
|
}
|
|
82
84
|
function normalizeIpAddress(candidate) {
|
|
@@ -337,6 +339,17 @@ function isApiErrorLike(candidate) {
|
|
|
337
339
|
const maybeError = candidate;
|
|
338
340
|
return typeof maybeError.code === 'number' && typeof maybeError.message === 'string';
|
|
339
341
|
}
|
|
342
|
+
function asHttpStatus(error) {
|
|
343
|
+
if (!error || typeof error !== 'object') {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const maybe = error;
|
|
347
|
+
const status = typeof maybe.status === 'number' ? maybe.status : maybe.statusCode;
|
|
348
|
+
if (typeof status === 'number' && status >= 400 && status <= 599) {
|
|
349
|
+
return status;
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
340
353
|
function fillConfig(config) {
|
|
341
354
|
return {
|
|
342
355
|
apiPort: config.apiPort ?? 3101,
|
|
@@ -369,7 +382,8 @@ function fillConfig(config) {
|
|
|
369
382
|
apiVersion: config.apiVersion ?? '',
|
|
370
383
|
minClientVersion: config.minClientVersion ?? '',
|
|
371
384
|
tokenStore: config.tokenStore,
|
|
372
|
-
authStores: config.authStores
|
|
385
|
+
authStores: config.authStores,
|
|
386
|
+
onStartError: config.onStartError
|
|
373
387
|
};
|
|
374
388
|
}
|
|
375
389
|
class ApiServer {
|
|
@@ -382,17 +396,23 @@ class ApiServer {
|
|
|
382
396
|
return null;
|
|
383
397
|
}
|
|
384
398
|
set currReq(_value) {
|
|
399
|
+
if (this.config.devMode && !this.currReqDeprecationWarned) {
|
|
400
|
+
this.currReqDeprecationWarned = true;
|
|
401
|
+
console.warn('[api-server-base] ApiServer.currReq is deprecated and always null. Use req.apiReq or res.locals.apiReq.');
|
|
402
|
+
}
|
|
385
403
|
void _value;
|
|
386
404
|
}
|
|
387
405
|
constructor(config = {}) {
|
|
388
406
|
this.finalized = false;
|
|
389
407
|
this.serverAuthAdapter = null;
|
|
390
408
|
this.apiNotFoundHandler = null;
|
|
409
|
+
this.apiErrorHandlerInstalled = false;
|
|
391
410
|
this.tokenStoreAdapter = null;
|
|
392
411
|
this.userStoreAdapter = null;
|
|
393
412
|
this.passkeyServiceAdapter = null;
|
|
394
413
|
this.oauthStoreAdapter = null;
|
|
395
414
|
this.canImpersonateAdapter = null;
|
|
415
|
+
this.currReqDeprecationWarned = false;
|
|
396
416
|
this.config = fillConfig(config);
|
|
397
417
|
this.apiBasePath = this.normalizeApiBasePath(this.config.apiBasePath);
|
|
398
418
|
this.startedAt = Date.now();
|
|
@@ -439,6 +459,7 @@ class ApiServer {
|
|
|
439
459
|
this.app.use(this.apiBasePath, this.apiRouter);
|
|
440
460
|
// addSwaggerUi(this.app);
|
|
441
461
|
this.installApiNotFoundHandler();
|
|
462
|
+
this.installApiErrorHandler();
|
|
442
463
|
}
|
|
443
464
|
assertNotFinalized(action) {
|
|
444
465
|
if (this.finalized) {
|
|
@@ -468,6 +489,7 @@ class ApiServer {
|
|
|
468
489
|
}
|
|
469
490
|
finalize() {
|
|
470
491
|
this.installApiNotFoundHandler();
|
|
492
|
+
this.installApiErrorHandler();
|
|
471
493
|
this.finalized = true;
|
|
472
494
|
return this;
|
|
473
495
|
}
|
|
@@ -810,8 +832,12 @@ class ApiServer {
|
|
|
810
832
|
const base = this.apiBasePath === '/' ? '' : this.apiBasePath;
|
|
811
833
|
const resolved = rawPath.length > 0 ? rawPath : `${base}/swagger`;
|
|
812
834
|
const path = resolved.startsWith('/') ? resolved : `/${resolved}`;
|
|
813
|
-
|
|
835
|
+
let specCache;
|
|
814
836
|
this.app.get(path, (_req, res) => {
|
|
837
|
+
if (specCache === undefined) {
|
|
838
|
+
specCache = this.loadSwaggerSpec();
|
|
839
|
+
}
|
|
840
|
+
const spec = specCache;
|
|
815
841
|
if (!spec) {
|
|
816
842
|
res.status(500).json({
|
|
817
843
|
success: false,
|
|
@@ -855,6 +881,13 @@ class ApiServer {
|
|
|
855
881
|
};
|
|
856
882
|
this.app.use(this.apiBasePath, this.apiNotFoundHandler);
|
|
857
883
|
}
|
|
884
|
+
installApiErrorHandler() {
|
|
885
|
+
if (this.apiErrorHandlerInstalled) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
this.apiErrorHandlerInstalled = true;
|
|
889
|
+
this.app.use(this.apiBasePath, this.expressErrorHandler());
|
|
890
|
+
}
|
|
858
891
|
describeMissingEndpoint(req) {
|
|
859
892
|
const method = typeof req.method === 'string' ? req.method.toUpperCase() : 'GET';
|
|
860
893
|
const target = req.originalUrl || req.url || this.apiBasePath;
|
|
@@ -889,10 +922,17 @@ class ApiServer {
|
|
|
889
922
|
}
|
|
890
923
|
const err = new Error(message);
|
|
891
924
|
err.cause = error;
|
|
925
|
+
if (typeof this.config.onStartError === 'function') {
|
|
926
|
+
this.config.onStartError(err);
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
892
929
|
throw err;
|
|
893
930
|
});
|
|
894
931
|
return this;
|
|
895
932
|
}
|
|
933
|
+
internalServerErrorMessage(error) {
|
|
934
|
+
return this.config.debug ? this.guessExceptionText(error) : 'Internal server error';
|
|
935
|
+
}
|
|
896
936
|
async verifyJWT(token) {
|
|
897
937
|
if (!this.config.accessSecret) {
|
|
898
938
|
return { tokenData: undefined, error: 'JWT authentication disabled; no jwtSecret set', expired: false };
|
|
@@ -907,39 +947,7 @@ class ApiServer {
|
|
|
907
947
|
return { tokenData: result.data, error: undefined, expired: false };
|
|
908
948
|
}
|
|
909
949
|
jwtCookieOptions(apiReq) {
|
|
910
|
-
|
|
911
|
-
const forwarded = apiReq.req.headers['x-forwarded-proto'];
|
|
912
|
-
const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
|
|
913
|
-
const origin = typeof referer === 'string' ? referer : '';
|
|
914
|
-
const forwardedProto = (typeof forwarded === 'string' ? forwarded : Array.isArray(forwarded) ? (forwarded[0] ?? '') : '')
|
|
915
|
-
.split(',')[0]
|
|
916
|
-
.trim()
|
|
917
|
-
.toLowerCase();
|
|
918
|
-
const isHttps = forwardedProto === 'https' || apiReq.req.protocol === 'https';
|
|
919
|
-
const isLocalhost = origin.includes('localhost');
|
|
920
|
-
const secure = conf.cookieSecure === true ? true : conf.cookieSecure === false ? false : /* auto */ Boolean(isHttps);
|
|
921
|
-
let sameSite = conf.cookieSameSite ?? 'lax';
|
|
922
|
-
if (sameSite !== 'lax' && sameSite !== 'strict' && sameSite !== 'none') {
|
|
923
|
-
sameSite = 'lax';
|
|
924
|
-
}
|
|
925
|
-
let resolvedSecure = secure;
|
|
926
|
-
if (sameSite === 'none' && resolvedSecure !== true) {
|
|
927
|
-
// Modern browsers reject SameSite=None cookies unless Secure is set.
|
|
928
|
-
resolvedSecure = true;
|
|
929
|
-
}
|
|
930
|
-
const options = {
|
|
931
|
-
httpOnly: conf.cookieHttpOnly ?? true,
|
|
932
|
-
secure: resolvedSecure,
|
|
933
|
-
sameSite,
|
|
934
|
-
domain: conf.cookieDomain || undefined,
|
|
935
|
-
path: conf.cookiePath || '/',
|
|
936
|
-
maxAge: undefined
|
|
937
|
-
};
|
|
938
|
-
if (conf.devMode && isLocalhost) {
|
|
939
|
-
// Domain cookies do not work on localhost; avoid breaking local development when cookieDomain is set.
|
|
940
|
-
options.domain = undefined;
|
|
941
|
-
}
|
|
942
|
-
return options;
|
|
950
|
+
return (0, auth_cookie_options_js_1.buildAuthCookieOptions)(this.config, apiReq.req);
|
|
943
951
|
}
|
|
944
952
|
setAccessCookie(apiReq, accessToken, sessionCookie) {
|
|
945
953
|
const conf = this.config;
|
|
@@ -1285,10 +1293,21 @@ class ApiServer {
|
|
|
1285
1293
|
res.status(apiError.code).json(errorPayload);
|
|
1286
1294
|
return;
|
|
1287
1295
|
}
|
|
1296
|
+
const status = asHttpStatus(error);
|
|
1297
|
+
if (status) {
|
|
1298
|
+
res.status(status).json({
|
|
1299
|
+
success: false,
|
|
1300
|
+
code: status,
|
|
1301
|
+
message: status === 400 ? 'Invalid request payload' : this.internalServerErrorMessage(error),
|
|
1302
|
+
data: null,
|
|
1303
|
+
errors: {}
|
|
1304
|
+
});
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1288
1307
|
const errorPayload = {
|
|
1289
1308
|
success: false,
|
|
1290
1309
|
code: 500,
|
|
1291
|
-
message: this.
|
|
1310
|
+
message: this.internalServerErrorMessage(error),
|
|
1292
1311
|
data: null,
|
|
1293
1312
|
errors: {}
|
|
1294
1313
|
};
|
|
@@ -1348,7 +1367,7 @@ class ApiServer {
|
|
|
1348
1367
|
const errorPayload = {
|
|
1349
1368
|
success: false,
|
|
1350
1369
|
code: 500,
|
|
1351
|
-
message: this.
|
|
1370
|
+
message: this.internalServerErrorMessage(error),
|
|
1352
1371
|
data: null,
|
|
1353
1372
|
errors: {}
|
|
1354
1373
|
};
|
|
@@ -1389,6 +1408,9 @@ class ApiServer {
|
|
|
1389
1408
|
case 'put':
|
|
1390
1409
|
router.put(r.path, handler);
|
|
1391
1410
|
break;
|
|
1411
|
+
case 'patch':
|
|
1412
|
+
router.patch(r.path, handler);
|
|
1413
|
+
break;
|
|
1392
1414
|
case 'delete':
|
|
1393
1415
|
router.delete(r.path, handler);
|
|
1394
1416
|
break;
|
|
@@ -129,6 +129,7 @@ export interface ApiServerConf {
|
|
|
129
129
|
minClientVersion: string;
|
|
130
130
|
tokenStore?: TokenStore;
|
|
131
131
|
authStores?: ApiServerAuthStores;
|
|
132
|
+
onStartError?: (error: Error) => void;
|
|
132
133
|
}
|
|
133
134
|
export declare class ApiServer {
|
|
134
135
|
app: Application;
|
|
@@ -141,12 +142,14 @@ export declare class ApiServer {
|
|
|
141
142
|
private moduleAdapter;
|
|
142
143
|
private serverAuthAdapter;
|
|
143
144
|
private apiNotFoundHandler;
|
|
145
|
+
private apiErrorHandlerInstalled;
|
|
144
146
|
private tokenStoreAdapter;
|
|
145
147
|
private userStoreAdapter;
|
|
146
148
|
private passkeyServiceAdapter;
|
|
147
149
|
private oauthStoreAdapter;
|
|
148
150
|
private canImpersonateAdapter;
|
|
149
151
|
private readonly jwtHelper;
|
|
152
|
+
private currReqDeprecationWarned;
|
|
150
153
|
/**
|
|
151
154
|
* @deprecated ApiServer does not track a global "current request". This value is always null.
|
|
152
155
|
* Use the per-request ApiRequest passed to handlers, or `req.apiReq` / `res.locals.apiReq`
|
|
@@ -225,8 +228,10 @@ export declare class ApiServer {
|
|
|
225
228
|
private installSwaggerHandler;
|
|
226
229
|
private normalizeApiBasePath;
|
|
227
230
|
private installApiNotFoundHandler;
|
|
231
|
+
private installApiErrorHandler;
|
|
228
232
|
private describeMissingEndpoint;
|
|
229
233
|
start(): this;
|
|
234
|
+
private internalServerErrorMessage;
|
|
230
235
|
private verifyJWT;
|
|
231
236
|
private jwtCookieOptions;
|
|
232
237
|
private setAccessCookie;
|
|
@@ -10,10 +10,16 @@ interface CanImpersonateContext<UserEntity> {
|
|
|
10
10
|
targetUser: UserEntity;
|
|
11
11
|
effectiveUserId: AuthIdentifier;
|
|
12
12
|
}
|
|
13
|
+
type AuthRateLimitEndpoint = 'login' | 'passkey-challenge' | 'oauth-token';
|
|
13
14
|
interface AuthModuleOptions<UserEntity> {
|
|
14
15
|
namespace?: string;
|
|
15
16
|
defaultDomain?: string;
|
|
16
17
|
canImpersonate?: (context: CanImpersonateContext<UserEntity>) => Promise<boolean> | boolean;
|
|
18
|
+
rateLimit?: (context: {
|
|
19
|
+
apiReq: ApiRequest;
|
|
20
|
+
endpoint: AuthRateLimitEndpoint;
|
|
21
|
+
}) => Promise<void> | void;
|
|
22
|
+
allowInsecurePkcePlain?: boolean;
|
|
17
23
|
}
|
|
18
24
|
type TokenMetadata = Partial<Token> & {
|
|
19
25
|
sessionCookie?: boolean;
|
|
@@ -42,9 +48,12 @@ type AuthCapableServer<PublicUser> = ApiServer & {
|
|
|
42
48
|
};
|
|
43
49
|
export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<UserEntity> implements AuthProviderModule<UserEntity> {
|
|
44
50
|
static defaultNamespace: string;
|
|
45
|
-
server: AuthCapableServer<PublicUser>;
|
|
51
|
+
get server(): AuthCapableServer<PublicUser>;
|
|
52
|
+
set server(value: AuthCapableServer<PublicUser>);
|
|
46
53
|
private readonly defaultDomain?;
|
|
47
54
|
private readonly canImpersonateHook?;
|
|
55
|
+
private readonly rateLimitHook?;
|
|
56
|
+
private readonly allowInsecurePkcePlain;
|
|
48
57
|
constructor(options?: AuthModuleOptions<UserEntity>);
|
|
49
58
|
protected get storage(): AuthAdapter<UserEntity, PublicUser>;
|
|
50
59
|
protected canImpersonate(apiReq: ApiRequest, realUser: UserEntity, targetUser: UserEntity): Promise<boolean>;
|
|
@@ -100,6 +109,8 @@ export default class AuthModule<UserEntity, PublicUser> extends BaseAuthModule<U
|
|
|
100
109
|
private hasOAuthStore;
|
|
101
110
|
private storageImplements;
|
|
102
111
|
private storageImplementsAll;
|
|
112
|
+
private applyRateLimit;
|
|
113
|
+
private resolvePkceChallengeMethod;
|
|
103
114
|
defineRoutes(): ApiRoute[];
|
|
104
115
|
}
|
|
105
116
|
export {};
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const node_crypto_1 = require("node:crypto");
|
|
4
4
|
const helpers_1 = require("@simplewebauthn/server/helpers");
|
|
5
5
|
const api_server_base_js_1 = require("../api-server-base.js");
|
|
6
|
+
const auth_cookie_options_js_1 = require("../auth-cookie-options.js");
|
|
6
7
|
const module_js_1 = require("./module.js");
|
|
7
8
|
const storage_js_1 = require("./storage.js");
|
|
8
9
|
function isAuthIdentifier(value) {
|
|
@@ -32,10 +33,18 @@ function sha256Base64Url(value) {
|
|
|
32
33
|
return base64UrlEncode(hash);
|
|
33
34
|
}
|
|
34
35
|
class AuthModule extends module_js_1.BaseAuthModule {
|
|
36
|
+
get server() {
|
|
37
|
+
return super.server;
|
|
38
|
+
}
|
|
39
|
+
set server(value) {
|
|
40
|
+
super.server = value;
|
|
41
|
+
}
|
|
35
42
|
constructor(options = {}) {
|
|
36
43
|
super({ namespace: options.namespace ?? AuthModule.defaultNamespace });
|
|
37
44
|
this.defaultDomain = options.defaultDomain;
|
|
38
45
|
this.canImpersonateHook = options.canImpersonate;
|
|
46
|
+
this.rateLimitHook = options.rateLimit;
|
|
47
|
+
this.allowInsecurePkcePlain = options.allowInsecurePkcePlain ?? true;
|
|
39
48
|
}
|
|
40
49
|
get storage() {
|
|
41
50
|
return this.server.getAuthStorage();
|
|
@@ -236,37 +245,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
236
245
|
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
237
246
|
}
|
|
238
247
|
cookieOptions(apiReq) {
|
|
239
|
-
|
|
240
|
-
const forwarded = apiReq.req.headers['x-forwarded-proto'];
|
|
241
|
-
const referer = apiReq.req.headers['origin'] ?? apiReq.req.headers['referer'];
|
|
242
|
-
const origin = typeof referer === 'string' ? referer : '';
|
|
243
|
-
const forwardedProto = (typeof forwarded === 'string' ? forwarded : Array.isArray(forwarded) ? (forwarded[0] ?? '') : '')
|
|
244
|
-
.split(',')[0]
|
|
245
|
-
.trim()
|
|
246
|
-
.toLowerCase();
|
|
247
|
-
const isHttps = forwardedProto === 'https' || apiReq.req.protocol === 'https';
|
|
248
|
-
const isLocalhost = origin.includes('localhost');
|
|
249
|
-
const secure = conf.cookieSecure === true ? true : conf.cookieSecure === false ? false : /* auto */ Boolean(isHttps);
|
|
250
|
-
let sameSite = conf.cookieSameSite ?? 'lax';
|
|
251
|
-
if (sameSite !== 'lax' && sameSite !== 'strict' && sameSite !== 'none') {
|
|
252
|
-
sameSite = 'lax';
|
|
253
|
-
}
|
|
254
|
-
let resolvedSecure = secure;
|
|
255
|
-
if (sameSite === 'none' && resolvedSecure !== true) {
|
|
256
|
-
resolvedSecure = true;
|
|
257
|
-
}
|
|
258
|
-
const options = {
|
|
259
|
-
httpOnly: conf.cookieHttpOnly ?? true,
|
|
260
|
-
secure: resolvedSecure,
|
|
261
|
-
sameSite,
|
|
262
|
-
domain: conf.cookieDomain || undefined,
|
|
263
|
-
path: conf.cookiePath || '/',
|
|
264
|
-
maxAge: undefined
|
|
265
|
-
};
|
|
266
|
-
if (conf.devMode && isLocalhost) {
|
|
267
|
-
options.domain = undefined;
|
|
268
|
-
}
|
|
269
|
-
return options;
|
|
248
|
+
return (0, auth_cookie_options_js_1.buildAuthCookieOptions)(this.server.config, apiReq.req);
|
|
270
249
|
}
|
|
271
250
|
setJwtCookies(apiReq, tokens, preferences = {}) {
|
|
272
251
|
const conf = this.server.config;
|
|
@@ -293,7 +272,10 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
293
272
|
async issueTokens(apiReq, user, metadata = {}) {
|
|
294
273
|
const conf = this.server.config;
|
|
295
274
|
const enrichedMetadata = this.enrichTokenMetadata(apiReq, metadata);
|
|
296
|
-
const payload =
|
|
275
|
+
const payload = {
|
|
276
|
+
...this.buildTokenPayload(user, enrichedMetadata),
|
|
277
|
+
jti: (0, node_crypto_1.randomUUID)()
|
|
278
|
+
};
|
|
297
279
|
const access = this.server.jwtSign(payload, conf.accessSecret, conf.accessExpiry);
|
|
298
280
|
if (!access.success || !access.token) {
|
|
299
281
|
throw new api_server_base_js_1.ApiError({ code: 500, message: access.error ?? 'Unable to sign access token' });
|
|
@@ -463,6 +445,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
463
445
|
return undefined;
|
|
464
446
|
}
|
|
465
447
|
async postLogin(apiReq) {
|
|
448
|
+
await this.applyRateLimit(apiReq, 'login');
|
|
466
449
|
this.assertAuthReady();
|
|
467
450
|
const { login, password, ...metadata } = this.parseLoginBody(apiReq);
|
|
468
451
|
const user = await this.storage.getUser(login);
|
|
@@ -626,6 +609,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
626
609
|
];
|
|
627
610
|
}
|
|
628
611
|
async postPasskeyChallenge(apiReq) {
|
|
612
|
+
await this.applyRateLimit(apiReq, 'passkey-challenge');
|
|
629
613
|
if (typeof this.storage.createPasskeyChallenge !== 'function') {
|
|
630
614
|
throw new api_server_base_js_1.ApiError({ code: 501, message: 'Passkey support is not configured' });
|
|
631
615
|
}
|
|
@@ -763,7 +747,8 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
763
747
|
async deleteImpersonation(apiReq) {
|
|
764
748
|
this.assertAuthReady();
|
|
765
749
|
const actor = await this.resolveActorContext(apiReq);
|
|
766
|
-
const
|
|
750
|
+
const query = (apiReq.req.query ?? {});
|
|
751
|
+
const metadata = this.buildImpersonationMetadata(query);
|
|
767
752
|
metadata.loginType = metadata.loginType ?? 'impersonation-end';
|
|
768
753
|
const tokens = await this.issueTokens(apiReq, actor.user, metadata);
|
|
769
754
|
const publicUser = this.storage.filterUser(actor.user);
|
|
@@ -835,6 +820,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
835
820
|
const state = toStringOrNull(body.state) ?? undefined;
|
|
836
821
|
const codeChallenge = toStringOrNull(body.codeChallenge) ?? undefined;
|
|
837
822
|
const codeChallengeMethod = toStringOrNull(body.codeChallengeMethod) ?? undefined;
|
|
823
|
+
const resolvedCodeChallengeMethod = this.resolvePkceChallengeMethod(codeChallengeMethod);
|
|
838
824
|
if (!clientId) {
|
|
839
825
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'clientId is required' });
|
|
840
826
|
}
|
|
@@ -854,7 +840,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
854
840
|
redirectUri,
|
|
855
841
|
scope: resolvedScope,
|
|
856
842
|
codeChallenge,
|
|
857
|
-
codeChallengeMethod:
|
|
843
|
+
codeChallengeMethod: resolvedCodeChallengeMethod,
|
|
858
844
|
expiresInSeconds: 300
|
|
859
845
|
});
|
|
860
846
|
const redirect = new URL(redirectUri);
|
|
@@ -865,6 +851,7 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
865
851
|
return [200, { code: codeRecord.code, redirectUri: redirect.toString(), state }];
|
|
866
852
|
}
|
|
867
853
|
async postOAuthToken(apiReq) {
|
|
854
|
+
await this.applyRateLimit(apiReq, 'oauth-token');
|
|
868
855
|
if (typeof this.storage.getClient !== 'function' || typeof this.storage.consumeAuthCode !== 'function') {
|
|
869
856
|
throw new api_server_base_js_1.ApiError({ code: 501, message: 'OAuth token storage is not configured' });
|
|
870
857
|
}
|
|
@@ -918,6 +905,9 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
918
905
|
}
|
|
919
906
|
}
|
|
920
907
|
else if (record.codeChallengeMethod === 'plain') {
|
|
908
|
+
if (!this.allowInsecurePkcePlain) {
|
|
909
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
|
|
910
|
+
}
|
|
921
911
|
if (codeVerifier !== record.codeChallenge) {
|
|
922
912
|
throw new api_server_base_js_1.ApiError({ code: 400, message: 'code_verifier does not match challenge' });
|
|
923
913
|
}
|
|
@@ -1118,6 +1108,24 @@ class AuthModule extends module_js_1.BaseAuthModule {
|
|
|
1118
1108
|
storageImplementsAll(keys) {
|
|
1119
1109
|
return keys.every((key) => this.storageImplements(key));
|
|
1120
1110
|
}
|
|
1111
|
+
async applyRateLimit(apiReq, endpoint) {
|
|
1112
|
+
if (!this.rateLimitHook) {
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
await this.rateLimitHook({ apiReq, endpoint });
|
|
1116
|
+
}
|
|
1117
|
+
resolvePkceChallengeMethod(value) {
|
|
1118
|
+
if (value === 'S256') {
|
|
1119
|
+
return 'S256';
|
|
1120
|
+
}
|
|
1121
|
+
if (value === 'plain') {
|
|
1122
|
+
if (!this.allowInsecurePkcePlain) {
|
|
1123
|
+
throw new api_server_base_js_1.ApiError({ code: 400, message: 'PKCE plain is not permitted' });
|
|
1124
|
+
}
|
|
1125
|
+
return 'plain';
|
|
1126
|
+
}
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1121
1129
|
defineRoutes() {
|
|
1122
1130
|
const routes = [];
|
|
1123
1131
|
const coreAuthSupported = this.storageImplementsAll([
|
|
@@ -2,33 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.MemAuthStore = void 0;
|
|
4
4
|
const memory_js_1 = require("../oauth/memory.js");
|
|
5
|
+
const config_js_1 = require("../passkey/config.js");
|
|
5
6
|
const memory_js_2 = require("../passkey/memory.js");
|
|
6
7
|
const memory_js_3 = require("../token/memory.js");
|
|
7
8
|
const memory_js_4 = require("../user/memory.js");
|
|
8
9
|
const compat_auth_storage_js_1 = require("./compat-auth-storage.js");
|
|
9
|
-
const DEFAULT_PASSKEY_CONFIG = {
|
|
10
|
-
rpId: 'localhost',
|
|
11
|
-
rpName: 'API Server',
|
|
12
|
-
origins: ['http://localhost:5173'],
|
|
13
|
-
timeoutMs: 5 * 60 * 1000,
|
|
14
|
-
userVerification: 'preferred'
|
|
15
|
-
};
|
|
16
|
-
function isOriginString(origin) {
|
|
17
|
-
return typeof origin === 'string' && origin.trim().length > 0;
|
|
18
|
-
}
|
|
19
|
-
function normalizePasskeyConfig(config = {}) {
|
|
20
|
-
const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
|
|
21
|
-
return {
|
|
22
|
-
rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
|
|
23
|
-
rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
|
|
24
|
-
origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
|
|
25
|
-
timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
|
|
26
|
-
? config.timeoutMs
|
|
27
|
-
: DEFAULT_PASSKEY_CONFIG.timeoutMs,
|
|
28
|
-
userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification,
|
|
29
|
-
debug: Boolean(config.debug)
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
10
|
class MemAuthStore {
|
|
33
11
|
constructor(params = {}) {
|
|
34
12
|
this.userStore = new memory_js_4.MemoryUserStore({
|
|
@@ -43,7 +21,7 @@ class MemAuthStore {
|
|
|
43
21
|
let passkeyStore;
|
|
44
22
|
let passkeyConfig;
|
|
45
23
|
if (params.passkeys !== false) {
|
|
46
|
-
passkeyConfig = normalizePasskeyConfig(params.passkeys ?? {});
|
|
24
|
+
passkeyConfig = (0, config_js_1.normalizePasskeyConfig)(params.passkeys ?? {});
|
|
47
25
|
const resolveUser = async (lookup) => {
|
|
48
26
|
const found = await this.userStore.findUser(lookup.userId ?? lookup.login ?? '');
|
|
49
27
|
if (!found) {
|
|
@@ -2,49 +2,21 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.SqlAuthStore = void 0;
|
|
4
4
|
const sequelize_js_1 = require("../oauth/sequelize.js");
|
|
5
|
+
const config_js_1 = require("../passkey/config.js");
|
|
5
6
|
const sequelize_js_2 = require("../passkey/sequelize.js");
|
|
7
|
+
const sequelize_utils_js_1 = require("../sequelize-utils.js");
|
|
6
8
|
const sequelize_js_3 = require("../token/sequelize.js");
|
|
7
9
|
const sequelize_js_4 = require("../user/sequelize.js");
|
|
8
10
|
const compat_auth_storage_js_1 = require("./compat-auth-storage.js");
|
|
9
|
-
const DEFAULT_PASSKEY_CONFIG = {
|
|
10
|
-
rpId: 'localhost',
|
|
11
|
-
rpName: 'API Server',
|
|
12
|
-
origins: ['http://localhost:5173'],
|
|
13
|
-
timeoutMs: 5 * 60 * 1000,
|
|
14
|
-
userVerification: 'preferred'
|
|
15
|
-
};
|
|
16
|
-
function normalizeTablePrefix(prefix) {
|
|
17
|
-
if (!prefix) {
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
const trimmed = prefix.trim();
|
|
21
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
22
|
-
}
|
|
23
11
|
function resolveTablePrefix(...prefixes) {
|
|
24
12
|
for (const prefix of prefixes) {
|
|
25
|
-
const normalized = normalizeTablePrefix(prefix);
|
|
13
|
+
const normalized = (0, sequelize_utils_js_1.normalizeTablePrefix)(prefix);
|
|
26
14
|
if (normalized) {
|
|
27
15
|
return normalized;
|
|
28
16
|
}
|
|
29
17
|
}
|
|
30
18
|
return undefined;
|
|
31
19
|
}
|
|
32
|
-
function isOriginString(origin) {
|
|
33
|
-
return typeof origin === 'string' && origin.trim().length > 0;
|
|
34
|
-
}
|
|
35
|
-
function normalizePasskeyConfig(config = {}) {
|
|
36
|
-
const candidateOrigins = Array.isArray(config.origins) && config.origins.length > 0 ? config.origins.filter(isOriginString) : null;
|
|
37
|
-
return {
|
|
38
|
-
rpId: config.rpId?.trim() || DEFAULT_PASSKEY_CONFIG.rpId,
|
|
39
|
-
rpName: config.rpName?.trim() || DEFAULT_PASSKEY_CONFIG.rpName,
|
|
40
|
-
origins: candidateOrigins ? candidateOrigins.map((origin) => origin.trim()) : DEFAULT_PASSKEY_CONFIG.origins,
|
|
41
|
-
timeoutMs: typeof config.timeoutMs === 'number' && config.timeoutMs > 0
|
|
42
|
-
? config.timeoutMs
|
|
43
|
-
: DEFAULT_PASSKEY_CONFIG.timeoutMs,
|
|
44
|
-
userVerification: config.userVerification ?? DEFAULT_PASSKEY_CONFIG.userVerification,
|
|
45
|
-
debug: Boolean(config.debug)
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
20
|
class SqlAuthStore {
|
|
49
21
|
constructor(params) {
|
|
50
22
|
this.closed = false;
|
|
@@ -84,7 +56,7 @@ class SqlAuthStore {
|
|
|
84
56
|
let passkeyConfig;
|
|
85
57
|
if (params.passkeys !== false) {
|
|
86
58
|
const passkeyTablePrefix = resolveTablePrefix(moduleTablePrefixes.passkey, params.tablePrefix);
|
|
87
|
-
passkeyConfig = normalizePasskeyConfig(params.passkeys ?? {});
|
|
59
|
+
passkeyConfig = (0, config_js_1.normalizePasskeyConfig)(params.passkeys ?? {});
|
|
88
60
|
const resolveUser = async (lookup) => {
|
|
89
61
|
const found = await this.userStore.findUser(lookup.userId ?? lookup.login ?? '');
|
|
90
62
|
if (!found) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AuthIdentifier } from './types.js';
|
|
2
|
+
export declare function normalizeComparableUserId(identifier: AuthIdentifier): string;
|
|
3
|
+
export declare function normalizeNumericUserId(identifier: AuthIdentifier): number;
|
|
4
|
+
export declare function normalizeStringUserId(identifier: AuthIdentifier): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeComparableUserId = normalizeComparableUserId;
|
|
4
|
+
exports.normalizeNumericUserId = normalizeNumericUserId;
|
|
5
|
+
exports.normalizeStringUserId = normalizeStringUserId;
|
|
6
|
+
function normalizeComparableUserId(identifier) {
|
|
7
|
+
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
|
|
8
|
+
return String(identifier);
|
|
9
|
+
}
|
|
10
|
+
if (typeof identifier === 'string') {
|
|
11
|
+
const trimmed = identifier.trim();
|
|
12
|
+
if (trimmed.length === 0) {
|
|
13
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
14
|
+
}
|
|
15
|
+
if (/^\d+$/.test(trimmed)) {
|
|
16
|
+
return String(Number(trimmed));
|
|
17
|
+
}
|
|
18
|
+
return trimmed;
|
|
19
|
+
}
|
|
20
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
21
|
+
}
|
|
22
|
+
function normalizeNumericUserId(identifier) {
|
|
23
|
+
const normalized = normalizeComparableUserId(identifier);
|
|
24
|
+
if (/^\d+$/.test(normalized)) {
|
|
25
|
+
return Number(normalized);
|
|
26
|
+
}
|
|
27
|
+
throw new Error(`Unable to normalise user identifier: ${identifier}`);
|
|
28
|
+
}
|
|
29
|
+
function normalizeStringUserId(identifier) {
|
|
30
|
+
return normalizeComparableUserId(identifier);
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Request } from 'express';
|
|
2
|
+
import type { CookieOptions } from 'express-serve-static-core';
|
|
3
|
+
export interface AuthCookieConfig {
|
|
4
|
+
cookieSecure?: boolean | 'auto';
|
|
5
|
+
cookieSameSite?: 'lax' | 'strict' | 'none';
|
|
6
|
+
cookieHttpOnly?: boolean;
|
|
7
|
+
cookieDomain?: string;
|
|
8
|
+
cookiePath?: string;
|
|
9
|
+
devMode?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export declare function buildAuthCookieOptions(config: AuthCookieConfig, req: Pick<Request, 'headers' | 'protocol'>): CookieOptions;
|