@rodit/rodit-auth-be 9.11.14 → 9.11.20

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.
@@ -25,9 +25,8 @@ logger.infoWithContext("TokenService using SessionManager instance", {
25
25
  timestamp: new Date().toISOString()
26
26
  });
27
27
  const stateManager = require('../blockchain/statemanager');
28
- const {
29
- nearorg_rpc_tokenfromroditid,
30
- nearorg_rpc_tokensfromaccountid,
28
+ const {
29
+ nearorg_rpc_tokenfromroditid,
31
30
  nearorg_rpc_fetchpublickeybytes,
32
31
  } = require("../blockchain/blockchainservice");
33
32
 
@@ -62,6 +61,15 @@ function isCanonicalBase64Url(value) {
62
61
  }
63
62
  }
64
63
 
64
+ /** Login peer RODiT token id embedded in JWT sub (`{sp};sub={peerTokenId}`). */
65
+ function extractLoginPeerRoditIdFromSub(sub) {
66
+ if (!sub || typeof sub !== "string") {
67
+ return "";
68
+ }
69
+ const subParts = sub.split(";sub=");
70
+ return subParts.length > 1 ? subParts[1] : "";
71
+ }
72
+
65
73
  function parseRoditJwtDurationSeconds(metadata) {
66
74
  const parsed = parseInt(metadata?.jwt_duration, 10);
67
75
  if (Number.isFinite(parsed) && parsed > 0) {
@@ -965,6 +973,31 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
965
973
  keyCreationDuration,
966
974
  });
967
975
 
976
+ const signatureStart = Date.now();
977
+ const timeString = await unixTimeToDateString(now);
978
+ const roditidandtimestamp = new TextEncoder().encode(
979
+ token.rodit_id + timeString
980
+ );
981
+ let privateKeyToUse = config_own_rodit.own_rodit_bytes_private_key;
982
+ if (Buffer.isBuffer(privateKeyToUse)) {
983
+ privateKeyToUse = new Uint8Array(privateKeyToUse);
984
+ } else if (!(privateKeyToUse instanceof Uint8Array)) {
985
+ privateKeyToUse = new Uint8Array(Array.from(privateKeyToUse));
986
+ }
987
+ const own_rodit_bytes_signature = nacl.sign.detached(
988
+ roditidandtimestamp,
989
+ privateKeyToUse
990
+ );
991
+ const renewed_rodit_idsignature = Buffer.from(
992
+ own_rodit_bytes_signature
993
+ ).toString("base64url");
994
+ logger.debug("Regenerated rodit_idsignature for renewed credential", {
995
+ requestId,
996
+ signatureDuration: Date.now() - signatureStart,
997
+ roditId: token.rodit_id,
998
+ iat: now,
999
+ });
1000
+
968
1001
  // Keep existing session ID and creation time
969
1002
  const session_id = existingSessionId;
970
1003
  const session_iat = token.session_iat;
@@ -1020,7 +1053,7 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
1020
1053
  rodit_id: token.rodit_id,
1021
1054
  rodit_owner: token.rodit_owner,
1022
1055
  rodit_allowediso3166list: token.rodit_allowediso3166list,
1023
- rodit_idsignature: token.rodit_idsignature,
1056
+ rodit_idsignature: renewed_rodit_idsignature,
1024
1057
  rodit_maxrequests: token.rodit_maxrequests,
1025
1058
  rodit_maxrqwindow: token.rodit_maxrqwindow,
1026
1059
  rodit_permissionedroutes: token.rodit_permissionedroutes,
@@ -1657,14 +1690,35 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
1657
1690
  newToken = renewalResult.newToken;
1658
1691
 
1659
1692
  if (isExpired && !newToken) {
1660
- logger.error("Token expired and renewal failed", {
1661
- component: "JwtAuth",
1662
- method: "validate_jwt_token_be",
1663
- requestId,
1664
- jti: payload.jti
1665
- });
1666
-
1667
- throw new Error("Error 007: Token has expired and renewal failed");
1693
+ const renewalNow = Math.floor(Date.now() / 1000);
1694
+ const sessionExpUnix =
1695
+ payload.session_exp != null ? Number(payload.session_exp) : null;
1696
+ const sessionStillActive =
1697
+ Number.isFinite(sessionExpUnix) && sessionExpUnix > renewalNow;
1698
+
1699
+ if (sessionStillActive) {
1700
+ logger.warn(
1701
+ "Credential expired and renewal failed but session still active; allowing request",
1702
+ {
1703
+ component: "JwtAuth",
1704
+ method: "validate_jwt_token_be",
1705
+ requestId,
1706
+ jti: payload.jti,
1707
+ sessionExp: sessionExpUnix,
1708
+ now: renewalNow,
1709
+ }
1710
+ );
1711
+ } else {
1712
+ logger.error("Token expired and renewal failed", {
1713
+ component: "JwtAuth",
1714
+ method: "validate_jwt_token_be",
1715
+ requestId,
1716
+ jti: payload.jti,
1717
+ sessionExp: sessionExpUnix,
1718
+ });
1719
+
1720
+ throw new Error("Error 007: Token has expired and renewal failed");
1721
+ }
1668
1722
  }
