@technomoron/api-server-base 2.0.0-beta.21 → 2.0.0-beta.22
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 +4 -1
- package/dist/cjs/api-module.d.ts +3 -0
- package/dist/cjs/api-server-base.cjs +102 -18
- package/dist/cjs/api-server-base.d.ts +17 -0
- package/dist/esm/api-module.d.ts +3 -0
- package/dist/esm/api-server-base.d.ts +17 -0
- package/dist/esm/api-server-base.js +102 -18
- package/docs/swagger/openapi.json +411 -125
- package/package.json +128 -134
package/README.txt
CHANGED
|
@@ -111,6 +111,9 @@ cookiePath (string, default '/') Path applied to auth cookies.
|
|
|
111
111
|
cookieSameSite ('lax' | 'strict' | 'none', default 'lax') SameSite attribute applied to auth cookies.
|
|
112
112
|
cookieSecure (boolean | 'auto', default 'auto') Secure attribute applied to auth cookies; 'auto' enables Secure only when the request is HTTPS (or forwarded as HTTPS).
|
|
113
113
|
cookieHttpOnly (boolean, default true) HttpOnly attribute applied to auth cookies.
|
|
114
|
+
apiKeyPrefix (string, default '') Optional prefix required before API key secrets in Authorization Bearer tokens.
|
|
115
|
+
tokenParam (string, default '') Optional request field name used for token fallback when Bearer is missing.
|
|
116
|
+
tokenParamLocation ('body' | 'query' | 'body-query', default 'body') Controls where tokenParam is read from.
|
|
114
117
|
accessCookie (string, default 'dat') Access token cookie name.
|
|
115
118
|
refreshCookie (string, default 'drt') Refresh token cookie name.
|
|
116
119
|
accessExpiry (number, default 60 * 15) Access token lifetime in seconds.
|
|
@@ -129,7 +132,7 @@ Tip: If you add new configuration fields in downstream projects, extend ApiServe
|
|
|
129
132
|
-----------------
|
|
130
133
|
1. Express middlewares (express.json, cookie-parser, optional multer) run before your handler.
|
|
131
134
|
2. ApiServer wraps the route inside handle_request, creating an ApiRequest and logging when debug is enabled.
|
|
132
|
-
3. authenticate enforces the ApiRoute auth type: `none`, `maybe`, `yes`, `strict`, or `apikey`. Bearer
|
|
135
|
+
3. authenticate enforces the ApiRoute auth type: `none`, `maybe`, `yes`, `strict`, or `apikey`. It always prioritizes `Authorization: Bearer ...`. If Bearer is missing and `tokenParam` is configured, token fallback is read from the configured request location (`body`, `query`, or `body-query`) before checking the access cookie (`accessCookie`, default `dat`). API key tokens are matched with the optional `apiKeyPrefix` config (default `''`, meaning raw key values). When `refreshSecret` is configured and your storage supports refresh lookups (`getToken({ refreshToken })` + `updateToken(...)`), `yes`/`strict` routes will automatically mint a new access token when it is missing or expired (and also recover from "Authorization token is no longer valid" by refreshing). `maybe` routes only do the same when `refreshMaybe: true`. The optional `strict` type (or server-wide `validateTokens` flag) requires the signed JWT to exist in storage; when it does, the persisted row is attached to `apiReq.authToken`. The dedicated `apikey` type simply means “an API key is required”; otherwise API keys are still accepted by `yes`/`strict` routes alongside JWTs, and `apiReq.apiKey` is populated when present.
|
|
133
136
|
4. authorize runs with the requested auth class (any or admin in the base implementation). Override to connect to your role system.
|
|
134
137
|
5. The handler executes and returns its tuple. Responses are normalized to { code, message, data } JSON.
|
|
135
138
|
6. Errors bubble into the wrapper. ApiError instances respect the provided status codes; other exceptions result in a 500 with text derived from guessExceptionText.
|
package/dist/cjs/api-module.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ export type ApiHandler<Data = unknown> = (apiReq: ApiRequest) => Promise<ApiHand
|
|
|
4
4
|
export type ApiAuthType = 'none' | 'maybe' | 'yes' | 'strict' | 'apikey';
|
|
5
5
|
export type ApiAuthClass = 'any' | 'admin';
|
|
6
6
|
export interface ApiKey {
|
|
7
|
+
/** Real user identity — who the key belongs to. */
|
|
7
8
|
uid: unknown;
|
|
9
|
+
/** Effective user identity — who the key acts as. When set, the request is treated as impersonation (isImpersonating() === true). */
|
|
10
|
+
euid?: unknown;
|
|
8
11
|
}
|
|
9
12
|
export type ApiRoute = {
|
|
10
13
|
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
@@ -372,6 +372,10 @@ function fillConfig(config) {
|
|
|
372
372
|
cookieSameSite: config.cookieSameSite ?? 'lax',
|
|
373
373
|
cookieSecure: config.cookieSecure ?? 'auto',
|
|
374
374
|
cookieHttpOnly: config.cookieHttpOnly ?? true,
|
|
375
|
+
apiKeyPrefix: config.apiKeyPrefix ?? '',
|
|
376
|
+
apiKeyEnabled: config.apiKeyEnabled ?? false,
|
|
377
|
+
tokenParam: config.tokenParam ?? '',
|
|
378
|
+
tokenParamLocation: config.tokenParamLocation ?? 'body',
|
|
375
379
|
accessCookie: config.accessCookie ?? 'dat',
|
|
376
380
|
refreshCookie: config.refreshCookie ?? 'drt',
|
|
377
381
|
accessExpiry: config.accessExpiry ?? 60 * 15,
|
|
@@ -1042,19 +1046,28 @@ class ApiServer {
|
|
|
1042
1046
|
}
|
|
1043
1047
|
let token = null;
|
|
1044
1048
|
const authHeader = apiReq.req.headers.authorization;
|
|
1049
|
+
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() || null : null;
|
|
1050
|
+
const paramToken = bearerToken ? null : this.resolveTokenFromRequest(apiReq.req);
|
|
1045
1051
|
const requiresAuthToken = this.requiresAuthToken(authType);
|
|
1046
1052
|
const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1053
|
+
if (this.config.apiKeyEnabled || authType === 'apikey') {
|
|
1054
|
+
const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, bearerToken ?? paramToken);
|
|
1055
|
+
if (apiKeyAuth) {
|
|
1056
|
+
return apiKeyAuth;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
token = bearerToken ?? paramToken;
|
|
1060
|
+
if (bearerToken) {
|
|
1061
|
+
apiReq.authMethod = 'bearer';
|
|
1050
1062
|
}
|
|
1051
|
-
if (
|
|
1052
|
-
|
|
1063
|
+
else if (paramToken) {
|
|
1064
|
+
apiReq.authMethod = 'param';
|
|
1053
1065
|
}
|
|
1054
1066
|
if (!token) {
|
|
1055
1067
|
const access = apiReq.req.cookies?.[this.config.accessCookie];
|
|
1056
1068
|
if (access) {
|
|
1057
1069
|
token = access;
|
|
1070
|
+
apiReq.authMethod = 'cookie';
|
|
1058
1071
|
}
|
|
1059
1072
|
}
|
|
1060
1073
|
let tokenData;
|
|
@@ -1134,40 +1147,83 @@ class ApiServer {
|
|
|
1134
1147
|
apiReq.token = token;
|
|
1135
1148
|
return tokenData;
|
|
1136
1149
|
}
|
|
1137
|
-
async tryAuthenticateApiKey(apiReq, authType,
|
|
1138
|
-
if (!
|
|
1150
|
+
async tryAuthenticateApiKey(apiReq, authType, tokenCandidate) {
|
|
1151
|
+
if (!tokenCandidate) {
|
|
1139
1152
|
if (authType === 'apikey') {
|
|
1140
1153
|
throw new ApiError({ code: 401, message: 'Authorization header is missing or invalid' });
|
|
1141
1154
|
}
|
|
1142
1155
|
return null;
|
|
1143
1156
|
}
|
|
1144
|
-
const keyToken =
|
|
1145
|
-
|
|
1157
|
+
const keyToken = tokenCandidate;
|
|
1158
|
+
const prefix = this.config.apiKeyPrefix;
|
|
1159
|
+
const secret = prefix === '' ? keyToken : keyToken.startsWith(prefix) ? keyToken.slice(prefix.length) : null;
|
|
1160
|
+
if (!secret) {
|
|
1146
1161
|
if (authType === 'apikey') {
|
|
1147
1162
|
throw new ApiError({ code: 401, message: 'Invalid API Key' });
|
|
1148
1163
|
}
|
|
1149
1164
|
return null;
|
|
1150
1165
|
}
|
|
1151
|
-
const secret = keyToken.replace(/^apikey-/, '');
|
|
1152
1166
|
const key = await this.getApiKey(secret);
|
|
1153
1167
|
if (!key) {
|
|
1154
|
-
|
|
1168
|
+
if (authType === 'apikey') {
|
|
1169
|
+
throw new ApiError({ code: 401, message: 'Invalid API Key' });
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1155
1172
|
}
|
|
1156
1173
|
apiReq.token = secret;
|
|
1157
1174
|
apiReq.apiKey = key;
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1175
|
+
apiReq.authMethod = 'apikey';
|
|
1176
|
+
// uid is the real identity; euid (if set) is the effective/impersonated identity.
|
|
1177
|
+
const resolvedRuid = this.normalizeAuthIdentifier(key.uid);
|
|
1178
|
+
const resolvedEuid = key.euid !== undefined ? this.normalizeAuthIdentifier(key.euid) : null;
|
|
1179
|
+
const effectiveUid = resolvedEuid ?? resolvedRuid;
|
|
1180
|
+
if (resolvedRuid !== null) {
|
|
1181
|
+
apiReq.realUid = resolvedRuid;
|
|
1162
1182
|
}
|
|
1163
1183
|
return {
|
|
1164
|
-
uid:
|
|
1184
|
+
uid: effectiveUid,
|
|
1185
|
+
...(resolvedEuid !== null ? { ruid: resolvedRuid !== null ? String(resolvedRuid) : undefined } : {}),
|
|
1165
1186
|
domain: '',
|
|
1166
1187
|
fingerprint: '',
|
|
1167
1188
|
iat: 0,
|
|
1168
1189
|
exp: 0
|
|
1169
1190
|
};
|
|
1170
1191
|
}
|
|
1192
|
+
resolveTokenFromRequest(req) {
|
|
1193
|
+
const paramName = this.config.tokenParam.trim();
|
|
1194
|
+
if (!paramName) {
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1197
|
+
const location = this.config.tokenParamLocation;
|
|
1198
|
+
if (location === 'body') {
|
|
1199
|
+
return this.readNamedValue(req.body, paramName);
|
|
1200
|
+
}
|
|
1201
|
+
if (location === 'query') {
|
|
1202
|
+
return this.readNamedValue(req.query, paramName);
|
|
1203
|
+
}
|
|
1204
|
+
return this.readNamedValue(req.body, paramName) ?? this.readNamedValue(req.query, paramName);
|
|
1205
|
+
}
|
|
1206
|
+
readNamedValue(container, key) {
|
|
1207
|
+
if (!container || typeof container !== 'object' || Array.isArray(container)) {
|
|
1208
|
+
return null;
|
|
1209
|
+
}
|
|
1210
|
+
const raw = container[key];
|
|
1211
|
+
if (typeof raw === 'string') {
|
|
1212
|
+
const token = raw.trim();
|
|
1213
|
+
return token.length > 0 ? token : null;
|
|
1214
|
+
}
|
|
1215
|
+
if (Array.isArray(raw)) {
|
|
1216
|
+
for (const entry of raw) {
|
|
1217
|
+
if (typeof entry === 'string') {
|
|
1218
|
+
const token = entry.trim();
|
|
1219
|
+
if (token.length > 0) {
|
|
1220
|
+
return token;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1171
1227
|
requiresAuthToken(authType) {
|
|
1172
1228
|
return authType === 'yes' || authType === 'strict';
|
|
1173
1229
|
}
|
|
@@ -1238,6 +1294,7 @@ class ApiServer {
|
|
|
1238
1294
|
res,
|
|
1239
1295
|
token: '',
|
|
1240
1296
|
tokenData: null,
|
|
1297
|
+
authMethod: null,
|
|
1241
1298
|
realUid: null,
|
|
1242
1299
|
getClientInfo: () => ensureClientInfo(apiReq),
|
|
1243
1300
|
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
@@ -1431,12 +1488,25 @@ class ApiServer {
|
|
|
1431
1488
|
}
|
|
1432
1489
|
dumpRequest(apiReq) {
|
|
1433
1490
|
const req = apiReq.req;
|
|
1491
|
+
const tokenParam = this.config.tokenParam.trim();
|
|
1434
1492
|
console.log('--- Incoming Request! ---');
|
|
1435
1493
|
const url = req.originalUrl || req.url;
|
|
1436
1494
|
console.log('URL:', url);
|
|
1437
1495
|
console.log('Method:', req.method);
|
|
1438
|
-
|
|
1496
|
+
if (tokenParam &&
|
|
1497
|
+
req.query &&
|
|
1498
|
+
typeof req.query === 'object' &&
|
|
1499
|
+
tokenParam in req.query) {
|
|
1500
|
+
const maskedQuery = { ...req.query, [tokenParam]: '[REDACTED]' };
|
|
1501
|
+
console.log('Query Params:', maskedQuery);
|
|
1502
|
+
}
|
|
1503
|
+
else {
|
|
1504
|
+
console.log('Query Params:', req.query || {});
|
|
1505
|
+
}
|
|
1439
1506
|
const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
|
|
1507
|
+
if (tokenParam) {
|
|
1508
|
+
sensitiveBodyKeys.push(tokenParam);
|
|
1509
|
+
}
|
|
1440
1510
|
const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
|
|
1441
1511
|
if (body && typeof body === 'object') {
|
|
1442
1512
|
for (const key of sensitiveBodyKeys) {
|
|
@@ -1507,7 +1577,21 @@ class ApiServer {
|
|
|
1507
1577
|
return value;
|
|
1508
1578
|
}
|
|
1509
1579
|
dumpResponse(apiReq, payload, status) {
|
|
1510
|
-
const
|
|
1580
|
+
const rawUrl = apiReq.req.originalUrl || apiReq.req.url;
|
|
1581
|
+
const tokenParam = this.config.tokenParam.trim();
|
|
1582
|
+
let url = rawUrl;
|
|
1583
|
+
if (tokenParam && rawUrl) {
|
|
1584
|
+
try {
|
|
1585
|
+
const parsed = new URL(rawUrl, 'http://x');
|
|
1586
|
+
if (parsed.searchParams.has(tokenParam)) {
|
|
1587
|
+
parsed.searchParams.set(tokenParam, '[REDACTED]');
|
|
1588
|
+
url = parsed.pathname + '?' + parsed.searchParams.toString();
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
catch {
|
|
1592
|
+
// Leave url unmodified if parsing fails
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1511
1595
|
console.log('--- Outgoing Response! ---');
|
|
1512
1596
|
console.log('URL:', url);
|
|
1513
1597
|
console.log('Status:', status);
|
|
@@ -31,6 +31,7 @@ export interface ApiTokenData extends JwtPayload, Partial<Token> {
|
|
|
31
31
|
iat?: number;
|
|
32
32
|
exp?: number;
|
|
33
33
|
}
|
|
34
|
+
export type ApiAuthMethod = 'bearer' | 'cookie' | 'param' | 'apikey' | null;
|
|
34
35
|
export interface ApiRequest {
|
|
35
36
|
server: ApiServer;
|
|
36
37
|
req: ExtendedReq;
|
|
@@ -39,6 +40,8 @@ export interface ApiRequest {
|
|
|
39
40
|
token?: string;
|
|
40
41
|
authToken?: Token | null;
|
|
41
42
|
apiKey?: ApiKey | null;
|
|
43
|
+
/** How this request was authenticated. null when unauthenticated. */
|
|
44
|
+
authMethod?: ApiAuthMethod;
|
|
42
45
|
clientInfo?: ClientInfo;
|
|
43
46
|
realUid?: AuthIdentifier | null;
|
|
44
47
|
getClientInfo: () => ClientInfo;
|
|
@@ -115,6 +118,18 @@ export interface ApiServerConf {
|
|
|
115
118
|
cookieSecure?: boolean | 'auto';
|
|
116
119
|
/** Cookie HttpOnly attribute for auth cookies. */
|
|
117
120
|
cookieHttpOnly?: boolean;
|
|
121
|
+
/** Prefix used for API keys in Authorization Bearer tokens. */
|
|
122
|
+
apiKeyPrefix: string;
|
|
123
|
+
/**
|
|
124
|
+
* Enable API key authentication on non-apikey routes (e.g. 'yes', 'strict', 'maybe').
|
|
125
|
+
* When false (default), API key lookups only run on routes with authType 'apikey'.
|
|
126
|
+
* Set to true if you need API keys to authenticate on JWT routes as well.
|
|
127
|
+
*/
|
|
128
|
+
apiKeyEnabled: boolean;
|
|
129
|
+
/** Optional request field name used to read auth tokens when Bearer is missing. */
|
|
130
|
+
tokenParam: string;
|
|
131
|
+
/** Where to read `tokenParam` from. */
|
|
132
|
+
tokenParamLocation: 'body' | 'query' | 'body-query';
|
|
118
133
|
accessCookie: string;
|
|
119
134
|
refreshCookie: string;
|
|
120
135
|
accessExpiry: number;
|
|
@@ -238,6 +253,8 @@ export declare class ApiServer {
|
|
|
238
253
|
private tryRefreshAccessToken;
|
|
239
254
|
private authenticate;
|
|
240
255
|
private tryAuthenticateApiKey;
|
|
256
|
+
private resolveTokenFromRequest;
|
|
257
|
+
private readNamedValue;
|
|
241
258
|
private requiresAuthToken;
|
|
242
259
|
private shouldValidateStoredToken;
|
|
243
260
|
private assertStoredAccessToken;
|
package/dist/esm/api-module.d.ts
CHANGED
|
@@ -4,7 +4,10 @@ export type ApiHandler<Data = unknown> = (apiReq: ApiRequest) => Promise<ApiHand
|
|
|
4
4
|
export type ApiAuthType = 'none' | 'maybe' | 'yes' | 'strict' | 'apikey';
|
|
5
5
|
export type ApiAuthClass = 'any' | 'admin';
|
|
6
6
|
export interface ApiKey {
|
|
7
|
+
/** Real user identity — who the key belongs to. */
|
|
7
8
|
uid: unknown;
|
|
9
|
+
/** Effective user identity — who the key acts as. When set, the request is treated as impersonation (isImpersonating() === true). */
|
|
10
|
+
euid?: unknown;
|
|
8
11
|
}
|
|
9
12
|
export type ApiRoute = {
|
|
10
13
|
method: 'get' | 'post' | 'put' | 'patch' | 'delete';
|
|
@@ -31,6 +31,7 @@ export interface ApiTokenData extends JwtPayload, Partial<Token> {
|
|
|
31
31
|
iat?: number;
|
|
32
32
|
exp?: number;
|
|
33
33
|
}
|
|
34
|
+
export type ApiAuthMethod = 'bearer' | 'cookie' | 'param' | 'apikey' | null;
|
|
34
35
|
export interface ApiRequest {
|
|
35
36
|
server: ApiServer;
|
|
36
37
|
req: ExtendedReq;
|
|
@@ -39,6 +40,8 @@ export interface ApiRequest {
|
|
|
39
40
|
token?: string;
|
|
40
41
|
authToken?: Token | null;
|
|
41
42
|
apiKey?: ApiKey | null;
|
|
43
|
+
/** How this request was authenticated. null when unauthenticated. */
|
|
44
|
+
authMethod?: ApiAuthMethod;
|
|
42
45
|
clientInfo?: ClientInfo;
|
|
43
46
|
realUid?: AuthIdentifier | null;
|
|
44
47
|
getClientInfo: () => ClientInfo;
|
|
@@ -115,6 +118,18 @@ export interface ApiServerConf {
|
|
|
115
118
|
cookieSecure?: boolean | 'auto';
|
|
116
119
|
/** Cookie HttpOnly attribute for auth cookies. */
|
|
117
120
|
cookieHttpOnly?: boolean;
|
|
121
|
+
/** Prefix used for API keys in Authorization Bearer tokens. */
|
|
122
|
+
apiKeyPrefix: string;
|
|
123
|
+
/**
|
|
124
|
+
* Enable API key authentication on non-apikey routes (e.g. 'yes', 'strict', 'maybe').
|
|
125
|
+
* When false (default), API key lookups only run on routes with authType 'apikey'.
|
|
126
|
+
* Set to true if you need API keys to authenticate on JWT routes as well.
|
|
127
|
+
*/
|
|
128
|
+
apiKeyEnabled: boolean;
|
|
129
|
+
/** Optional request field name used to read auth tokens when Bearer is missing. */
|
|
130
|
+
tokenParam: string;
|
|
131
|
+
/** Where to read `tokenParam` from. */
|
|
132
|
+
tokenParamLocation: 'body' | 'query' | 'body-query';
|
|
118
133
|
accessCookie: string;
|
|
119
134
|
refreshCookie: string;
|
|
120
135
|
accessExpiry: number;
|
|
@@ -238,6 +253,8 @@ export declare class ApiServer {
|
|
|
238
253
|
private tryRefreshAccessToken;
|
|
239
254
|
private authenticate;
|
|
240
255
|
private tryAuthenticateApiKey;
|
|
256
|
+
private resolveTokenFromRequest;
|
|
257
|
+
private readNamedValue;
|
|
241
258
|
private requiresAuthToken;
|
|
242
259
|
private shouldValidateStoredToken;
|
|
243
260
|
private assertStoredAccessToken;
|
|
@@ -364,6 +364,10 @@ function fillConfig(config) {
|
|
|
364
364
|
cookieSameSite: config.cookieSameSite ?? 'lax',
|
|
365
365
|
cookieSecure: config.cookieSecure ?? 'auto',
|
|
366
366
|
cookieHttpOnly: config.cookieHttpOnly ?? true,
|
|
367
|
+
apiKeyPrefix: config.apiKeyPrefix ?? '',
|
|
368
|
+
apiKeyEnabled: config.apiKeyEnabled ?? false,
|
|
369
|
+
tokenParam: config.tokenParam ?? '',
|
|
370
|
+
tokenParamLocation: config.tokenParamLocation ?? 'body',
|
|
367
371
|
accessCookie: config.accessCookie ?? 'dat',
|
|
368
372
|
refreshCookie: config.refreshCookie ?? 'drt',
|
|
369
373
|
accessExpiry: config.accessExpiry ?? 60 * 15,
|
|
@@ -1034,19 +1038,28 @@ export class ApiServer {
|
|
|
1034
1038
|
}
|
|
1035
1039
|
let token = null;
|
|
1036
1040
|
const authHeader = apiReq.req.headers.authorization;
|
|
1041
|
+
const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7).trim() || null : null;
|
|
1042
|
+
const paramToken = bearerToken ? null : this.resolveTokenFromRequest(apiReq.req);
|
|
1037
1043
|
const requiresAuthToken = this.requiresAuthToken(authType);
|
|
1038
1044
|
const allowRefresh = requiresAuthToken || (authType === 'maybe' && this.config.refreshMaybe);
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1045
|
+
if (this.config.apiKeyEnabled || authType === 'apikey') {
|
|
1046
|
+
const apiKeyAuth = await this.tryAuthenticateApiKey(apiReq, authType, bearerToken ?? paramToken);
|
|
1047
|
+
if (apiKeyAuth) {
|
|
1048
|
+
return apiKeyAuth;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
token = bearerToken ?? paramToken;
|
|
1052
|
+
if (bearerToken) {
|
|
1053
|
+
apiReq.authMethod = 'bearer';
|
|
1042
1054
|
}
|
|
1043
|
-
if (
|
|
1044
|
-
|
|
1055
|
+
else if (paramToken) {
|
|
1056
|
+
apiReq.authMethod = 'param';
|
|
1045
1057
|
}
|
|
1046
1058
|
if (!token) {
|
|
1047
1059
|
const access = apiReq.req.cookies?.[this.config.accessCookie];
|
|
1048
1060
|
if (access) {
|
|
1049
1061
|
token = access;
|
|
1062
|
+
apiReq.authMethod = 'cookie';
|
|
1050
1063
|
}
|
|
1051
1064
|
}
|
|
1052
1065
|
let tokenData;
|
|
@@ -1126,40 +1139,83 @@ export class ApiServer {
|
|
|
1126
1139
|
apiReq.token = token;
|
|
1127
1140
|
return tokenData;
|
|
1128
1141
|
}
|
|
1129
|
-
async tryAuthenticateApiKey(apiReq, authType,
|
|
1130
|
-
if (!
|
|
1142
|
+
async tryAuthenticateApiKey(apiReq, authType, tokenCandidate) {
|
|
1143
|
+
if (!tokenCandidate) {
|
|
1131
1144
|
if (authType === 'apikey') {
|
|
1132
1145
|
throw new ApiError({ code: 401, message: 'Authorization header is missing or invalid' });
|
|
1133
1146
|
}
|
|
1134
1147
|
return null;
|
|
1135
1148
|
}
|
|
1136
|
-
const keyToken =
|
|
1137
|
-
|
|
1149
|
+
const keyToken = tokenCandidate;
|
|
1150
|
+
const prefix = this.config.apiKeyPrefix;
|
|
1151
|
+
const secret = prefix === '' ? keyToken : keyToken.startsWith(prefix) ? keyToken.slice(prefix.length) : null;
|
|
1152
|
+
if (!secret) {
|
|
1138
1153
|
if (authType === 'apikey') {
|
|
1139
1154
|
throw new ApiError({ code: 401, message: 'Invalid API Key' });
|
|
1140
1155
|
}
|
|
1141
1156
|
return null;
|
|
1142
1157
|
}
|
|
1143
|
-
const secret = keyToken.replace(/^apikey-/, '');
|
|
1144
1158
|
const key = await this.getApiKey(secret);
|
|
1145
1159
|
if (!key) {
|
|
1146
|
-
|
|
1160
|
+
if (authType === 'apikey') {
|
|
1161
|
+
throw new ApiError({ code: 401, message: 'Invalid API Key' });
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1147
1164
|
}
|
|
1148
1165
|
apiReq.token = secret;
|
|
1149
1166
|
apiReq.apiKey = key;
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1167
|
+
apiReq.authMethod = 'apikey';
|
|
1168
|
+
// uid is the real identity; euid (if set) is the effective/impersonated identity.
|
|
1169
|
+
const resolvedRuid = this.normalizeAuthIdentifier(key.uid);
|
|
1170
|
+
const resolvedEuid = key.euid !== undefined ? this.normalizeAuthIdentifier(key.euid) : null;
|
|
1171
|
+
const effectiveUid = resolvedEuid ?? resolvedRuid;
|
|
1172
|
+
if (resolvedRuid !== null) {
|
|
1173
|
+
apiReq.realUid = resolvedRuid;
|
|
1154
1174
|
}
|
|
1155
1175
|
return {
|
|
1156
|
-
uid:
|
|
1176
|
+
uid: effectiveUid,
|
|
1177
|
+
...(resolvedEuid !== null ? { ruid: resolvedRuid !== null ? String(resolvedRuid) : undefined } : {}),
|
|
1157
1178
|
domain: '',
|
|
1158
1179
|
fingerprint: '',
|
|
1159
1180
|
iat: 0,
|
|
1160
1181
|
exp: 0
|
|
1161
1182
|
};
|
|
1162
1183
|
}
|
|
1184
|
+
resolveTokenFromRequest(req) {
|
|
1185
|
+
const paramName = this.config.tokenParam.trim();
|
|
1186
|
+
if (!paramName) {
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
const location = this.config.tokenParamLocation;
|
|
1190
|
+
if (location === 'body') {
|
|
1191
|
+
return this.readNamedValue(req.body, paramName);
|
|
1192
|
+
}
|
|
1193
|
+
if (location === 'query') {
|
|
1194
|
+
return this.readNamedValue(req.query, paramName);
|
|
1195
|
+
}
|
|
1196
|
+
return this.readNamedValue(req.body, paramName) ?? this.readNamedValue(req.query, paramName);
|
|
1197
|
+
}
|
|
1198
|
+
readNamedValue(container, key) {
|
|
1199
|
+
if (!container || typeof container !== 'object' || Array.isArray(container)) {
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
const raw = container[key];
|
|
1203
|
+
if (typeof raw === 'string') {
|
|
1204
|
+
const token = raw.trim();
|
|
1205
|
+
return token.length > 0 ? token : null;
|
|
1206
|
+
}
|
|
1207
|
+
if (Array.isArray(raw)) {
|
|
1208
|
+
for (const entry of raw) {
|
|
1209
|
+
if (typeof entry === 'string') {
|
|
1210
|
+
const token = entry.trim();
|
|
1211
|
+
if (token.length > 0) {
|
|
1212
|
+
return token;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1163
1219
|
requiresAuthToken(authType) {
|
|
1164
1220
|
return authType === 'yes' || authType === 'strict';
|
|
1165
1221
|
}
|
|
@@ -1230,6 +1286,7 @@ export class ApiServer {
|
|
|
1230
1286
|
res,
|
|
1231
1287
|
token: '',
|
|
1232
1288
|
tokenData: null,
|
|
1289
|
+
authMethod: null,
|
|
1233
1290
|
realUid: null,
|
|
1234
1291
|
getClientInfo: () => ensureClientInfo(apiReq),
|
|
1235
1292
|
getClientIp: () => ensureClientInfo(apiReq).ip,
|
|
@@ -1423,12 +1480,25 @@ export class ApiServer {
|
|
|
1423
1480
|
}
|
|
1424
1481
|
dumpRequest(apiReq) {
|
|
1425
1482
|
const req = apiReq.req;
|
|
1483
|
+
const tokenParam = this.config.tokenParam.trim();
|
|
1426
1484
|
console.log('--- Incoming Request! ---');
|
|
1427
1485
|
const url = req.originalUrl || req.url;
|
|
1428
1486
|
console.log('URL:', url);
|
|
1429
1487
|
console.log('Method:', req.method);
|
|
1430
|
-
|
|
1488
|
+
if (tokenParam &&
|
|
1489
|
+
req.query &&
|
|
1490
|
+
typeof req.query === 'object' &&
|
|
1491
|
+
tokenParam in req.query) {
|
|
1492
|
+
const maskedQuery = { ...req.query, [tokenParam]: '[REDACTED]' };
|
|
1493
|
+
console.log('Query Params:', maskedQuery);
|
|
1494
|
+
}
|
|
1495
|
+
else {
|
|
1496
|
+
console.log('Query Params:', req.query || {});
|
|
1497
|
+
}
|
|
1431
1498
|
const sensitiveBodyKeys = ['password', 'client_secret', 'clientSecret', 'secret'];
|
|
1499
|
+
if (tokenParam) {
|
|
1500
|
+
sensitiveBodyKeys.push(tokenParam);
|
|
1501
|
+
}
|
|
1432
1502
|
const body = req.body && typeof req.body === 'object' ? { ...req.body } : req.body;
|
|
1433
1503
|
if (body && typeof body === 'object') {
|
|
1434
1504
|
for (const key of sensitiveBodyKeys) {
|
|
@@ -1499,7 +1569,21 @@ export class ApiServer {
|
|
|
1499
1569
|
return value;
|
|
1500
1570
|
}
|
|
1501
1571
|
dumpResponse(apiReq, payload, status) {
|
|
1502
|
-
const
|
|
1572
|
+
const rawUrl = apiReq.req.originalUrl || apiReq.req.url;
|
|
1573
|
+
const tokenParam = this.config.tokenParam.trim();
|
|
1574
|
+
let url = rawUrl;
|
|
1575
|
+
if (tokenParam && rawUrl) {
|
|
1576
|
+
try {
|
|
1577
|
+
const parsed = new URL(rawUrl, 'http://x');
|
|
1578
|
+
if (parsed.searchParams.has(tokenParam)) {
|
|
1579
|
+
parsed.searchParams.set(tokenParam, '[REDACTED]');
|
|
1580
|
+
url = parsed.pathname + '?' + parsed.searchParams.toString();
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
catch {
|
|
1584
|
+
// Leave url unmodified if parsing fails
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1503
1587
|
console.log('--- Outgoing Response! ---');
|
|
1504
1588
|
console.log('URL:', url);
|
|
1505
1589
|
console.log('Status:', status);
|