@rodit/rodit-auth-be 9.16.0 → 9.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +9 -5
- package/index.js +104 -9
- package/lib/auth/authentication.js +45 -10
- package/lib/auth/login-nonce.js +164 -0
- package/lib/auth/tokenservice.js +110 -32
- package/lib/blockchain/blockchainservice.js +190 -25
- package/lib/middleware/authenticationmw.js +298 -16
- package/lib/middleware/webhookhandlermw.js +25 -7
- package/package.json +1 -1
- package/services/configsdk.js +5 -3
|
@@ -24,6 +24,11 @@ const {
|
|
|
24
24
|
verify_peer_rodit,
|
|
25
25
|
verify_rodit_ownership_withnep413
|
|
26
26
|
} = require("../auth/authentication");
|
|
27
|
+
const {
|
|
28
|
+
normalizeOptionalLoginNonce,
|
|
29
|
+
buildLoginSigningMessageBytes,
|
|
30
|
+
createLoginTimestampChallenge,
|
|
31
|
+
} = require("../auth/login-nonce");
|
|
27
32
|
const {
|
|
28
33
|
nearorg_rpc_tokenfromroditid
|
|
29
34
|
} = require("../blockchain/blockchainservice");
|
|
@@ -163,7 +168,16 @@ function buildLoginUrl(apiendpoint, loginPath = "/api/login") {
|
|
|
163
168
|
async function resolveServerLoginTimestamp(apiendpoint, options = {}) {
|
|
164
169
|
const explicit = parseRequiredServerLoginTimestamp(options.timestamp);
|
|
165
170
|
if (explicit !== null) {
|
|
166
|
-
|
|
171
|
+
const nonceResult = normalizeOptionalLoginNonce(options.nonce);
|
|
172
|
+
if (nonceResult.errorCode) {
|
|
173
|
+
return {
|
|
174
|
+
timestamp: null,
|
|
175
|
+
nonce: null,
|
|
176
|
+
errorCode: nonceResult.errorCode,
|
|
177
|
+
error: nonceResult.error,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return { timestamp: explicit, nonce: nonceResult.nonce };
|
|
167
181
|
}
|
|
168
182
|
|
|
169
183
|
const timestampPath =
|
|
@@ -183,6 +197,7 @@ async function resolveServerLoginTimestamp(apiendpoint, options = {}) {
|
|
|
183
197
|
if (!response.ok) {
|
|
184
198
|
return {
|
|
185
199
|
timestamp: null,
|
|
200
|
+
nonce: null,
|
|
186
201
|
errorCode: "LOGIN_TIMESTAMP_FETCH_FAILED",
|
|
187
202
|
error: `Failed to fetch login timestamp challenge: HTTP ${response.status}`,
|
|
188
203
|
};
|
|
@@ -193,15 +208,29 @@ async function resolveServerLoginTimestamp(apiendpoint, options = {}) {
|
|
|
193
208
|
if (parsed === null) {
|
|
194
209
|
return {
|
|
195
210
|
timestamp: null,
|
|
211
|
+
nonce: null,
|
|
196
212
|
errorCode: "INVALID_LOGIN_TIMESTAMP",
|
|
197
213
|
error: "Login timestamp challenge response is missing a valid timestamp",
|
|
198
214
|
};
|
|
199
215
|
}
|
|
200
216
|
|
|
201
|
-
|
|
217
|
+
const nonceResult = normalizeOptionalLoginNonce(
|
|
218
|
+
options.nonce !== undefined ? options.nonce : data?.nonce
|
|
219
|
+
);
|
|
220
|
+
if (nonceResult.errorCode) {
|
|
221
|
+
return {
|
|
222
|
+
timestamp: null,
|
|
223
|
+
nonce: null,
|
|
224
|
+
errorCode: nonceResult.errorCode,
|
|
225
|
+
error: nonceResult.error,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { timestamp: parsed, nonce: nonceResult.nonce };
|
|
202
230
|
} catch (error) {
|
|
203
231
|
return {
|
|
204
232
|
timestamp: null,
|
|
233
|
+
nonce: null,
|
|
205
234
|
errorCode: "LOGIN_TIMESTAMP_FETCH_FAILED",
|
|
206
235
|
error: `Failed to fetch login timestamp challenge: ${error.message}`,
|
|
207
236
|
};
|
|
@@ -302,6 +331,30 @@ async function login_client(req, res) {
|
|
|
302
331
|
const hasAccountId = accountid.length > 0;
|
|
303
332
|
const peer_timestamp = parseRequiredLoginTimestamp(body.timestamp);
|
|
304
333
|
const base64url_signature = extractLoginBase64UrlSignature(body);
|
|
334
|
+
const nonceResult = normalizeOptionalLoginNonce(body.nonce);
|
|
335
|
+
if (nonceResult.errorCode) {
|
|
336
|
+
const duration = Date.now() - startTime;
|
|
337
|
+
logger.metric("login_attempt_duration_ms", duration, {
|
|
338
|
+
component: "RoditAuth",
|
|
339
|
+
success: false,
|
|
340
|
+
result: "failure",
|
|
341
|
+
reason: "invalid_login_nonce",
|
|
342
|
+
error: nonceResult.errorCode,
|
|
343
|
+
});
|
|
344
|
+
logger.metric("failed_login_attempts_total", 1, {
|
|
345
|
+
component: "RoditAuth",
|
|
346
|
+
result: "failure",
|
|
347
|
+
reason: nonceResult.errorCode,
|
|
348
|
+
});
|
|
349
|
+
return respondLoginValidationFailure(res, {
|
|
350
|
+
silenceLoginFailures,
|
|
351
|
+
statusCode: 400,
|
|
352
|
+
requestId,
|
|
353
|
+
code: nonceResult.errorCode,
|
|
354
|
+
message: nonceResult.error,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const peer_nonce = nonceResult.nonce;
|
|
305
358
|
|
|
306
359
|
logger.infoWithContext("Login request identifiers (sanitized)", {
|
|
307
360
|
...baseContext,
|
|
@@ -309,6 +362,7 @@ async function login_client(req, res) {
|
|
|
309
362
|
accountid: accountid || undefined,
|
|
310
363
|
login_mode: hasRoditId ? "roditid" : hasAccountId ? "accountid" : "none",
|
|
311
364
|
timestamp: peer_timestamp,
|
|
365
|
+
has_nonce: !!peer_nonce,
|
|
312
366
|
has_base64url_signature: base64url_signature.length > 0,
|
|
313
367
|
});
|
|
314
368
|
|
|
@@ -449,6 +503,7 @@ async function login_client(req, res) {
|
|
|
449
503
|
hasRoditId: roditid.length > 0,
|
|
450
504
|
hasAccountId: accountid.length > 0,
|
|
451
505
|
hasTimestamp: peer_timestamp !== undefined && peer_timestamp !== null,
|
|
506
|
+
hasNonce: !!peer_nonce,
|
|
452
507
|
has_base64url_signature: base64url_signature.length > 0,
|
|
453
508
|
});
|
|
454
509
|
|
|
@@ -503,7 +558,8 @@ async function login_client(req, res) {
|
|
|
503
558
|
await resolve_peer_rodit_for_login(roditid, accountid),
|
|
504
559
|
peer_roditid,
|
|
505
560
|
peer_timestamp,
|
|
506
|
-
base64url_signature
|
|
561
|
+
base64url_signature,
|
|
562
|
+
peer_nonce
|
|
507
563
|
);
|
|
508
564
|
|
|
509
565
|
const { peer_rodit, goodrodit: isRoditValid, failureReason, failureMessage } = result;
|
|
@@ -1287,6 +1343,191 @@ async function login_client(req, res) {
|
|
|
1287
1343
|
}
|
|
1288
1344
|
}
|
|
1289
1345
|
|
|
1346
|
+
/**
|
|
1347
|
+
* Optional client-initiated access-credential refresh.
|
|
1348
|
+
* Not required for default SERVER-INITIATED deployments: the server renews
|
|
1349
|
+
* access JWTs on authenticated API calls and returns them via New-Token.
|
|
1350
|
+
* Mount POST /api/refresh only for idle-client timers or CLIENT-INITIATED mode.
|
|
1351
|
+
*
|
|
1352
|
+
* @param {Object} req - Express request object
|
|
1353
|
+
* @param {Object} res - Express response object
|
|
1354
|
+
* @returns {Object} Response object
|
|
1355
|
+
*/
|
|
1356
|
+
async function refresh_client(req, res) {
|
|
1357
|
+
const requestId = ulid();
|
|
1358
|
+
const startTime = Date.now();
|
|
1359
|
+
const baseContext = createLogContext(
|
|
1360
|
+
"AuthenticationService",
|
|
1361
|
+
"refresh_client",
|
|
1362
|
+
{
|
|
1363
|
+
requestId,
|
|
1364
|
+
path: req.path,
|
|
1365
|
+
method: req.method,
|
|
1366
|
+
ip: req.ip,
|
|
1367
|
+
}
|
|
1368
|
+
);
|
|
1369
|
+
|
|
1370
|
+
logger.infoWithContext("Token refresh request received", {
|
|
1371
|
+
...baseContext,
|
|
1372
|
+
userAgent: req.get("User-Agent"),
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
try {
|
|
1376
|
+
verifySessionManager();
|
|
1377
|
+
|
|
1378
|
+
const jwt_token =
|
|
1379
|
+
req.headers.authorization &&
|
|
1380
|
+
req.headers.authorization.startsWith("Bearer ")
|
|
1381
|
+
? req.headers.authorization.substring(7)
|
|
1382
|
+
: null;
|
|
1383
|
+
|
|
1384
|
+
if (!jwt_token) {
|
|
1385
|
+
logger.metric &&
|
|
1386
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1387
|
+
component: "AuthenticationService",
|
|
1388
|
+
result: "no_jwt_token",
|
|
1389
|
+
});
|
|
1390
|
+
return sendError(res, {
|
|
1391
|
+
statusCode: 401,
|
|
1392
|
+
requestId,
|
|
1393
|
+
code: "MISSING_TOKEN",
|
|
1394
|
+
message: "No authentication jwt_token provided",
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
const isTokenInvalid = await sessionManager.isTokenInvalidated(jwt_token);
|
|
1399
|
+
if (isTokenInvalid) {
|
|
1400
|
+
const invalidationInfo =
|
|
1401
|
+
await sessionManager.getTokenInvalidationInfo(jwt_token);
|
|
1402
|
+
logger.metric &&
|
|
1403
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1404
|
+
component: "AuthenticationService",
|
|
1405
|
+
result: "invalidated_token",
|
|
1406
|
+
});
|
|
1407
|
+
return sendError(res, {
|
|
1408
|
+
statusCode: 401,
|
|
1409
|
+
requestId,
|
|
1410
|
+
code: "INVALIDATED_TOKEN",
|
|
1411
|
+
message: "Token has been invalidated",
|
|
1412
|
+
details: {
|
|
1413
|
+
reason: invalidationInfo?.reason || "session_inactive",
|
|
1414
|
+
invalidatedAt: invalidationInfo?.timestamp,
|
|
1415
|
+
},
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
const config_own_rodit = await stateManager.getConfigOwnRodit();
|
|
1420
|
+
if (!config_own_rodit || !config_own_rodit.own_rodit) {
|
|
1421
|
+
return sendError(res, {
|
|
1422
|
+
statusCode: 500,
|
|
1423
|
+
requestId,
|
|
1424
|
+
code: "SERVER_CONFIG_ERROR",
|
|
1425
|
+
message: "Server configuration not initialized",
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
let validationResult;
|
|
1430
|
+
try {
|
|
1431
|
+
validationResult = await validate_jwt_token_be(
|
|
1432
|
+
jwt_token,
|
|
1433
|
+
config_own_rodit.own_rodit,
|
|
1434
|
+
{
|
|
1435
|
+
allowExpiredToken: true,
|
|
1436
|
+
forceCredentialRenewal: true,
|
|
1437
|
+
}
|
|
1438
|
+
);
|
|
1439
|
+
} catch (validationError) {
|
|
1440
|
+
const message = validationError.message || "Token refresh failed";
|
|
1441
|
+
const sessionClosed =
|
|
1442
|
+
/Error 012/i.test(message) ||
|
|
1443
|
+
/Session is not active/i.test(message) ||
|
|
1444
|
+
validationError.renewalReason === "session_closed";
|
|
1445
|
+
logger.metric &&
|
|
1446
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1447
|
+
component: "AuthenticationService",
|
|
1448
|
+
result: sessionClosed ? "session_closed" : "validation_failed",
|
|
1449
|
+
});
|
|
1450
|
+
return sendError(res, {
|
|
1451
|
+
statusCode: sessionClosed ? 401 : 403,
|
|
1452
|
+
requestId,
|
|
1453
|
+
code: sessionClosed
|
|
1454
|
+
? "INVALIDATED_TOKEN"
|
|
1455
|
+
: validationError.code || "REFRESH_FAILED",
|
|
1456
|
+
message,
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const newToken = validationResult?.newToken;
|
|
1461
|
+
if (!newToken) {
|
|
1462
|
+
logger.metric &&
|
|
1463
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1464
|
+
component: "AuthenticationService",
|
|
1465
|
+
result: "no_new_token",
|
|
1466
|
+
});
|
|
1467
|
+
return sendError(res, {
|
|
1468
|
+
statusCode: 403,
|
|
1469
|
+
requestId,
|
|
1470
|
+
code: "REFRESH_FAILED",
|
|
1471
|
+
message: "Token refresh did not produce a new credential",
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
if (typeof res.setHeader === "function") {
|
|
1476
|
+
res.setHeader("New-Token", newToken);
|
|
1477
|
+
} else if (typeof res.set === "function") {
|
|
1478
|
+
res.set("New-Token", newToken);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const duration = Date.now() - startTime;
|
|
1482
|
+
logger.infoWithContext("Token refresh completed", {
|
|
1483
|
+
...baseContext,
|
|
1484
|
+
duration,
|
|
1485
|
+
success: true,
|
|
1486
|
+
});
|
|
1487
|
+
logger.metric &&
|
|
1488
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1489
|
+
component: "AuthenticationService",
|
|
1490
|
+
result: "success",
|
|
1491
|
+
});
|
|
1492
|
+
logger.metric &&
|
|
1493
|
+
logger.metric("token_refresh_duration_ms", duration, {
|
|
1494
|
+
component: "AuthenticationService",
|
|
1495
|
+
success: true,
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
return res.json({
|
|
1499
|
+
token: newToken,
|
|
1500
|
+
jwt_token: newToken,
|
|
1501
|
+
message: "Token refreshed",
|
|
1502
|
+
requestId,
|
|
1503
|
+
});
|
|
1504
|
+
} catch (error) {
|
|
1505
|
+
const duration = Date.now() - startTime;
|
|
1506
|
+
logErrorWithMetrics(
|
|
1507
|
+
"Token refresh process failed",
|
|
1508
|
+
{
|
|
1509
|
+
...baseContext,
|
|
1510
|
+
duration,
|
|
1511
|
+
},
|
|
1512
|
+
error,
|
|
1513
|
+
"refresh_error",
|
|
1514
|
+
{ error_type: "general_refresh_error" }
|
|
1515
|
+
);
|
|
1516
|
+
logger.metric &&
|
|
1517
|
+
logger.metric("token_refresh_attempts", 1, {
|
|
1518
|
+
component: "AuthenticationService",
|
|
1519
|
+
result: "error",
|
|
1520
|
+
});
|
|
1521
|
+
return sendError(res, {
|
|
1522
|
+
statusCode: 500,
|
|
1523
|
+
requestId,
|
|
1524
|
+
code: "REFRESH_ERROR",
|
|
1525
|
+
message: "Internal server error during token refresh",
|
|
1526
|
+
details: !isStrictEnvironment() ? { error: error.message } : undefined,
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1290
1531
|
/**
|
|
1291
1532
|
* Handle client login with NEP-413 standard
|
|
1292
1533
|
*
|
|
@@ -1624,6 +1865,16 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
1624
1865
|
requestId
|
|
1625
1866
|
};
|
|
1626
1867
|
}
|
|
1868
|
+
const nonceResult = normalizeOptionalLoginNonce(options.nonce);
|
|
1869
|
+
if (nonceResult.errorCode) {
|
|
1870
|
+
return {
|
|
1871
|
+
error: nonceResult.error,
|
|
1872
|
+
errorCode: nonceResult.errorCode,
|
|
1873
|
+
failureReason: nonceResult.errorCode,
|
|
1874
|
+
requestId
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
const nonce = nonceResult.nonce;
|
|
1627
1878
|
|
|
1628
1879
|
const hasRoditId = roditid.length > 0;
|
|
1629
1880
|
const hasAccountId = accountid.length > 0;
|
|
@@ -1638,8 +1889,10 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
1638
1889
|
|
|
1639
1890
|
const timeString = await unixTimeToDateString(timestamp);
|
|
1640
1891
|
const signatureIdentifier = hasRoditId ? roditid : accountid;
|
|
1641
|
-
const signatureIdentifierandtimestamp =
|
|
1642
|
-
signatureIdentifier
|
|
1892
|
+
const signatureIdentifierandtimestamp = buildLoginSigningMessageBytes(
|
|
1893
|
+
signatureIdentifier,
|
|
1894
|
+
timeString,
|
|
1895
|
+
nonce
|
|
1643
1896
|
);
|
|
1644
1897
|
|
|
1645
1898
|
logger.debug("Generating authentication signature", {
|
|
@@ -1649,6 +1902,7 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
1649
1902
|
roditId: roditid,
|
|
1650
1903
|
accountId: accountid,
|
|
1651
1904
|
timestamp,
|
|
1905
|
+
hasNonce: !!nonce,
|
|
1652
1906
|
});
|
|
1653
1907
|
|
|
1654
1908
|
// Create signature
|
|
@@ -1674,17 +1928,21 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
1674
1928
|
});
|
|
1675
1929
|
|
|
1676
1930
|
try {
|
|
1931
|
+
const requestBody = {
|
|
1932
|
+
...(hasRoditId ? { roditid } : {}),
|
|
1933
|
+
...(hasAccountId ? { accountid } : {}),
|
|
1934
|
+
timestamp,
|
|
1935
|
+
roditid_base64url_signature,
|
|
1936
|
+
};
|
|
1937
|
+
if (nonce) {
|
|
1938
|
+
requestBody.nonce = nonce;
|
|
1939
|
+
}
|
|
1677
1940
|
const response = await fetch(fetchUrl, {
|
|
1678
1941
|
method: "POST",
|
|
1679
1942
|
headers: {
|
|
1680
1943
|
"Content-Type": "application/json",
|
|
1681
1944
|
},
|
|
1682
|
-
body: JSON.stringify(
|
|
1683
|
-
...(hasRoditId ? { roditid } : {}),
|
|
1684
|
-
...(hasAccountId ? { accountid } : {}),
|
|
1685
|
-
timestamp,
|
|
1686
|
-
roditid_base64url_signature,
|
|
1687
|
-
}),
|
|
1945
|
+
body: JSON.stringify(requestBody),
|
|
1688
1946
|
});
|
|
1689
1947
|
|
|
1690
1948
|
if (!response.ok) {
|
|
@@ -1972,8 +2230,12 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
1972
2230
|
});
|
|
1973
2231
|
|
|
1974
2232
|
const roditid = normalizeOptionalLoginString(own_rodit?.token_id);
|
|
1975
|
-
const {
|
|
1976
|
-
|
|
2233
|
+
const {
|
|
2234
|
+
timestamp,
|
|
2235
|
+
nonce,
|
|
2236
|
+
error: timestampError,
|
|
2237
|
+
errorCode: timestampErrorCode,
|
|
2238
|
+
} = await resolveServerLoginTimestamp(apiendpoint, options);
|
|
1977
2239
|
const accountid = normalizeOptionalServerAccountId(options.accountId);
|
|
1978
2240
|
|
|
1979
2241
|
if (timestamp === null) {
|
|
@@ -2005,13 +2267,16 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
2005
2267
|
roditId: roditid,
|
|
2006
2268
|
accountId: accountid,
|
|
2007
2269
|
timestamp,
|
|
2270
|
+
hasNonce: !!nonce,
|
|
2008
2271
|
});
|
|
2009
2272
|
|
|
2010
2273
|
const timeString = await unixTimeToDateString(timestamp);
|
|
2011
2274
|
|
|
2012
2275
|
const signatureIdentifier = hasRoditId ? roditid : accountid;
|
|
2013
|
-
const signatureIdentifierandtimestamp =
|
|
2014
|
-
signatureIdentifier
|
|
2276
|
+
const signatureIdentifierandtimestamp = buildLoginSigningMessageBytes(
|
|
2277
|
+
signatureIdentifier,
|
|
2278
|
+
timeString,
|
|
2279
|
+
nonce
|
|
2015
2280
|
);
|
|
2016
2281
|
|
|
2017
2282
|
logger.debug("Generating signature", {
|
|
@@ -2020,6 +2285,7 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
2020
2285
|
requestId,
|
|
2021
2286
|
hasPrivateKey: !!config_own_rodit.own_rodit_bytes_private_key,
|
|
2022
2287
|
signatureIdentifier,
|
|
2288
|
+
hasNonce: !!nonce,
|
|
2023
2289
|
});
|
|
2024
2290
|
|
|
2025
2291
|
const own_rodit_bytes_signature = nacl.sign.detached(
|
|
@@ -2044,6 +2310,10 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
2044
2310
|
requestBody.accountid = accountid;
|
|
2045
2311
|
}
|
|
2046
2312
|
|
|
2313
|
+
if (nonce) {
|
|
2314
|
+
requestBody.nonce = nonce;
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2047
2317
|
logger.debug("Sending login request", {
|
|
2048
2318
|
component: "AuthenticationService",
|
|
2049
2319
|
method,
|
|
@@ -2051,6 +2321,7 @@ async function login_portal(config_own_rodit, port, options = {}) {
|
|
|
2051
2321
|
roditid,
|
|
2052
2322
|
accountId: accountid,
|
|
2053
2323
|
timestamp,
|
|
2324
|
+
hasNonce: !!nonce,
|
|
2054
2325
|
signatureLength: roditid_base64url_signature?.length,
|
|
2055
2326
|
apiEndpoint: loginUrl,
|
|
2056
2327
|
});
|
|
@@ -2379,4 +2650,15 @@ async function logout_server(jwt_token) {
|
|
|
2379
2650
|
|
|
2380
2651
|
|
|
2381
2652
|
// Export the class directly (will be instantiated in rodit.js)
|
|
2382
|
-
module.exports = {
|
|
2653
|
+
module.exports = {
|
|
2654
|
+
authenticate_apicall,
|
|
2655
|
+
authenticate_logout,
|
|
2656
|
+
login_server,
|
|
2657
|
+
login_portal,
|
|
2658
|
+
login_client,
|
|
2659
|
+
login_client_withnep413,
|
|
2660
|
+
logout_client,
|
|
2661
|
+
logout_server,
|
|
2662
|
+
refresh_client,
|
|
2663
|
+
createLoginTimestampChallenge,
|
|
2664
|
+
};
|
|
@@ -121,6 +121,27 @@ function validateOutboundWebhookUrl(raw) {
|
|
|
121
121
|
return parsed;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Build the absolute outbound webhook URL.
|
|
126
|
+
* If `webhookUrl` is host-only (`host` or `host:port`), append `endpoint`.
|
|
127
|
+
* If it already includes a path, use that path as-is (do not append).
|
|
128
|
+
*
|
|
129
|
+
* @param {string} webhookUrl Peer `rodit_webhookurl` / `webhook_url` (scheme optional)
|
|
130
|
+
* @param {string} [endpoint='/webhook'] Path to append when none is present
|
|
131
|
+
* @returns {string} Absolute https URL
|
|
132
|
+
*/
|
|
133
|
+
function formatOutboundWebhookTargetUrl(webhookUrl, endpoint = "/webhook") {
|
|
134
|
+
const cleanWebhookUrl = String(webhookUrl || "")
|
|
135
|
+
.replace(/^(https?:\/\/)/i, "")
|
|
136
|
+
.replace(/\/+$/, "");
|
|
137
|
+
const hasPath = cleanWebhookUrl.includes("/");
|
|
138
|
+
if (hasPath) {
|
|
139
|
+
return `https://${cleanWebhookUrl}`;
|
|
140
|
+
}
|
|
141
|
+
const normalizedEndpoint = `/${String(endpoint || "/webhook").replace(/^\/+/, "")}`;
|
|
142
|
+
return `https://${cleanWebhookUrl}${normalizedEndpoint}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
function ipv4ToInt(ip) {
|
|
125
146
|
return ip.split(".").reduce((acc, oct) => ((acc << 8) + Number(oct)) >>> 0, 0) >>> 0;
|
|
126
147
|
}
|
|
@@ -1208,13 +1229,9 @@ async function resolveOutboundWebhookSessionId(options = {}, req = null) {
|
|
|
1208
1229
|
endpoint
|
|
1209
1230
|
});
|
|
1210
1231
|
|
|
1211
|
-
//
|
|
1212
|
-
//
|
|
1213
|
-
const
|
|
1214
|
-
.replace(/^(https?:\/\/)/, "")
|
|
1215
|
-
.replace(/\/+$/, "");
|
|
1216
|
-
const normalizedEndpoint = `/${String(endpoint || "/webhook").replace(/^\/+/, "")}`;
|
|
1217
|
-
const formattedWebhookUrl = `https://${cleanWebhookUrl}${normalizedEndpoint}`;
|
|
1232
|
+
// Append endpoint only when webhook_url has no path
|
|
1233
|
+
// (e.g. host:7443 → …/hooks/wake; host:7443/hooks/agent stays as-is).
|
|
1234
|
+
const formattedWebhookUrl = formatOutboundWebhookTargetUrl(webhookUrl, endpoint);
|
|
1218
1235
|
|
|
1219
1236
|
// Outbound SSRF controls: reject userinfo / private / metadata destinations,
|
|
1220
1237
|
// enforce peer webhook_cidr, and pin DNS for the request lifetime.
|
|
@@ -2101,6 +2118,7 @@ module.exports = {
|
|
|
2101
2118
|
|
|
2102
2119
|
// Outbound SSRF / CIDR / DNS-pin helpers
|
|
2103
2120
|
validateOutboundWebhookUrl,
|
|
2121
|
+
formatOutboundWebhookTargetUrl,
|
|
2104
2122
|
resolveAndPinWebhookDestination,
|
|
2105
2123
|
createPinnedDispatcher,
|
|
2106
2124
|
|
package/package.json
CHANGED
package/services/configsdk.js
CHANGED
|
@@ -229,10 +229,12 @@ const FALLBACK_DEFAULTS = {
|
|
|
229
229
|
LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY: "0.80",
|
|
230
230
|
THRESHOLD_VALIDATION_TYPE: "0.10",
|
|
231
231
|
DURATIONRAMP: "0.85",
|
|
232
|
-
//
|
|
232
|
+
// Token renewal initiator (default: server handles renewals).
|
|
233
233
|
// Options:
|
|
234
|
-
// - "SERVER-INITIATED":
|
|
235
|
-
// -
|
|
234
|
+
// - "SERVER-INITIATED" (default): renew access JWT during authenticate and
|
|
235
|
+
// return New-Token. Clients normally do not need POST /api/refresh.
|
|
236
|
+
// - "CLIENT-INITIATED" (optional/advanced): skip piggyback renewal; client
|
|
237
|
+
// must call the optional POST /api/refresh endpoint.
|
|
236
238
|
SERVERORCLIENT: "SERVER-INITIATED",
|
|
237
239
|
// Login error behavior.
|
|
238
240
|
// Options:
|