1669
1723
  } else if (isExpired) {
1670
1724
  logger.info("Allowing signature-valid expired token for special flow", {
@@ -1727,20 +1781,30 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
1727
1781
  const startTime = Date.now();
1728
1782
 
1729
1783
  try {
1784
+ const extractedSub = extractLoginPeerRoditIdFromSub(token.sub);
1785
+ if (!extractedSub) {
1786
+ const duration = Date.now() - startTime;
1787
+ logger.warn("Brief token validation failed - missing login peer in sub", {
1788
+ component: "JwtAuth",
1789
+ method: "brief_validate_jwt_token_be",
1790
+ requestId,
1791
+ duration,
1792
+ tokenJti: token.jti,
1793
+ tokenSub: token.sub,
1794
+ });
1795
+ logger.metric("jwt_brief_validation", duration, {
1796
+ result: "failure",
1797
+ token_jti: token.jti || "unknown",
1798
+ id_match: "false",
1799
+ });
1800
+ return { isValid: false, notAfter: null };
1801
+ }
1802
+
1730
1803
  const tokenFetchStart = Date.now();
1731
- const peer_rodit =
1732
- await nearorg_rpc_tokensfromaccountid(
1733
-
1734
- token.aud
1735
- );
1804
+ const peer_rodit = await nearorg_rpc_tokenfromroditid(extractedSub);
1736
1805
  const tokenFetchDuration = Date.now() - tokenFetchStart;
1737
1806
 
1738
- const subParts = token.sub.split(";sub=");
1739
- const extractedSub = subParts.length > 1 ? subParts[1] : "";
1740
-
1741
- const isValid =
1742
- peer_rodit.token_id === extractedSub &&
1743
- peer_rodit.owner_id === token.aud;
1807
+ const isValid = !!peer_rodit?.token_id && peer_rodit.token_id === extractedSub;
1744
1808
 
1745
1809
  const totalDuration = Date.now() - startTime;
1746
1810
 
@@ -1753,6 +1817,7 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
1753
1817
  tokenFetchDuration,
1754
1818
  tokenJti: token.jti,
1755
1819
  peerRoditId: peer_rodit.token_id,
1820
+ extractedSub,
1756
1821
  notAfter: peer_rodit.metadata.not_after,
1757
1822
  });
1758
1823
 
@@ -1769,26 +1834,23 @@ function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
1769
1834
  duration: totalDuration,
1770
1835
  tokenFetchDuration,
1771
1836
  tokenJti: token.jti,
1772
- peerRoditId: peer_rodit.token_id,
1837
+ peerRoditId: peer_rodit?.token_id || null,
1773
1838
  extractedSub,
1774
1839
  tokenAud: token.aud,
1775
- peerRoditOwnerId: peer_rodit.owner_id,
1776
- idMatch: peer_rodit.token_id === extractedSub,
1777
- ownerMatch: peer_rodit.owner_id === token.aud,
1840
+ idMatch: peer_rodit?.token_id === extractedSub,
1778
1841
  });
1779
1842
 
1780
1843
  // Add metrics for failed brief validations
1781
1844
  logger.metric("jwt_brief_validation", totalDuration, {
1782
1845
  result: "failure",
1783
1846
  token_jti: token.jti || "unknown",
1784
- id_match: peer_rodit.token_id === extractedSub ? "true" : "false",
1785
- owner_match: peer_rodit.owner_id === token.aud ? "true" : "false",
1847
+ id_match: peer_rodit?.token_id === extractedSub ? "true" : "false",
1786
1848
  });
1787
1849
  }
1788
1850
 
1789
1851
  return {
1790
1852
  isValid,
1791
- notAfter: peer_rodit.metadata.not_after,
1853
+ notAfter: peer_rodit?.metadata?.not_after ?? null,
1792
1854
  };
1793
1855
  } catch (error) {
1794
1856
  const duration = Date.now() - startTime;
@@ -1831,14 +1893,32 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
1831
1893
  const startTime = performance.now(); // More precise timing measurement
1832
1894
 
1833
1895
  try {
1896
+ const extractedSub = extractLoginPeerRoditIdFromSub(token.sub);
1897
+ if (!extractedSub) {
1898
+ logger.warn("Thorough token validation failed - missing login peer in sub", {
1899
+ component: "JwtAuth",
1900
+ method: "thorough_validate_jwt_token_be",
1901
+ requestId,
1902
+ duration: performance.now() - startTime,
1903
+ tokenJti: token?.jti,
1904
+ tokenSub: token?.sub,
1905
+ });
1906
+ logger.metric &&
1907
+ logger.metric("jwt_thorough_validation", performance.now() - startTime, {
1908
+ result: "missing_login_peer_sub",
1909
+ token_jti: token.jti || "unknown",
1910
+ });
1911
+ return { isValid: false, notAfter: null };
1912
+ }
1913
+
1834
1914
  // Fetch configuration with better timing measurements
1835
1915
  const configStart = performance.now();
1836
1916
  const config_own_rodit = await stateManager.getConfigOwnRodit();
1837
1917
  const configDuration = performance.now() - configStart;
1838
1918
 
1839
- // Fetch peer RODiT with clearer logging
1919
+ // Fetch login peer RODiT (client identity from sub), not server passport (rodit_id claim)
1840
1920
  const tokenFetchStart = performance.now();
1841
- const peer_rodit = await nearorg_rpc_tokenfromroditid(token.rodit_id);
1921
+ const peer_rodit = await nearorg_rpc_tokenfromroditid(extractedSub);
1842
1922
  const tokenFetchDuration = performance.now() - tokenFetchStart;
1843
1923
 
1844
1924
  if (!peer_rodit) {
@@ -1846,7 +1926,7 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
1846
1926
  component: "JwtAuth",
1847
1927
  requestId,
1848
1928
  duration: performance.now() - startTime,
1849
- tokenRoditId: token?.rodit_id,
1929
+ loginPeerRoditId: extractedSub,
1850
1930
  });
1851
1931
 
1852
1932
  // Add metrics for failed token fetch
@@ -2075,11 +2155,7 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2075
2155
  };
2076
2156
  }
2077
2157
 
2078
- // Extract subject and perform final validation
2079
- const subParts = token.sub.split(";sub=");
2080
- const extractedSub = subParts.length > 1 ? subParts[1] : "";
2081
-
2082
- logger.debug("Extracted subject from token", {
2158
+ logger.debug("Login peer identity check", {
2083
2159
  requestId,
2084
2160
  extractedSub,
2085
2161
  tokenSub: token.sub,
@@ -2088,10 +2164,8 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2088
2164
  tokenAud: token.aud,
2089
2165
  });
2090
2166
 
2091
- // Additional identity checks
2092
2167
  const idMatch = peer_rodit.token_id === extractedSub;
2093
- const ownerMatch = peer_rodit.owner_id === token.aud;
2094
- const isValid = idMatch && ownerMatch;
2168
+ const isValid = idMatch;
2095
2169
 
2096
2170
  const totalDuration = performance.now() - startTime;
2097
2171
 
@@ -2114,10 +2188,6 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2114
2188
  peer_rodit_id: peer_rodit.token_id,
2115
2189
  });
2116
2190
  } else {
2117
- const failedIdentityChecks = [];
2118
- if (!idMatch) failedIdentityChecks.push("token_id_mismatch");
2119
- if (!ownerMatch) failedIdentityChecks.push("owner_id_mismatch");
2120
-
2121
2191
  logger.warn("Token identity verification failed", {
2122
2192
  component: "JwtAuth",
2123
2193
  method: "thorough_validate_jwt_token_be",
@@ -2129,8 +2199,7 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2129
2199
  tokenAud: token.aud,
2130
2200
  peerRoditOwnerId: peer_rodit.owner_id,
2131
2201
  idMatch,
2132
- ownerMatch,
2133
- failedIdentityChecks,
2202
+ failedIdentityChecks: ["token_id_mismatch"],
2134
2203
  });
2135
2204
 
2136
2205
  // Add metrics for identity mismatch with more details
@@ -2138,19 +2207,22 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2138
2207
  logger.metric("jwt_thorough_validation", totalDuration, {
2139
2208
  result: "identity_mismatch",
2140
2209
  token_jti: token.jti || "unknown",
2141
- id_match: idMatch ? "true" : "false",
2142
- owner_match: ownerMatch ? "true" : "false",
2143
- failed_checks: failedIdentityChecks.join(","),
2210
+ id_match: "false",
2211
+ failed_checks: "token_id_mismatch",
2144
2212
  peer_rodit_id: peer_rodit.token_id,
2145
2213
  });
2146
2214
  }
2147
2215
 
2216
+ const failedIdentityChecks = !isValid ? ["token_id_mismatch"] : [];
2217
+
2148
2218
  return {
2149
2219
  isValid,
2150
2220
  notAfter: peer_rodit.metadata.not_after,
2151
2221
  error: !isValid ? "Token identity verification failed" : undefined,
2152
2222
  errorCode: !isValid ? "SERVER_TOKEN_IDENTITY_MISMATCH" : undefined,
2153
- errorMessage: !isValid ? `Server token identity mismatch: ${failedIdentityChecks.join(", ")}` : undefined
2223
+ errorMessage: !isValid
2224
+ ? `Server token identity mismatch: ${failedIdentityChecks.join(", ")}`
2225
+ : undefined
2154
2226
  };
2155
2227
  } catch (error) {
2156
2228
  const duration = performance.now() - startTime;
@@ -2187,6 +2259,70 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2187
2259
  }
2188
2260
  }
2189
2261
 
2262
+ /**
2263
+ * Decide brief vs thorough renewal verification using existing SECURITY_OPTIONS constants.
2264
+ *
2265
+ * @param {Object} params
2266
+ * @returns {Object} Plan with shouldDoFullVerification, urgency, pThorough, newduration, floors
2267
+ */
2268
+ function resolveRenewalVerificationPlan({
2269
+ forceRenewal = false,
2270
+ durationLeftpct = 0,
2271
+ currentDuration = 0,
2272
+ lapsedProportion = 0.8,
2273
+ thresholdValidationType = 0.1,
2274
+ durationRamp = 0.85,
2275
+ fallbackJwtDuration = 3600,
2276
+ roditMaxRqWindow,
2277
+ randomNumber = Math.random(),
2278
+ }) {
2279
+ const eligibilityTail = 1.0 - lapsedProportion;
2280
+ const safeCurrentDuration = Math.max(0, Number(currentDuration) || 0);
2281
+ const newduration = safeCurrentDuration * durationRamp;
2282
+
2283
+ let urgency;
2284
+ if (forceRenewal) {
2285
+ urgency = 1;
2286
+ } else if (eligibilityTail <= 0) {
2287
+ urgency = 1;
2288
+ } else {
2289
+ const tailFraction = durationLeftpct / 100 / eligibilityTail;
2290
+ urgency = Math.min(1, Math.max(0, 1 - tailFraction));
2291
+ }
2292
+
2293
+ const pThorough =
2294
+ thresholdValidationType + urgency * (1 - thresholdValidationType);
2295
+
2296
+ const baselineDuration = Math.max(1, Number(fallbackJwtDuration) || 3600);
2297
+ const rampedFloor = baselineDuration * eligibilityTail * durationRamp;
2298
+ const maxRqWindow = Number(roditMaxRqWindow) || baselineDuration;
2299
+ const rqWindowFloor = maxRqWindow * eligibilityTail;
2300
+
2301
+ const stochasticThorough = urgency >= 1 || randomNumber < pThorough;
2302
+ const rampedFloorThorough = newduration <= rampedFloor;
2303
+ const rqWindowFloorThorough = newduration <= rqWindowFloor;
2304
+ const deterministicThorough = rampedFloorThorough || rqWindowFloorThorough;
2305
+
2306
+ let verificationReason = "brief";
2307
+ if (deterministicThorough) {
2308
+ verificationReason = rampedFloorThorough ? "ramped_floor" : "rq_window_floor";
2309
+ } else if (stochasticThorough) {
2310
+ verificationReason = "stochastic";
2311
+ }
2312
+
2313
+ return {
2314
+ shouldDoFullVerification: stochasticThorough || deterministicThorough,
2315
+ urgency,
2316
+ pThorough,
2317
+ newduration,
2318
+ rampedFloor,
2319
+ rqWindowFloor,
2320
+ stochasticThorough,
2321
+ deterministicThorough,
2322
+ verificationReason,
2323
+ };
2324
+ }
2325
+
2190
2326
  /**
2191
2327
  * Check if a token needs renewal and renew if necessary
2192
2328
  *
@@ -2210,12 +2346,16 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2210
2346
  const DURATIONRAMP = parseFloat(
2211
2347
  config.get('SECURITY_OPTIONS.DURATIONRAMP', '0.85')
2212
2348
  );
2349
+ const FALLBACK_JWT_DURATION = parseInt(
2350
+ config.get('SECURITY_OPTIONS.FALLBACK_JWT_DURATION', '3600'),
2351
+ 10
2352
+ );
2213
2353
 
2214
2354
  const currentTime = Math.floor(Date.now() / 1000);
2215
2355
  const timeLeft = payload.exp - currentTime;
2216
2356
  const currentDuration = payload.exp - payload.iat;
2217
- const durationLeftpct = (timeLeft / currentDuration) * 100;
2218
- const newduration = currentDuration * DURATIONRAMP;
2357
+ const durationLeftpct =
2358
+ currentDuration > 0 ? (timeLeft / currentDuration) * 100 : 0;
2219
2359
 
2220
2360
  // Log session information
2221
2361
  const sessionInfo = {
@@ -2275,13 +2415,40 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2275
2415
  ...sessionInfo,
2276
2416
  });
2277
2417
 
2278
- // Determine verification method
2279
- const randomNumber = Math.random();
2280
- const shouldDoFullVerification =
2281
- randomNumber < THRESHOLD_VALIDATION_TYPE ||
2282
- newduration >
2283
- payload.rodit_maxrqwindow *
2284
- (100 - (LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY * 100));
2418
+ const renewalPlan = resolveRenewalVerificationPlan({
2419
+ forceRenewal,
2420
+ durationLeftpct,
2421
+ currentDuration,
2422
+ lapsedProportion: LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY,
2423
+ thresholdValidationType: THRESHOLD_VALIDATION_TYPE,
2424
+ durationRamp: DURATIONRAMP,
2425
+ fallbackJwtDuration: FALLBACK_JWT_DURATION,
2426
+ roditMaxRqWindow: payload.rodit_maxrqwindow,
2427
+ });
2428
+ const {
2429
+ shouldDoFullVerification,
2430
+ urgency,
2431
+ pThorough,
2432
+ newduration,
2433
+ rampedFloor,
2434
+ rqWindowFloor,
2435
+ verificationReason,
2436
+ } = renewalPlan;
2437
+
2438
+ logger.debug("Renewal verification plan", {
2439
+ component: "TokenRenewalService",
2440
+ method: "checkandrenew_jwt_token",
2441
+ requestId,
2442
+ forceRenewal,
2443
+ durationLeftpct: durationLeftpct.toFixed(1),
2444
+ urgency: urgency.toFixed(3),
2445
+ pThorough: pThorough.toFixed(3),
2446
+ newduration: Math.floor(newduration),
2447
+ rampedFloor: Math.floor(rampedFloor),
2448
+ rqWindowFloor: Math.floor(rqWindowFloor),
2449
+ verificationReason,
2450
+ shouldDoFullVerification,
2451
+ });
2285
2452
 
2286
2453
  const verificationStartTime = Date.now();
2287
2454
 
@@ -2366,7 +2533,7 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2366
2533
  logInfo: {
2367
2534
  newDuration: newduration,
2368
2535
  reason: shouldDoFullVerification
2369
- ? "Thorough verification"
2536
+ ? `Thorough verification (${verificationReason})`
2370
2537
  : "Brief verification",
2371
2538
  notAfter: notAfter,
2372
2539
  renewalDuration,
@@ -2409,10 +2576,18 @@ async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
2409
2576
 
2410
2577
 
2411
2578
  // Export the class directly (will be instantiated in rodit.js)
2412
- module.exports = {generate_jwt_token,base64url2jwk_public_key,
2579
+ module.exports = {
2580
+ generate_jwt_token,
2581
+ base64url2jwk_public_key,
2413
2582
  checkandrenew_jwt_token,
2583
+ resolveRenewalVerificationPlan,
2414
2584
  thorough_validate_jwt_token_be,
2415
2585
  brief_validate_jwt_token_be,
2416
2586
  generate_jwt_token_fromtoken,
2417
- verify_jwt_token,validate_jwt_token_be, generate_session_termination_token
2587
+ verify_jwt_token,
2588
+ validate_jwt_token_be,
2589
+ generate_session_termination_token,
2590
+ parseRoditJwtDurationSeconds,
2591
+ resolveSessionExpirationUnix,
2592
+ resolveCredentialExpirationUnix,
2418
2593
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rodit/rodit-auth-be",
3
- "version": "9.11.14",
3
+ "version": "9.11.20",
4
4
  "description": "RODiT-based authentication system for Express.js applications",
5
5
  "main": "index.js",
6
6
  "exports": {