@rodit/rodit-auth-be 9.11.14
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 +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
|
@@ -0,0 +1,2418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for JWT token operations
|
|
3
|
+
* Copyright (c) 2026 Discernible IO. All rights reserved.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { ulid } = require("ulid");
|
|
7
|
+
const config = require('../../services/configsdk');
|
|
8
|
+
const logger = require("../../services/logger");
|
|
9
|
+
const { createLogContext, logErrorWithMetrics } = logger;
|
|
10
|
+
const nacl = require("tweetnacl");
|
|
11
|
+
const crypto = require("crypto");
|
|
12
|
+
const {
|
|
13
|
+
dateStringToUnixTime,
|
|
14
|
+
unixTimeToDateString,
|
|
15
|
+
isRoditUnboundedDate,
|
|
16
|
+
roditNotAfterUnixCap,
|
|
17
|
+
} = require("../../services/utils");
|
|
18
|
+
const { sessionManager } = require('./sessionmanager');
|
|
19
|
+
|
|
20
|
+
// Log which SessionManager instance is being used
|
|
21
|
+
logger.infoWithContext("TokenService using SessionManager instance", {
|
|
22
|
+
component: "TokenService",
|
|
23
|
+
event: "sessionManager_import",
|
|
24
|
+
sessionManagerInstanceId: sessionManager._instanceId,
|
|
25
|
+
timestamp: new Date().toISOString()
|
|
26
|
+
});
|
|
27
|
+
const stateManager = require('../blockchain/statemanager');
|
|
28
|
+
const {
|
|
29
|
+
nearorg_rpc_tokenfromroditid,
|
|
30
|
+
nearorg_rpc_tokensfromaccountid,
|
|
31
|
+
nearorg_rpc_fetchpublickeybytes,
|
|
32
|
+
} = require("../blockchain/blockchainservice");
|
|
33
|
+
|
|
34
|
+
// Dynamic import for ESM 'jose' in CommonJS context
|
|
35
|
+
let _josePromise;
|
|
36
|
+
async function getJose() {
|
|
37
|
+
if (!_josePromise) {
|
|
38
|
+
_josePromise = import("jose");
|
|
39
|
+
}
|
|
40
|
+
return _josePromise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Ensures base64url data is canonical (no equivalent alternative encodings).
|
|
45
|
+
*
|
|
46
|
+
* @param {string} value - base64url encoded value
|
|
47
|
+
* @returns {boolean} true when value round-trips to the exact same string
|
|
48
|
+
*/
|
|
49
|
+
function isCanonicalBase64Url(value) {
|
|
50
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
return Buffer.from(value, "base64url").toString("base64url") === value;
|
|
60
|
+
} catch (_error) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseRoditJwtDurationSeconds(metadata) {
|
|
66
|
+
const parsed = parseInt(metadata?.jwt_duration, 10);
|
|
67
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
68
|
+
return Math.floor(parsed);
|
|
69
|
+
}
|
|
70
|
+
return config.getDefaultJwtDurationSeconds();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Server session end time (unix seconds).
|
|
75
|
+
* Prefers SECURITY_OPTIONS.SESSION_TTL_SECONDS when set; else passport not_after / jwt_duration.
|
|
76
|
+
* Always capped by bounded peer/own not_after. Credential JWT exp is separate (renewal).
|
|
77
|
+
*/
|
|
78
|
+
async function resolveSessionExpirationUnix(peer_rodit, own_rodit, now) {
|
|
79
|
+
const peerCap = await roditNotAfterUnixCap(peer_rodit?.metadata?.not_after);
|
|
80
|
+
const ownCap = await roditNotAfterUnixCap(own_rodit?.metadata?.not_after);
|
|
81
|
+
const notAfterCaps = [peerCap, ownCap].filter((cap) => cap !== null);
|
|
82
|
+
|
|
83
|
+
const configuredTtl = config.getSessionTtlSeconds();
|
|
84
|
+
let sessionExpiration;
|
|
85
|
+
|
|
86
|
+
if (configuredTtl != null) {
|
|
87
|
+
sessionExpiration = now + configuredTtl;
|
|
88
|
+
} else if (notAfterCaps.length > 0) {
|
|
89
|
+
sessionExpiration = Math.min(...notAfterCaps);
|
|
90
|
+
} else {
|
|
91
|
+
const peerSec = parseRoditJwtDurationSeconds(peer_rodit?.metadata);
|
|
92
|
+
const ownSec = parseRoditJwtDurationSeconds(own_rodit?.metadata);
|
|
93
|
+
sessionExpiration = now + Math.max(peerSec, ownSec);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const cap of notAfterCaps) {
|
|
97
|
+
if (sessionExpiration > cap) {
|
|
98
|
+
sessionExpiration = cap;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return sessionExpiration;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Short-lived access credential; renewed until session expires. */
|
|
106
|
+
function resolveCredentialExpirationUnix(now, sessionExpiration, own_rodit) {
|
|
107
|
+
let tokenExpiration = now + parseRoditJwtDurationSeconds(own_rodit?.metadata);
|
|
108
|
+
if (tokenExpiration > sessionExpiration) {
|
|
109
|
+
tokenExpiration = sessionExpiration;
|
|
110
|
+
}
|
|
111
|
+
return tokenExpiration;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Converts a base64url string to a JWK public key
|
|
116
|
+
*
|
|
117
|
+
* @param {string} base64url_public_key - Base64url encoded public key
|
|
118
|
+
* @returns {Promise<Object>} JWK public key object
|
|
119
|
+
*/
|
|
120
|
+
async function base64url2jwk_public_key(base64url_public_key) {
|
|
121
|
+
const startTime = Date.now();
|
|
122
|
+
const requestId = ulid();
|
|
123
|
+
|
|
124
|
+
// Create a base context that will be used throughout this function
|
|
125
|
+
const baseContext = createLogContext(
|
|
126
|
+
"Transformer",
|
|
127
|
+
"base64url2jwk_public_key",
|
|
128
|
+
{ requestId }
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
logger.debugWithContext("Converting base64url to JWK public key", baseContext);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const jwk_public_key = {
|
|
135
|
+
kty: "OKP",
|
|
136
|
+
crv: "Ed25519",
|
|
137
|
+
x: base64url_public_key,
|
|
138
|
+
use: "sig",
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
logger.debug("JWK public key structure created", {
|
|
142
|
+
component: "Transformer",
|
|
143
|
+
method: "base64url2jwk_public_key",
|
|
144
|
+
requestId,
|
|
145
|
+
jwk: {
|
|
146
|
+
kty: jwk_public_key.kty,
|
|
147
|
+
crv: jwk_public_key.crv,
|
|
148
|
+
use: jwk_public_key.use,
|
|
149
|
+
xLength: jwk_public_key.x.length,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const { importJWK } = await getJose();
|
|
154
|
+
const session_jwk_public_key = await importJWK(jwk_public_key, "EdDSA");
|
|
155
|
+
|
|
156
|
+
const duration = Date.now() - startTime;
|
|
157
|
+
|
|
158
|
+
logger.debugWithContext("JWK public key import successful", {
|
|
159
|
+
...baseContext,
|
|
160
|
+
duration
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Emit metrics for dashboards
|
|
164
|
+
logger.metric("jwk_import_duration_ms", duration, {
|
|
165
|
+
component: "Transformer",
|
|
166
|
+
success: true
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
return session_jwk_public_key;
|
|
170
|
+
} catch (error) {
|
|
171
|
+
const duration = Date.now() - startTime;
|
|
172
|
+
|
|
173
|
+
// Use logErrorWithMetrics for standardized error logging and metrics
|
|
174
|
+
logErrorWithMetrics({
|
|
175
|
+
error,
|
|
176
|
+
context: {
|
|
177
|
+
...baseContext,
|
|
178
|
+
duration
|
|
179
|
+
},
|
|
180
|
+
metrics: [
|
|
181
|
+
{
|
|
182
|
+
name: "jwk_import_duration_ms",
|
|
183
|
+
value: duration,
|
|
184
|
+
tags: { success: false, error: error.constructor.name }
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
name: "jwk_import_errors_total",
|
|
188
|
+
value: 1,
|
|
189
|
+
tags: { errorType: error.constructor.name }
|
|
190
|
+
}
|
|
191
|
+
]
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Verifies a JWT token
|
|
200
|
+
*
|
|
201
|
+
* @param {string} token - JWT token to verify
|
|
202
|
+
* @param {Object} jwk_public_key - JWK public key for verification
|
|
203
|
+
* @param {number} timestamp - Current timestamp
|
|
204
|
+
* @param {string} requestId - Request ID for tracking
|
|
205
|
+
* @returns {Promise<Object>} Verification result with payload
|
|
206
|
+
*/
|
|
207
|
+
async function verify_jwt_token(token, jwk_public_key, timestamp, requestId) {
|
|
208
|
+
const startTime = Date.now();
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const { jwtVerify } = await getJose();
|
|
212
|
+
const result = await jwtVerify(token, jwk_public_key, {
|
|
213
|
+
algorithms: ["EdDSA"],
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const duration = Date.now() - startTime;
|
|
217
|
+
|
|
218
|
+
// Log session information if available
|
|
219
|
+
const sessionInfo = {
|
|
220
|
+
sessionId: result.payload.session_id || "none",
|
|
221
|
+
sessionStatus: result.payload.session_status || "unknown",
|
|
222
|
+
sessionCreatedAt: result.payload.session_iat
|
|
223
|
+
? new Date(result.payload.session_iat * 1000).toISOString()
|
|
224
|
+
: "unknown",
|
|
225
|
+
sessionExpiresAt: result.payload.session_exp
|
|
226
|
+
? new Date(result.payload.session_exp * 1000).toISOString()
|
|
227
|
+
: "unknown",
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Emit metrics for dashboards
|
|
231
|
+
logger.metric("token_verification_duration_ms", duration, {
|
|
232
|
+
component: "TokenVerifier",
|
|
233
|
+
success: true,
|
|
234
|
+
session_status: result.payload.session_status || "unknown",
|
|
235
|
+
});
|
|
236
|
+
logger.metric("token_verifications_total", 1, {
|
|
237
|
+
component: "TokenVerifier",
|
|
238
|
+
success: true,
|
|
239
|
+
algorithm: "EdDSA",
|
|
240
|
+
session_status: result.payload.session_status || "unknown",
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return result;
|
|
244
|
+
} catch (jwtError) {
|
|
245
|
+
const duration = Date.now() - startTime;
|
|
246
|
+
|
|
247
|
+
if (jwtError.code === "ERR_JWT_EXPIRED") {
|
|
248
|
+
logger.error("Token expired, attempting renewal", {
|
|
249
|
+
component: "TokenVerifier",
|
|
250
|
+
method: "verify_jwt_token",
|
|
251
|
+
requestId,
|
|
252
|
+
duration,
|
|
253
|
+
errorCode: jwtError.code,
|
|
254
|
+
errorMessage: jwtError.message,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Emit metrics for dashboards
|
|
258
|
+
logger.metric("token_verification_duration_ms", duration, {
|
|
259
|
+
component: "TokenVerifier",
|
|
260
|
+
success: false,
|
|
261
|
+
error: "TOKEN_EXPIRED",
|
|
262
|
+
});
|
|
263
|
+
logger.metric("token_verifications_total", 1, {
|
|
264
|
+
component: "TokenVerifier",
|
|
265
|
+
success: false,
|
|
266
|
+
error: "TOKEN_EXPIRED",
|
|
267
|
+
});
|
|
268
|
+
logger.metric("expired_tokens_total", 1, {
|
|
269
|
+
component: "TokenVerifier",
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const config_own_rodit = await stateManager.getConfigOwnRodit();
|
|
274
|
+
const { decodeJwt } = await getJose();
|
|
275
|
+
const unverifiedpayload = decodeJwt(token);
|
|
276
|
+
|
|
277
|
+
// Log session information from expired token
|
|
278
|
+
const sessionInfo = {
|
|
279
|
+
sessionId: unverifiedpayload.session_id || "none",
|
|
280
|
+
sessionStatus: unverifiedpayload.session_status || "unknown",
|
|
281
|
+
sessionCreatedAt: unverifiedpayload.session_iat
|
|
282
|
+
? new Date(unverifiedpayload.session_iat * 1000).toISOString()
|
|
283
|
+
: "unknown",
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const renewalStartTime = Date.now();
|
|
287
|
+
const { isValid, notAfter } =
|
|
288
|
+
await thorough_validate_jwt_token_be(
|
|
289
|
+
unverifiedpayload,
|
|
290
|
+
requestId
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
if (isValid) {
|
|
294
|
+
logger.info("Generating new token for expired but valid token", {
|
|
295
|
+
component: "TokenVerifier",
|
|
296
|
+
method: "verify_jwt_token",
|
|
297
|
+
requestId,
|
|
298
|
+
subject: unverifiedpayload.sub,
|
|
299
|
+
notAfter: notAfter,
|
|
300
|
+
sessionId: unverifiedpayload.session_id || "none",
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Use full verification for expired tokens
|
|
304
|
+
const newToken = await generate_jwt_token_fromtoken(
|
|
305
|
+
unverifiedpayload,
|
|
306
|
+
config_own_rodit.own_rodit.metadata.jwt_duration,
|
|
307
|
+
notAfter,
|
|
308
|
+
timestamp,
|
|
309
|
+
"full" // Expired tokens require full verification
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const renewalDuration = Date.now() - renewalStartTime;
|
|
313
|
+
// Emit metrics for dashboards
|
|
314
|
+
logger.metric("token_renewal_duration_ms", renewalDuration, {
|
|
315
|
+
component: "TokenVerifier",
|
|
316
|
+
success: true,
|
|
317
|
+
reason: "EXPIRED",
|
|
318
|
+
session_status: "renewed_full_verification",
|
|
319
|
+
});
|
|
320
|
+
logger.metric("token_renewals_total", 1, {
|
|
321
|
+
component: "TokenVerifier",
|
|
322
|
+
reason: "EXPIRED",
|
|
323
|
+
session_status: "renewed_full_verification",
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
payload: unverifiedpayload,
|
|
328
|
+
protectedHeader: null,
|
|
329
|
+
newToken,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const renewalDuration = Date.now() - renewalStartTime;
|
|
334
|
+
logger.error("Token renewal failed - invalid token", {
|
|
335
|
+
component: "TokenVerifier",
|
|
336
|
+
method: "verify_jwt_token",
|
|
337
|
+
requestId,
|
|
338
|
+
renewalDuration,
|
|
339
|
+
totalDuration: Date.now() - startTime,
|
|
340
|
+
tokenId: unverifiedpayload.jti || "unknown",
|
|
341
|
+
sessionId: unverifiedpayload.session_id || "none",
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// Emit metrics for dashboards
|
|
345
|
+
logger.metric("token_renewal_duration_ms", renewalDuration, {
|
|
346
|
+
component: "TokenVerifier",
|
|
347
|
+
success: false,
|
|
348
|
+
error: "VALIDATION_FAILED",
|
|
349
|
+
});
|
|
350
|
+
logger.metric("token_renewal_failures_total", 1, {
|
|
351
|
+
component: "TokenVerifier",
|
|
352
|
+
reason: "VALIDATION_FAILED",
|
|
353
|
+
});
|
|
354
|
+
} catch (renewalError) {
|
|
355
|
+
logger.error("Error during token renewal process", {
|
|
356
|
+
component: "TokenVerifier",
|
|
357
|
+
method: "verify_jwt_token",
|
|
358
|
+
requestId,
|
|
359
|
+
duration: Date.now() - startTime,
|
|
360
|
+
errorMessage: renewalError.message,
|
|
361
|
+
errorCode: renewalError.code || "UNKNOWN_ERROR",
|
|
362
|
+
stack: renewalError.stack,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
// Emit metrics for dashboards
|
|
366
|
+
logger.metric("token_renewal_errors_total", 1, {
|
|
367
|
+
component: "TokenVerifier",
|
|
368
|
+
error: renewalError.code || "UNKNOWN_ERROR",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
// Handle other JWT errors
|
|
373
|
+
logger.error("JWT verification error", {
|
|
374
|
+
component: "TokenVerifier",
|
|
375
|
+
method: "verify_jwt_token",
|
|
376
|
+
requestId,
|
|
377
|
+
duration,
|
|
378
|
+
errorCode: jwtError.code || "UNKNOWN_ERROR",
|
|
379
|
+
errorMessage: jwtError.message,
|
|
380
|
+
stack: jwtError.stack,
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Emit metrics for dashboards
|
|
384
|
+
logger.metric("token_verification_duration_ms", duration, {
|
|
385
|
+
component: "TokenVerifier",
|
|
386
|
+
success: false,
|
|
387
|
+
error: jwtError.code || "UNKNOWN_ERROR",
|
|
388
|
+
});
|
|
389
|
+
logger.metric("token_verifications_total", 1, {
|
|
390
|
+
component: "TokenVerifier",
|
|
391
|
+
success: false,
|
|
392
|
+
error: jwtError.code || "UNKNOWN_ERROR",
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
throw jwtError;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Generate a new JWT token
|
|
402
|
+
*
|
|
403
|
+
* @param {Object} peer_rodit - Peer RODiT token object
|
|
404
|
+
* @param {number} peer_timestamp - Peer timestamp
|
|
405
|
+
* @param {Object} own_rodit - Own RODiT token object
|
|
406
|
+
* @param {Uint8Array} own_rodit_bytes_private_key - Private key bytes
|
|
407
|
+
* @param {string} session_status - Session status
|
|
408
|
+
* @returns {Promise<string>} Generated JWT token
|
|
409
|
+
*/
|
|
410
|
+
async function generate_jwt_token(
|
|
411
|
+
peer_rodit,
|
|
412
|
+
peer_timestamp,
|
|
413
|
+
own_rodit,
|
|
414
|
+
own_rodit_bytes_private_key,
|
|
415
|
+
session_status = "new"
|
|
416
|
+
) {
|
|
417
|
+
const requestId = ulid();
|
|
418
|
+
const startTime = Date.now();
|
|
419
|
+
|
|
420
|
+
// Create a base context that will be used throughout this function
|
|
421
|
+
const baseContext = createLogContext(
|
|
422
|
+
"JwtAuth",
|
|
423
|
+
"generate_jwt_token",
|
|
424
|
+
{
|
|
425
|
+
requestId,
|
|
426
|
+
peerRoditId: peer_rodit?.token_id,
|
|
427
|
+
peerTimestamp: peer_timestamp,
|
|
428
|
+
ownRoditId: own_rodit?.token_id,
|
|
429
|
+
sessionStatus: session_status
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
const now = peer_timestamp;
|
|
435
|
+
|
|
436
|
+
const notafterStart = Date.now();
|
|
437
|
+
const sessionExpiration = await resolveSessionExpirationUnix(
|
|
438
|
+
peer_rodit,
|
|
439
|
+
own_rodit,
|
|
440
|
+
now
|
|
441
|
+
);
|
|
442
|
+
const tokenExpiration = resolveCredentialExpirationUnix(
|
|
443
|
+
now,
|
|
444
|
+
sessionExpiration,
|
|
445
|
+
own_rodit
|
|
446
|
+
);
|
|
447
|
+
const sessionValidFor = sessionExpiration - now;
|
|
448
|
+
const credentialValidFor = tokenExpiration - now;
|
|
449
|
+
const notafterDuration = Date.now() - notafterStart;
|
|
450
|
+
|
|
451
|
+
logger.debugWithContext("Calculated token parameters", {
|
|
452
|
+
...baseContext,
|
|
453
|
+
now,
|
|
454
|
+
sessionExpiration,
|
|
455
|
+
tokenExpiration,
|
|
456
|
+
sessionValidFor,
|
|
457
|
+
credentialValidFor,
|
|
458
|
+
credentialShorterThanSession: credentialValidFor < sessionValidFor,
|
|
459
|
+
sessionTtlSeconds: config.getSessionTtlSeconds(),
|
|
460
|
+
notafterDuration
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
const notbeforeStart = Date.now();
|
|
464
|
+
const notbefore = await dateStringToUnixTime(
|
|
465
|
+
own_rodit.metadata.not_before
|
|
466
|
+
);
|
|
467
|
+
const notbeforeDuration = Date.now() - notbeforeStart;
|
|
468
|
+
|
|
469
|
+
logger.debugWithContext("Retrieved not-before time", {
|
|
470
|
+
...baseContext,
|
|
471
|
+
notbefore,
|
|
472
|
+
notbeforeDuration
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const encodeStart = Date.now();
|
|
476
|
+
const timeString = await unixTimeToDateString(peer_timestamp);
|
|
477
|
+
const roditidandtimestamp = new TextEncoder().encode(
|
|
478
|
+
own_rodit.token_id + timeString
|
|
479
|
+
);
|
|
480
|
+
const encodeDuration = Date.now() - encodeStart;
|
|
481
|
+
|
|
482
|
+
logger.debugWithContext("Encoded RODiT and timestamp", {
|
|
483
|
+
...baseContext,
|
|
484
|
+
encodeDuration,
|
|
485
|
+
roditIdLength: own_rodit.token_id.length,
|
|
486
|
+
timestampLength: now.toString().length,
|
|
487
|
+
totalLength: roditidandtimestamp.length
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
const signatureStart = Date.now();
|
|
491
|
+
|
|
492
|
+
// DEVELOPMENT ENVIRONMENT ONLY - Add detailed private key debugging before signing
|
|
493
|
+
logger.debugWithContext("PRIVATE KEY DEBUG - Before Signing", {
|
|
494
|
+
...baseContext,
|
|
495
|
+
keyType: typeof own_rodit_bytes_private_key,
|
|
496
|
+
isUint8Array: own_rodit_bytes_private_key instanceof Uint8Array,
|
|
497
|
+
isBuffer: Buffer.isBuffer(own_rodit_bytes_private_key),
|
|
498
|
+
keyLength: own_rodit_bytes_private_key ? own_rodit_bytes_private_key.length : 0,
|
|
499
|
+
keyConstructor: own_rodit_bytes_private_key ? own_rodit_bytes_private_key.constructor.name : 'undefined',
|
|
500
|
+
keyIsNull: own_rodit_bytes_private_key === null,
|
|
501
|
+
keyIsNotDefined: own_rodit_bytes_private_key === undefined,
|
|
502
|
+
keySource: 'tokenservice.generate_jwt_token.before_signing',
|
|
503
|
+
// DEV ONLY - Show actual key bytes for debugging
|
|
504
|
+
keyFirstBytes: own_rodit_bytes_private_key && own_rodit_bytes_private_key.length > 0 ?
|
|
505
|
+
Array.from(own_rodit_bytes_private_key.slice(0, 8)).map(b => b.toString(16).padStart(2, '0')).join(' ') : 'N/A',
|
|
506
|
+
dataToSign: roditidandtimestamp.toString('hex').substring(0, 50) + '...'
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Check if the private key is a Uint8Array, which is required for nacl.sign.detached
|
|
510
|
+
let privateKeyToUse = own_rodit_bytes_private_key;
|
|
511
|
+
|
|
512
|
+
// Add diagnostic logging to help identify the issue
|
|
513
|
+
if (!(own_rodit_bytes_private_key instanceof Uint8Array)) {
|
|
514
|
+
// Capture detailed information about the key
|
|
515
|
+
const keyInfo = {
|
|
516
|
+
...baseContext,
|
|
517
|
+
type: typeof own_rodit_bytes_private_key,
|
|
518
|
+
isNull: own_rodit_bytes_private_key === null,
|
|
519
|
+
isUndefined: own_rodit_bytes_private_key === undefined,
|
|
520
|
+
isBuffer: Buffer.isBuffer(own_rodit_bytes_private_key),
|
|
521
|
+
keyLength: own_rodit_bytes_private_key ? own_rodit_bytes_private_key.length : 0,
|
|
522
|
+
keyConstructor: own_rodit_bytes_private_key ? own_rodit_bytes_private_key.constructor.name : 'undefined',
|
|
523
|
+
// DEV ONLY - Show actual key representation for debugging
|
|
524
|
+
keyStringified: own_rodit_bytes_private_key ?
|
|
525
|
+
JSON.stringify(own_rodit_bytes_private_key).substring(0, 100) + '...' : 'N/A',
|
|
526
|
+
keySource: 'tokenservice.generate_jwt_token'
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
// Log the detailed information
|
|
530
|
+
logger.debugWithContext("Private key is not a Uint8Array - Detailed Analysis", keyInfo);
|
|
531
|
+
|
|
532
|
+
// If it's a Buffer, we can convert it to a Uint8Array
|
|
533
|
+
if (Buffer.isBuffer(own_rodit_bytes_private_key)) {
|
|
534
|
+
logger.infoWithContext("Converting Buffer to Uint8Array", baseContext);
|
|
535
|
+
privateKeyToUse = new Uint8Array(own_rodit_bytes_private_key);
|
|
536
|
+
|
|
537
|
+
// Verify the conversion was successful
|
|
538
|
+
logger.debugWithContext("Buffer conversion result", {
|
|
539
|
+
...baseContext,
|
|
540
|
+
convertedIsUint8Array: privateKeyToUse instanceof Uint8Array,
|
|
541
|
+
convertedLength: privateKeyToUse.length,
|
|
542
|
+
originalLength: own_rodit_bytes_private_key.length,
|
|
543
|
+
// DEV ONLY - Show first few bytes to verify integrity
|
|
544
|
+
convertedFirstBytes: Array.from(privateKeyToUse.slice(0, 8)).map(b => b.toString(16).padStart(2, '0')).join(' '),
|
|
545
|
+
originalFirstBytes: Array.from(own_rodit_bytes_private_key.slice(0, 8)).map(b => b.toString(16).padStart(2, '0')).join(' ')
|
|
546
|
+
});
|
|
547
|
+
} else if (typeof own_rodit_bytes_private_key === 'object' && own_rodit_bytes_private_key !== null) {
|
|
548
|
+
// Try to recover from a JSON-serialized Uint8Array or similar object
|
|
549
|
+
logger.warnWithContext("Attempting to recover private key from non-standard format", {
|
|
550
|
+
...baseContext,
|
|
551
|
+
recoveryAttempt: true
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
try {
|
|
555
|
+
// If it's an array-like object, try to convert it to Uint8Array
|
|
556
|
+
if (Array.isArray(own_rodit_bytes_private_key) ||
|
|
557
|
+
(own_rodit_bytes_private_key.length !== undefined && typeof own_rodit_bytes_private_key.length === 'number')) {
|
|
558
|
+
privateKeyToUse = new Uint8Array(
|
|
559
|
+
Array.isArray(own_rodit_bytes_private_key) ?
|
|
560
|
+
own_rodit_bytes_private_key :
|
|
561
|
+
Array.from(own_rodit_bytes_private_key)
|
|
562
|
+
);
|
|
563
|
+
|
|
564
|
+
logger.infoWithContext("Successfully recovered private key from array-like object", {
|
|
565
|
+
...baseContext,
|
|
566
|
+
recoveredKeyLength: privateKeyToUse.length,
|
|
567
|
+
recoveredIsUint8Array: privateKeyToUse instanceof Uint8Array,
|
|
568
|
+
// DEV ONLY - Show first few bytes
|
|
569
|
+
recoveredFirstBytes: Array.from(privateKeyToUse.slice(0, 8)).map(b => b.toString(16).padStart(2, '0')).join(' ')
|
|
570
|
+
});
|
|
571
|
+
} else {
|
|
572
|
+
throw new Error("Cannot recover key - not an array-like object");
|
|
573
|
+
}
|
|
574
|
+
} catch (recoveryError) {
|
|
575
|
+
logErrorWithMetrics({
|
|
576
|
+
error: new Error(`Private key recovery failed: ${recoveryError.message}`),
|
|
577
|
+
context: {
|
|
578
|
+
...keyInfo,
|
|
579
|
+
recoveryError: recoveryError.message
|
|
580
|
+
},
|
|
581
|
+
metrics: [
|
|
582
|
+
{
|
|
583
|
+
name: "private_key_recovery_failures",
|
|
584
|
+
value: 1,
|
|
585
|
+
tags: { keyType: typeof own_rodit_bytes_private_key }
|
|
586
|
+
}
|
|
587
|
+
]
|
|
588
|
+
});
|
|
589
|
+
throw new Error("Private key must be a Uint8Array or Buffer for nacl.sign.detached");
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
logErrorWithMetrics({
|
|
593
|
+
error: new Error("Private key must be a Uint8Array or Buffer for nacl.sign.detached"),
|
|
594
|
+
context: keyInfo,
|
|
595
|
+
metrics: [
|
|
596
|
+
{
|
|
597
|
+
name: "private_key_format_errors_total",
|
|
598
|
+
value: 1,
|
|
599
|
+
tags: { keyType: typeof own_rodit_bytes_private_key }
|
|
600
|
+
}
|
|
601
|
+
]
|
|
602
|
+
});
|
|
603
|
+
throw new Error("Private key must be a Uint8Array or Buffer for nacl.sign.detached");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const own_rodit_bytes_signature = nacl.sign.detached(
|
|
608
|
+
roditidandtimestamp,
|
|
609
|
+
privateKeyToUse
|
|
610
|
+
);
|
|
611
|
+
const signatureDuration = Date.now() - signatureStart;
|
|
612
|
+
|
|
613
|
+
logger.debugWithContext("Created signature", {
|
|
614
|
+
...baseContext,
|
|
615
|
+
signatureDuration,
|
|
616
|
+
signatureLength: own_rodit_bytes_signature.length
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const base64Start = Date.now();
|
|
620
|
+
const own_roditid_base64url_signature = Buffer.from(
|
|
621
|
+
own_rodit_bytes_signature
|
|
622
|
+
).toString("base64url");
|
|
623
|
+
const base64Duration = Date.now() - base64Start;
|
|
624
|
+
|
|
625
|
+
logger.debugWithContext("Converted signature to base64url", {
|
|
626
|
+
...baseContext,
|
|
627
|
+
base64Duration,
|
|
628
|
+
base64Length: own_roditid_base64url_signature.length
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
const keyStart = Date.now();
|
|
632
|
+
const own_rodit_keyobject_private_key = crypto.createPrivateKey({
|
|
633
|
+
key: Buffer.concat([
|
|
634
|
+
Buffer.from("302e020100300506032b657004220420", "hex"),
|
|
635
|
+
own_rodit_bytes_private_key,
|
|
636
|
+
]),
|
|
637
|
+
format: "der",
|
|
638
|
+
type: "pkcs8",
|
|
639
|
+
});
|
|
640
|
+
const keyDuration = Date.now() - keyStart;
|
|
641
|
+
|
|
642
|
+
logger.debugWithContext("Created private key object", {
|
|
643
|
+
...baseContext,
|
|
644
|
+
keyDuration
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// Create and register session in SessionManager
|
|
648
|
+
const sessionData = {
|
|
649
|
+
roditId: peer_rodit.token_id,
|
|
650
|
+
ownerId: peer_rodit.owner_id,
|
|
651
|
+
createdAt: now,
|
|
652
|
+
expiresAt: sessionExpiration,
|
|
653
|
+
metadata: {
|
|
654
|
+
serviceProviderId: peer_rodit.metadata.serviceprovider_id,
|
|
655
|
+
ownRoditId: own_rodit.token_id,
|
|
656
|
+
notAfter: peer_rodit.metadata.not_after,
|
|
657
|
+
status: session_status,
|
|
658
|
+
},
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
let session_id = null; // Initialize to null, will be set by createSession
|
|
662
|
+
const sessionCreateStart = Date.now();
|
|
663
|
+
|
|
664
|
+
// Always attempt to create a session - SessionManager should handle all cases
|
|
665
|
+
try {
|
|
666
|
+
if (!sessionManager) {
|
|
667
|
+
logger.errorWithContext("Session manager is undefined - this should never happen", {
|
|
668
|
+
...baseContext,
|
|
669
|
+
roditId: peer_rodit.token_id
|
|
670
|
+
});
|
|
671
|
+
throw new Error("SessionManager is required for token generation");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (typeof sessionManager.createSession !== 'function') {
|
|
675
|
+
logger.errorWithContext("Session manager createSession method is not available", {
|
|
676
|
+
...baseContext,
|
|
677
|
+
roditId: peer_rodit.token_id,
|
|
678
|
+
sessionManagerType: typeof sessionManager,
|
|
679
|
+
hasCreateSession: sessionManager ? 'createSession' in sessionManager : false
|
|
680
|
+
});
|
|
681
|
+
throw new Error("SessionManager.createSession method is required");
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Session manager is available, proceed with session creation
|
|
685
|
+
const session = await sessionManager.createSession(sessionData);
|
|
686
|
+
const sessionCreateDuration = Date.now() - sessionCreateStart;
|
|
687
|
+
|
|
688
|
+
// Use the actual session ID returned by createSession
|
|
689
|
+
session_id = session?.id;
|
|
690
|
+
|
|
691
|
+
if (!session_id) {
|
|
692
|
+
throw new Error("SessionManager.createSession returned invalid session");
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
logger.infoWithContext("Session created in session manager", {
|
|
696
|
+
...baseContext,
|
|
697
|
+
sessionId: session?.id,
|
|
698
|
+
roditId: peer_rodit.token_id,
|
|
699
|
+
sessionStatus: session?.status,
|
|
700
|
+
sessionExpiresAt: session?.expiresAt,
|
|
701
|
+
sessionManagerInstanceId: sessionManager._instanceId,
|
|
702
|
+
sessionCreateDuration
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
} catch (sessionError) {
|
|
706
|
+
logger.errorWithContext(
|
|
707
|
+
"Failed to create session - cannot generate JWT token without valid session",
|
|
708
|
+
{
|
|
709
|
+
...baseContext,
|
|
710
|
+
error: sessionError.message,
|
|
711
|
+
roditId: peer_rodit.token_id,
|
|
712
|
+
sessionManagerInstanceId: sessionManager?._instanceId
|
|
713
|
+
}
|
|
714
|
+
);
|
|
715
|
+
throw new Error(`Session creation failed: ${sessionError.message}`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const jwtId = "jti" + ulid();
|
|
719
|
+
|
|
720
|
+
// Validate session_id before embedding in JWT token
|
|
721
|
+
if (!session_id || typeof session_id !== 'string' || session_id.trim() === '') {
|
|
722
|
+
logger.errorWithContext("Invalid session ID for JWT token generation", {
|
|
723
|
+
...baseContext,
|
|
724
|
+
sessionIdForJWT: session_id,
|
|
725
|
+
sessionIdType: typeof session_id,
|
|
726
|
+
jwtId,
|
|
727
|
+
roditId: peer_rodit.token_id,
|
|
728
|
+
sessionManagerInstanceId: sessionManager._instanceId
|
|
729
|
+
});
|
|
730
|
+
throw new Error(`Invalid session ID for JWT token: ${session_id}`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// Log the session ID that will be embedded in the JWT token
|
|
734
|
+
logger.infoWithContext("Embedding session ID in JWT token", {
|
|
735
|
+
...baseContext,
|
|
736
|
+
sessionIdForJWT: session_id,
|
|
737
|
+
sessionIdLength: session_id.length,
|
|
738
|
+
jwtId,
|
|
739
|
+
roditId: peer_rodit.token_id,
|
|
740
|
+
sessionManagerInstanceId: sessionManager._instanceId
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const jwtSignStart = Date.now();
|
|
744
|
+
const { SignJWT } = await getJose();
|
|
745
|
+
const token = await new SignJWT({
|
|
746
|
+
iss: peer_rodit.metadata.subjectuniqueidentifier_url,
|
|
747
|
+
sub:
|
|
748
|
+
peer_rodit.metadata.serviceprovider_id +
|
|
749
|
+
";sub=" +
|
|
750
|
+
peer_rodit.token_id,
|
|
751
|
+
aud: own_rodit.owner_id,
|
|
752
|
+
exp: tokenExpiration,
|
|
753
|
+
nbf: notbefore,
|
|
754
|
+
iat: now,
|
|
755
|
+
jti: jwtId,
|
|
756
|
+
// Add session information
|
|
757
|
+
session_id: session_id,
|
|
758
|
+
session_iat: now,
|
|
759
|
+
session_exp: sessionExpiration,
|
|
760
|
+
session_status: session_status,
|
|
761
|
+
rodit_id: own_rodit.token_id,
|
|
762
|
+
rodit_owner: own_rodit.owner_id,
|
|
763
|
+
rodit_idsignature: own_roditid_base64url_signature,
|
|
764
|
+
rodit_maxrequests: peer_rodit.metadata.max_requests,
|
|
765
|
+
rodit_maxrqwindow: peer_rodit.metadata.maxrq_window,
|
|
766
|
+
rodit_permissionedroutes: peer_rodit.metadata.permissioned_routes,
|
|
767
|
+
rodit_webhookcidr: peer_rodit.metadata.webhook_cidr,
|
|
768
|
+
rodit_allowedcidr: peer_rodit.metadata.allowed_cidr,
|
|
769
|
+
rodit_allowediso3166list: peer_rodit.metadata.allowed_iso3166list,
|
|
770
|
+
rodit_webhookurl: peer_rodit.metadata.webhook_url,
|
|
771
|
+
config_iso639: null,
|
|
772
|
+
config_iso3166: null,
|
|
773
|
+
config_iso15924: null,
|
|
774
|
+
config_timeoptions: null,
|
|
775
|
+
})
|
|
776
|
+
.setProtectedHeader({ alg: "EdDSA", typ: "JWT" })
|
|
777
|
+
.sign(own_rodit_keyobject_private_key);
|
|
778
|
+
const jwtSignDuration = Date.now() - jwtSignStart;
|
|
779
|
+
|
|
780
|
+
const totalDuration = Date.now() - startTime;
|
|
781
|
+
|
|
782
|
+
logger.infoWithContext("JWT token generation successful", {
|
|
783
|
+
...baseContext,
|
|
784
|
+
duration: totalDuration,
|
|
785
|
+
notafterDuration,
|
|
786
|
+
notbeforeDuration,
|
|
787
|
+
encodeDuration,
|
|
788
|
+
signatureDuration,
|
|
789
|
+
base64Duration,
|
|
790
|
+
keyDuration,
|
|
791
|
+
jwtSignDuration,
|
|
792
|
+
peerRoditId: peer_rodit.token_id,
|
|
793
|
+
ownRoditId: own_rodit.token_id,
|
|
794
|
+
jwtId,
|
|
795
|
+
sessionId: session_id,
|
|
796
|
+
sessionStatus: session_status,
|
|
797
|
+
tokenValidFor: tokenExpiration - now,
|
|
798
|
+
sessionValidFor: sessionExpiration - now
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Add metrics for successful token generation
|
|
802
|
+
logger.metric("jwt_token_generation", totalDuration, {
|
|
803
|
+
result: "success",
|
|
804
|
+
peer_rodit_id: peer_rodit.token_id,
|
|
805
|
+
valid_seconds: tokenExpiration - now,
|
|
806
|
+
session_status: session_status,
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
return token;
|
|
810
|
+
} catch (error) {
|
|
811
|
+
const duration = Date.now() - startTime;
|
|
812
|
+
|
|
813
|
+
logErrorWithMetrics({
|
|
814
|
+
error,
|
|
815
|
+
context: {
|
|
816
|
+
...baseContext,
|
|
817
|
+
duration,
|
|
818
|
+
peerRoditId: peer_rodit?.token_id,
|
|
819
|
+
ownRoditId: own_rodit?.token_id
|
|
820
|
+
},
|
|
821
|
+
metrics: [
|
|
822
|
+
{
|
|
823
|
+
name: "jwt_token_generation_errors",
|
|
824
|
+
value: 1,
|
|
825
|
+
tags: {
|
|
826
|
+
error_type: error.name || "Unknown",
|
|
827
|
+
peer_rodit_id: peer_rodit?.token_id || "unknown"
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
]
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
throw error;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Generate a new JWT token from an existing token
|
|
839
|
+
*
|
|
840
|
+
* @param {Object} token - Token payload
|
|
841
|
+
* @param {number} duration - New token duration in seconds
|
|
842
|
+
* @param {string} notafter - Not-after date string
|
|
843
|
+
* @param {number} timestamp - Current timestamp
|
|
844
|
+
* @param {string} verification_level - Verification level used
|
|
845
|
+
* @returns {Promise<string>} New JWT token
|
|
846
|
+
*/
|
|
847
|
+
async function generate_jwt_token_fromtoken(
|
|
848
|
+
token,
|
|
849
|
+
duration,
|
|
850
|
+
notafter,
|
|
851
|
+
timestamp,
|
|
852
|
+
verification_level = "light"
|
|
853
|
+
) {
|
|
854
|
+
const requestId = ulid();
|
|
855
|
+
const startTime = Date.now();
|
|
856
|
+
|
|
857
|
+
// Set session status based on verification level
|
|
858
|
+
const session_status =
|
|
859
|
+
verification_level === "full"
|
|
860
|
+
? "renewed_full_verification"
|
|
861
|
+
: "renewed_light_verification";
|
|
862
|
+
|
|
863
|
+
try {
|
|
864
|
+
const { SignJWT } = await getJose();
|
|
865
|
+
const now = Math.floor(Date.now() / 1000);
|
|
866
|
+
|
|
867
|
+
// Get token and session information from existing token
|
|
868
|
+
const existingSessionId = token.session_id;
|
|
869
|
+
|
|
870
|
+
// Check if session exists and is active in session manager
|
|
871
|
+
const sessionCheckStart = Date.now();
|
|
872
|
+
let isSessionValid = true;
|
|
873
|
+
|
|
874
|
+
if (existingSessionId) {
|
|
875
|
+
try {
|
|
876
|
+
isSessionValid = await sessionManager.isSessionActive(existingSessionId);
|
|
877
|
+
|
|
878
|
+
if (!isSessionValid) {
|
|
879
|
+
logger.error("Session inactive or closed - token renewal rejected", {
|
|
880
|
+
component: "JwtAuth",
|
|
881
|
+
method: "generate_jwt_token_fromtoken",
|
|
882
|
+
requestId,
|
|
883
|
+
sessionId: existingSessionId,
|
|
884
|
+
tokenJti: token.jti,
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
throw new Error("Session inactive or closed");
|
|
888
|
+
}
|
|
889
|
+
} catch (sessionError) {
|
|
890
|
+
logger.error("Session check failed", {
|
|
891
|
+
component: "JwtAuth",
|
|
892
|
+
method: "generate_jwt_token_fromtoken",
|
|
893
|
+
requestId,
|
|
894
|
+
sessionId: existingSessionId,
|
|
895
|
+
error: sessionError.message,
|
|
896
|
+
});
|
|
897
|
+
// Fail closed for session validation errors during renewal.
|
|
898
|
+
throw new Error(`Session check failed: ${sessionError.message}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Calculate new token expiration time (using the provided duration)
|
|
903
|
+
const slashedDuration = Math.floor(duration);
|
|
904
|
+
let tokenexpiration = slashedDuration + now;
|
|
905
|
+
const notafterCap = await roditNotAfterUnixCap(notafter);
|
|
906
|
+
const jwtMaxSecondsRoditUnbounded = parseInt(
|
|
907
|
+
config.get(
|
|
908
|
+
"SECURITY_OPTIONS.JWT_MAX_DURATION_SECONDS_RODIT_UNBOUNDED",
|
|
909
|
+
"86400"
|
|
910
|
+
),
|
|
911
|
+
10
|
|
912
|
+
);
|
|
913
|
+
const roditLinkedJwtCap =
|
|
914
|
+
notafterCap !== null
|
|
915
|
+
? notafterCap
|
|
916
|
+
: now +
|
|
917
|
+
(Number.isFinite(jwtMaxSecondsRoditUnbounded) &&
|
|
918
|
+
jwtMaxSecondsRoditUnbounded > 0
|
|
919
|
+
? jwtMaxSecondsRoditUnbounded
|
|
920
|
+
: 86400);
|
|
921
|
+
|
|
922
|
+
if (tokenexpiration > roditLinkedJwtCap) {
|
|
923
|
+
logger.error("Token renewal failed - RODiT-linked JWT cap exceeded", {
|
|
924
|
+
component: "JwtAuth",
|
|
925
|
+
requestId,
|
|
926
|
+
duration: Date.now() - startTime,
|
|
927
|
+
notAfterUnixTime: notafterCap,
|
|
928
|
+
roditLinkedJwtCap,
|
|
929
|
+
tokenExpiration: tokenexpiration,
|
|
930
|
+
difference: tokenexpiration - roditLinkedJwtCap,
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
throw new Error("RODiT has expired");
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const sessionExpUnix =
|
|
937
|
+
token.session_exp != null ? Number(token.session_exp) : null;
|
|
938
|
+
if (Number.isFinite(sessionExpUnix) && tokenexpiration > sessionExpUnix) {
|
|
939
|
+
tokenexpiration = sessionExpUnix;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const configStart = Date.now();
|
|
943
|
+
const config_own_rodit = await stateManager.getConfigOwnRodit();
|
|
944
|
+
const configDuration = Date.now() - configStart;
|
|
945
|
+
|
|
946
|
+
logger.debug("Retrieved configuration", {
|
|
947
|
+
requestId,
|
|
948
|
+
configDuration,
|
|
949
|
+
hasConfig: !!config_own_rodit,
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
const keyCreationStart = Date.now();
|
|
953
|
+
const own_rodit_keyobject_private_key = crypto.createPrivateKey({
|
|
954
|
+
key: Buffer.concat([
|
|
955
|
+
Buffer.from("302e020100300506032b657004220420", "hex"),
|
|
956
|
+
config_own_rodit.own_rodit_bytes_private_key,
|
|
957
|
+
]),
|
|
958
|
+
format: "der",
|
|
959
|
+
type: "pkcs8",
|
|
960
|
+
});
|
|
961
|
+
const keyCreationDuration = Date.now() - keyCreationStart;
|
|
962
|
+
|
|
963
|
+
logger.debug("Created private key object", {
|
|
964
|
+
requestId,
|
|
965
|
+
keyCreationDuration,
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// Keep existing session ID and creation time
|
|
969
|
+
const session_id = existingSessionId;
|
|
970
|
+
const session_iat = token.session_iat;
|
|
971
|
+
|
|
972
|
+
// Keep the original session expiration time consistent across renewals
|
|
973
|
+
const session_exp = token.session_exp;
|
|
974
|
+
|
|
975
|
+
// Update session information if needed
|
|
976
|
+
if (session_id) {
|
|
977
|
+
const sessionUpdateStart = Date.now();
|
|
978
|
+
const existingSession = await sessionManager.getSession(session_id);
|
|
979
|
+
if (!existingSession) {
|
|
980
|
+
throw new Error("Session not found during token renewal");
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const updated = await sessionManager.updateSession(session_id, {
|
|
984
|
+
status: "active",
|
|
985
|
+
metadata: {
|
|
986
|
+
...(existingSession.metadata || {}),
|
|
987
|
+
lastRenewalType: verification_level,
|
|
988
|
+
lastRenewalTime: now,
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
if (!updated) {
|
|
993
|
+
throw new Error("Failed to persist session update during token renewal");
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
logger.debug("Session updated in session manager", {
|
|
997
|
+
component: "JwtAuth",
|
|
998
|
+
method: "generate_jwt_token_fromtoken",
|
|
999
|
+
requestId,
|
|
1000
|
+
sessionId: session_id,
|
|
1001
|
+
updateDuration: Date.now() - sessionUpdateStart,
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
const jwtCreateStart = Date.now();
|
|
1006
|
+
const jwtId = "jti" + ulid();
|
|
1007
|
+
const newtoken = await new SignJWT({
|
|
1008
|
+
iss: token.iss,
|
|
1009
|
+
sub: token.sub,
|
|
1010
|
+
aud: token.aud,
|
|
1011
|
+
exp: tokenexpiration,
|
|
1012
|
+
nbf: token.nbf,
|
|
1013
|
+
iat: now,
|
|
1014
|
+
jti: jwtId,
|
|
1015
|
+
// Include consistent session information
|
|
1016
|
+
session_id: session_id,
|
|
1017
|
+
session_iat: session_iat,
|
|
1018
|
+
session_exp: session_exp,
|
|
1019
|
+
session_status: session_status,
|
|
1020
|
+
rodit_id: token.rodit_id,
|
|
1021
|
+
rodit_owner: token.rodit_owner,
|
|
1022
|
+
rodit_allowediso3166list: token.rodit_allowediso3166list,
|
|
1023
|
+
rodit_idsignature: token.rodit_idsignature,
|
|
1024
|
+
rodit_maxrequests: token.rodit_maxrequests,
|
|
1025
|
+
rodit_maxrqwindow: token.rodit_maxrqwindow,
|
|
1026
|
+
rodit_permissionedroutes: token.rodit_permissionedroutes,
|
|
1027
|
+
rodit_webhookcidr: token.rodit_webhookcidr,
|
|
1028
|
+
rodit_allowedcidr: token.rodit_allowedcidr,
|
|
1029
|
+
rodit_webhookurl: token.rodit_webhookurl,
|
|
1030
|
+
config_iso639: null,
|
|
1031
|
+
config_iso3166: null,
|
|
1032
|
+
config_iso15924: null,
|
|
1033
|
+
config_timeoptions: null,
|
|
1034
|
+
})
|
|
1035
|
+
.setProtectedHeader({ alg: "EdDSA", typ: "JWT" })
|
|
1036
|
+
.sign(own_rodit_keyobject_private_key);
|
|
1037
|
+
const jwtCreateDuration = Date.now() - jwtCreateStart;
|
|
1038
|
+
|
|
1039
|
+
const totalDuration = Date.now() - startTime;
|
|
1040
|
+
|
|
1041
|
+
logger.info("JWT token renewal successful", {
|
|
1042
|
+
component: "JwtAuth",
|
|
1043
|
+
method: "generate_jwt_token_fromtoken",
|
|
1044
|
+
requestId,
|
|
1045
|
+
duration: totalDuration,
|
|
1046
|
+
configDuration,
|
|
1047
|
+
keyCreationDuration,
|
|
1048
|
+
jwtCreateDuration,
|
|
1049
|
+
tokenJti: token.jti,
|
|
1050
|
+
newTokenJti: jwtId,
|
|
1051
|
+
newTokenExpiration: tokenexpiration,
|
|
1052
|
+
sessionId: session_id,
|
|
1053
|
+
sessionExpiration: new Date(session_exp * 1000).toISOString(),
|
|
1054
|
+
sessionStatus: session_status,
|
|
1055
|
+
verificationLevel: verification_level,
|
|
1056
|
+
validFor: tokenexpiration - now,
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// Add metrics for successful token renewals
|
|
1060
|
+
logger.metric("jwt_token_renewals", totalDuration, {
|
|
1061
|
+
result: "success",
|
|
1062
|
+
valid_seconds: tokenexpiration - now,
|
|
1063
|
+
verification_level: verification_level,
|
|
1064
|
+
session_status: session_status,
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
return newtoken;
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
const duration = Date.now() - startTime;
|
|
1070
|
+
|
|
1071
|
+
logger.error("Failed to generate new JWT token", {
|
|
1072
|
+
component: "JwtAuth",
|
|
1073
|
+
method: "generate_jwt_token_fromtoken",
|
|
1074
|
+
requestId,
|
|
1075
|
+
duration,
|
|
1076
|
+
tokenJti: token.jti,
|
|
1077
|
+
error: {
|
|
1078
|
+
message: error.message,
|
|
1079
|
+
stack: error.stack,
|
|
1080
|
+
name: error.name,
|
|
1081
|
+
},
|
|
1082
|
+
});
|
|
1083
|
+
// Add metrics for token generation errors
|
|
1084
|
+
logger.metric("jwt_token_renewal_errors", 1, {
|
|
1085
|
+
error_type: error.name || "Unknown",
|
|
1086
|
+
token_jti: token.jti || "unknown",
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
throw error;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Generate a session termination token for logout
|
|
1095
|
+
*
|
|
1096
|
+
* @param {Object} decodedToken - The decoded JWT token from the user's request
|
|
1097
|
+
* @param {number} duration - Token duration in seconds (typically short for termination tokens)
|
|
1098
|
+
* @returns {Promise<string>} Generated session termination token
|
|
1099
|
+
*/
|
|
1100
|
+
async function generate_session_termination_token(decodedToken, duration = 60) {
|
|
1101
|
+
const requestId = ulid();
|
|
1102
|
+
const startTime = Date.now();
|
|
1103
|
+
|
|
1104
|
+
logger.debug("Starting session termination token generation", {
|
|
1105
|
+
component: "JwtAuth",
|
|
1106
|
+
method: "generate_session_termination_token",
|
|
1107
|
+
requestId,
|
|
1108
|
+
tokenJti: decodedToken?.jti,
|
|
1109
|
+
sessionId: decodedToken?.session_id,
|
|
1110
|
+
duration
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
const { SignJWT } = await getJose();
|
|
1115
|
+
// Get configuration from state manager
|
|
1116
|
+
const config_own_rodit = await stateManager.getConfigOwnRodit();
|
|
1117
|
+
|
|
1118
|
+
if (!config_own_rodit || !config_own_rodit.own_rodit) {
|
|
1119
|
+
throw new Error("Missing own RODiT configuration");
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1123
|
+
const exp = now + duration;
|
|
1124
|
+
|
|
1125
|
+
// Create payload with session_status="closed"
|
|
1126
|
+
const payload = {
|
|
1127
|
+
...decodedToken,
|
|
1128
|
+
iat: now,
|
|
1129
|
+
exp: exp,
|
|
1130
|
+
session_status: "closed",
|
|
1131
|
+
jti: ulid() // Generate a new unique ID for this token
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
// Create a proper private key object from the raw bytes
|
|
1135
|
+
const own_rodit_keyobject_private_key = crypto.createPrivateKey({
|
|
1136
|
+
key: Buffer.concat([
|
|
1137
|
+
Buffer.from("302e020100300506032b657004220420", "hex"),
|
|
1138
|
+
config_own_rodit.own_rodit_bytes_private_key,
|
|
1139
|
+
]),
|
|
1140
|
+
format: "der",
|
|
1141
|
+
type: "pkcs8",
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// Sign the token with the proper key object
|
|
1145
|
+
const token = await new SignJWT(payload)
|
|
1146
|
+
.setProtectedHeader({ alg: "EdDSA", typ: "JWT" })
|
|
1147
|
+
.sign(own_rodit_keyobject_private_key);
|
|
1148
|
+
|
|
1149
|
+
logger.info("Generated session termination token", {
|
|
1150
|
+
component: "JwtAuth",
|
|
1151
|
+
method: "generate_session_termination_token",
|
|
1152
|
+
requestId,
|
|
1153
|
+
duration: Date.now() - startTime,
|
|
1154
|
+
tokenJti: payload.jti,
|
|
1155
|
+
expiration: new Date(exp * 1000).toISOString()
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
return token;
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
logger.error("Failed to generate session termination token", {
|
|
1161
|
+
component: "JwtAuth",
|
|
1162
|
+
method: "generate_session_termination_token",
|
|
1163
|
+
requestId,
|
|
1164
|
+
duration: Date.now() - startTime,
|
|
1165
|
+
error: error.message,
|
|
1166
|
+
stack: error.stack
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* Validate a JWT token
|
|
1175
|
+
*
|
|
1176
|
+
* @param {Object} token - Token payload
|
|
1177
|
+
* @param {Object} rodit - RODiT token object
|
|
1178
|
+
* @returns {Promise<Object>} Validation result with payload
|
|
1179
|
+
*/
|
|
1180
|
+
async function validate_jwt_token_be(token, rodit, options = {}) {
|
|
1181
|
+
const requestId = ulid();
|
|
1182
|
+
const startTime = Date.now();
|
|
1183
|
+
let isExpired = false;
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
// Decode the token without verification to get the payload
|
|
1187
|
+
const { decodeJwt } = await getJose();
|
|
1188
|
+
const unverifiedpayload = decodeJwt(token);
|
|
1189
|
+
|
|
1190
|
+
const sp_rodit = await nearorg_rpc_tokenfromroditid(
|
|
1191
|
+
unverifiedpayload.rodit_id
|
|
1192
|
+
);
|
|
1193
|
+
|
|
1194
|
+
// Additional diagnostic logging for troubleshooting
|
|
1195
|
+
if (sp_rodit && Object.keys(sp_rodit).length === 0) {
|
|
1196
|
+
logger.warn("RODiT lookup returned empty object - RODiT likely does not exist on blockchain", {
|
|
1197
|
+
component: "JwtAuth",
|
|
1198
|
+
method: "validate_jwt_token_be",
|
|
1199
|
+
requestId,
|
|
1200
|
+
roditId: unverifiedpayload.rodit_id,
|
|
1201
|
+
nearContractId: config.get("NEAR_CONTRACT_ID"),
|
|
1202
|
+
nearRpcUrl: config.get("NEAR_RPC_URL"),
|
|
1203
|
+
suggestion: "Verify RODiT ID exists on the specified NEAR contract"
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
if (!sp_rodit || !sp_rodit.token_id) {
|
|
1208
|
+
const errorDetails = {
|
|
1209
|
+
component: "JwtAuth",
|
|
1210
|
+
method: "validate_jwt_token_be",
|
|
1211
|
+
requestId,
|
|
1212
|
+
roditId: unverifiedpayload.rodit_id,
|
|
1213
|
+
duration: Date.now() - startTime,
|
|
1214
|
+
hasSpRodit: !!sp_rodit,
|
|
1215
|
+
spRoditKeys: sp_rodit ? Object.keys(sp_rodit) : [],
|
|
1216
|
+
spRoditOwnerId: sp_rodit?.owner_id || null,
|
|
1217
|
+
spRoditTokenId: sp_rodit?.token_id || null,
|
|
1218
|
+
nearContractId: config.get("NEAR_CONTRACT_ID"),
|
|
1219
|
+
nearRpcUrl: config.get("NEAR_RPC_URL"),
|
|
1220
|
+
tokenPayload: {
|
|
1221
|
+
aud: unverifiedpayload.aud,
|
|
1222
|
+
iss: unverifiedpayload.iss,
|
|
1223
|
+
sub: unverifiedpayload.sub,
|
|
1224
|
+
rodit_id: unverifiedpayload.rodit_id
|
|
1225
|
+
},
|
|
1226
|
+
diagnosisInfo: {
|
|
1227
|
+
roditExists: !!sp_rodit,
|
|
1228
|
+
hasTokenId: !!(sp_rodit && sp_rodit.token_id),
|
|
1229
|
+
hasOwnerId: !!(sp_rodit && sp_rodit.owner_id),
|
|
1230
|
+
isEmpty: sp_rodit && Object.keys(sp_rodit).length === 0,
|
|
1231
|
+
possibleCause: !sp_rodit ? "RODiT not found on blockchain" :
|
|
1232
|
+
!sp_rodit.token_id ? "RODiT exists but missing token_id field" :
|
|
1233
|
+
"Unknown validation failure"
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
logger.warn("Token validation failed - Invalid or missing service provider RODiT", errorDetails);
|
|
1238
|
+
|
|
1239
|
+
// Enhanced error message with diagnostic information
|
|
1240
|
+
const diagnosticMessage = `Error 008: Invalid or missing service provider RODiT (ID: ${unverifiedpayload.rodit_id}). ` +
|
|
1241
|
+
`Diagnosis: ${errorDetails.diagnosisInfo.possibleCause}. ` +
|
|
1242
|
+
`Contract: ${config.get("NEAR_CONTRACT_ID")}, Network: ${config.get("NEAR_RPC_URL")}`;
|
|
1243
|
+
|
|
1244
|
+
throw new Error(diagnosticMessage);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
const publicKeyBytes = await nearorg_rpc_fetchpublickeybytes(
|
|
1248
|
+
sp_rodit.owner_id
|
|
1249
|
+
);
|
|
1250
|
+
|
|
1251
|
+
const serviceprovider_base64_public_key =
|
|
1252
|
+
Buffer.from(publicKeyBytes).toString("base64url");
|
|
1253
|
+
|
|
1254
|
+
const sp_public_key = await base64url2jwk_public_key(
|
|
1255
|
+
serviceprovider_base64_public_key
|
|
1256
|
+
);
|
|
1257
|
+
|
|
1258
|
+
const publicKeyDigest = crypto.createHash("sha256").update(serviceprovider_base64_public_key).digest("hex");
|
|
1259
|
+
let payload;
|
|
1260
|
+
// Define jwtVerifyStartTime outside the try block so it's accessible in both try and catch
|
|
1261
|
+
const jwtVerifyStartTime = Date.now();
|
|
1262
|
+
|
|
1263
|
+
const tokenDigest = crypto.createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
1264
|
+
const tokenParts = token.split(".");
|
|
1265
|
+
const tokenSignatureLength = tokenParts[2]?.length || 0;
|
|
1266
|
+
const signatureDigest = tokenParts[2] ? crypto.createHash("sha256").update(tokenParts[2]).digest("hex") : "none";
|
|
1267
|
+
|
|
1268
|
+
try {
|
|
1269
|
+
// Try to verify the token signature
|
|
1270
|
+
const { jwtVerify } = await getJose();
|
|
1271
|
+
|
|
1272
|
+
// Enforce strict compact JWT and canonical base64url encoding before cryptographic verification.
|
|
1273
|
+
// This prevents equivalent textual encodings of the same bytes from being treated as distinct signatures.
|
|
1274
|
+
if (tokenParts.length !== 3) {
|
|
1275
|
+
throw new Error("Invalid JWT compact serialization: expected 3 parts");
|
|
1276
|
+
}
|
|
1277
|
+
if (!isCanonicalBase64Url(tokenParts[0]) || !isCanonicalBase64Url(tokenParts[1]) || !isCanonicalBase64Url(tokenParts[2])) {
|
|
1278
|
+
throw new Error("Invalid JWT encoding: non-canonical base64url segment");
|
|
1279
|
+
}
|
|
1280
|
+
if (Buffer.from(tokenParts[2], "base64url").length !== 64) {
|
|
1281
|
+
throw new Error("Invalid Ed25519 signature length");
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const verifyResult = await jwtVerify(token, sp_public_key, {
|
|
1285
|
+
algorithms: ["EdDSA"],
|
|
1286
|
+
});
|
|
1287
|
+
payload = verifyResult.payload;
|
|
1288
|
+
|
|
1289
|
+
logger.info("JWT signature verified successfully", {
|
|
1290
|
+
requestId,
|
|
1291
|
+
tokenDigest,
|
|
1292
|
+
signatureDigest,
|
|
1293
|
+
jwtVerifyDuration: Date.now() - jwtVerifyStartTime,
|
|
1294
|
+
payloadKeys: payload ? Object.keys(payload) : [],
|
|
1295
|
+
payloadRoditId: payload?.rodit_id,
|
|
1296
|
+
payloadJti: payload?.jti,
|
|
1297
|
+
publicKeyDigest,
|
|
1298
|
+
component: "JwtAuth",
|
|
1299
|
+
method: "validate_jwt_token_be",
|
|
1300
|
+
verificationResult: "SIGNATURE_ACCEPTED"
|
|
1301
|
+
});
|
|
1302
|
+
} catch (jwtError) {
|
|
1303
|
+
// Log all JWT errors with full details
|
|
1304
|
+
logger.warn("JWT verification error caught", {
|
|
1305
|
+
requestId,
|
|
1306
|
+
tokenDigest,
|
|
1307
|
+
signatureDigest,
|
|
1308
|
+
errorName: jwtError.name,
|
|
1309
|
+
errorMessage: jwtError.message,
|
|
1310
|
+
errorCode: jwtError.code,
|
|
1311
|
+
errorStack: jwtError.stack?.substring(0, 500),
|
|
1312
|
+
publicKeyDigest,
|
|
1313
|
+
component: "JwtAuth",
|
|
1314
|
+
method: "validate_jwt_token_be",
|
|
1315
|
+
verificationResult: "SIGNATURE_REJECTED"
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1318
|
+
// Check if this is an expiration error
|
|
1319
|
+
if (jwtError.name === "JWTExpired") {
|
|
1320
|
+
logger.info("JWT token expired, will attempt renewal", {
|
|
1321
|
+
component: "JwtAuth",
|
|
1322
|
+
method: "validate_jwt_token_be",
|
|
1323
|
+
requestId,
|
|
1324
|
+
errorName: jwtError.name,
|
|
1325
|
+
errorMessage: jwtError.message
|
|
1326
|
+
});
|
|
1327
|
+
isExpired = true;
|
|
1328
|
+
payload = unverifiedpayload;
|
|
1329
|
+
|
|
1330
|
+
try {
|
|
1331
|
+
const { jwtVerify: jwtVerifyIgnoreExp } = await getJose();
|
|
1332
|
+
await jwtVerifyIgnoreExp(token, sp_public_key, {
|
|
1333
|
+
algorithms: ["EdDSA"],
|
|
1334
|
+
currentDate: new Date(unverifiedpayload.exp * 1000 - 1000),
|
|
1335
|
+
});
|
|
1336
|
+
} catch (signatureError) {
|
|
1337
|
+
logger.error("Expired token has invalid signature - rejecting", {
|
|
1338
|
+
component: "JwtAuth",
|
|
1339
|
+
method: "validate_jwt_token_be",
|
|
1340
|
+
requestId,
|
|
1341
|
+
tokenDigest,
|
|
1342
|
+
errorName: signatureError.name,
|
|
1343
|
+
errorMessage: signatureError.message,
|
|
1344
|
+
originalError: jwtError.message
|
|
1345
|
+
});
|
|
1346
|
+
throw new Error(`Invalid signature on expired token: ${signatureError.message}`);
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
// For other JWT errors, rethrow
|
|
1350
|
+
logger.error("JWT signature verification failed - rejecting token", {
|
|
1351
|
+
component: "JwtAuth",
|
|
1352
|
+
method: "validate_jwt_token_be",
|
|
1353
|
+
requestId,
|
|
1354
|
+
errorName: jwtError.name,
|
|
1355
|
+
errorMessage: jwtError.message,
|
|
1356
|
+
roditId: unverifiedpayload?.rodit_id
|
|
1357
|
+
});
|
|
1358
|
+
throw jwtError;
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Only log JWT verification success if we didn't hit an error
|
|
1363
|
+
if (!isExpired) {
|
|
1364
|
+
// Signature already verified by jwtVerify.
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// API auth always enforces server session registration; portal/outbound login
|
|
1368
|
+
// passes enforceSessionRegistration: false via RELAXED_SESSION_VALIDATION_OPTIONS.
|
|
1369
|
+
const enforceSessionRegistration =
|
|
1370
|
+
options.enforceSessionRegistration !== false;
|
|
1371
|
+
|
|
1372
|
+
if (enforceSessionRegistration) {
|
|
1373
|
+
const tokenSessionId = payload?.session_id;
|
|
1374
|
+
if (!tokenSessionId || typeof tokenSessionId !== "string") {
|
|
1375
|
+
logger.warn("Token validation failed - Missing session ID in JWT", {
|
|
1376
|
+
component: "JwtAuth",
|
|
1377
|
+
method: "validate_jwt_token_be",
|
|
1378
|
+
requestId,
|
|
1379
|
+
tokenDigest,
|
|
1380
|
+
jti: payload?.jti,
|
|
1381
|
+
rodiTId: payload?.rodit_id,
|
|
1382
|
+
});
|
|
1383
|
+
throw new Error("Error 010: Missing session ID in token");
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const registeredSession = await sessionManager.getSession(tokenSessionId);
|
|
1387
|
+
const sessionNow = Math.floor(Date.now() / 1000);
|
|
1388
|
+
|
|
1389
|
+
if (!registeredSession) {
|
|
1390
|
+
logger.warn("Token validation failed - Unknown session ID", {
|
|
1391
|
+
component: "JwtAuth",
|
|
1392
|
+
method: "validate_jwt_token_be",
|
|
1393
|
+
requestId,
|
|
1394
|
+
tokenDigest,
|
|
1395
|
+
sessionId: tokenSessionId,
|
|
1396
|
+
jti: payload?.jti,
|
|
1397
|
+
roditId: payload?.rodit_id,
|
|
1398
|
+
});
|
|
1399
|
+
throw new Error("Error 011: Unknown session ID");
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
if (registeredSession.status !== "active") {
|
|
1403
|
+
logger.warn("Token validation failed - Session not active", {
|
|
1404
|
+
component: "JwtAuth",
|
|
1405
|
+
method: "validate_jwt_token_be",
|
|
1406
|
+
requestId,
|
|
1407
|
+
tokenDigest,
|
|
1408
|
+
sessionId: tokenSessionId,
|
|
1409
|
+
sessionStatus: registeredSession.status,
|
|
1410
|
+
jti: payload?.jti,
|
|
1411
|
+
roditId: payload?.rodit_id,
|
|
1412
|
+
});
|
|
1413
|
+
throw new Error("Error 012: Session is not active");
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (
|
|
1417
|
+
payload?.session_exp != null &&
|
|
1418
|
+
registeredSession.expiresAt != null &&
|
|
1419
|
+
Number(payload.session_exp) !== Number(registeredSession.expiresAt)
|
|
1420
|
+
) {
|
|
1421
|
+
logger.warn("Token validation failed - session_exp mismatch with storage", {
|
|
1422
|
+
component: "JwtAuth",
|
|
1423
|
+
method: "validate_jwt_token_be",
|
|
1424
|
+
requestId,
|
|
1425
|
+
tokenDigest,
|
|
1426
|
+
sessionId: tokenSessionId,
|
|
1427
|
+
claimSessionExp: payload.session_exp,
|
|
1428
|
+
storageExpiresAt: registeredSession.expiresAt,
|
|
1429
|
+
});
|
|
1430
|
+
throw new Error("Error 014: Session expiration claim does not match registered session");
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if (
|
|
1434
|
+
registeredSession.expiresAt &&
|
|
1435
|
+
Number(registeredSession.expiresAt) <= sessionNow
|
|
1436
|
+
) {
|
|
1437
|
+
logger.warn("Token validation failed - Session expired", {
|
|
1438
|
+
component: "JwtAuth",
|
|
1439
|
+
method: "validate_jwt_token_be",
|
|
1440
|
+
requestId,
|
|
1441
|
+
tokenDigest,
|
|
1442
|
+
sessionId: tokenSessionId,
|
|
1443
|
+
sessionExpiresAt: registeredSession.expiresAt,
|
|
1444
|
+
now: sessionNow,
|
|
1445
|
+
jti: payload?.jti,
|
|
1446
|
+
roditId: payload?.rodit_id,
|
|
1447
|
+
});
|
|
1448
|
+
throw new Error("Error 013: Session has expired");
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
const {
|
|
1453
|
+
resolve_peer_rodit_for_login,
|
|
1454
|
+
verify_peer_rodit,
|
|
1455
|
+
} = require("./authentication");
|
|
1456
|
+
|
|
1457
|
+
const verifyStartTime = Date.now();
|
|
1458
|
+
const roditIdTrimmed = String(unverifiedpayload.rodit_id || "").trim();
|
|
1459
|
+
const peer_rodit_resolved = await resolve_peer_rodit_for_login(
|
|
1460
|
+
roditIdTrimmed,
|
|
1461
|
+
""
|
|
1462
|
+
);
|
|
1463
|
+
let {
|
|
1464
|
+
peer_rodit,
|
|
1465
|
+
goodrodit,
|
|
1466
|
+
failureReason,
|
|
1467
|
+
failureMessage,
|
|
1468
|
+
} = await verify_peer_rodit(
|
|
1469
|
+
peer_rodit_resolved,
|
|
1470
|
+
roditIdTrimmed || unverifiedpayload.rodit_id,
|
|
1471
|
+
unverifiedpayload.iat,
|
|
1472
|
+
unverifiedpayload.rodit_idsignature
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
logger.debug("Verified peer RODiT", {
|
|
1476
|
+
requestId,
|
|
1477
|
+
verifyPeerDuration: Date.now() - verifyStartTime,
|
|
1478
|
+
goodRodit: goodrodit,
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
if (!goodrodit) {
|
|
1482
|
+
logger.warn("Token validation failed - Invalid peer RODiT", {
|
|
1483
|
+
component: "JwtAuth",
|
|
1484
|
+
method: "validate_jwt_token_be",
|
|
1485
|
+
requestId,
|
|
1486
|
+
roditId: payload.rodit_id,
|
|
1487
|
+
duration: Date.now() - startTime,
|
|
1488
|
+
failureReason,
|
|
1489
|
+
failureMessage
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
const error = new Error("Error 009: Invalid peer RODiT verification");
|
|
1493
|
+
error.code = failureReason || "INVALID_PEER_RODIT";
|
|
1494
|
+
error.failureReason = failureReason;
|
|
1495
|
+
error.failureMessage = failureMessage;
|
|
1496
|
+
throw error;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const now = Math.floor(Date.now() / 1000);
|
|
1500
|
+
if (!isExpired && payload.exp <= now) {
|
|
1501
|
+
logger.error("Token validation failed - Token expired", {
|
|
1502
|
+
component: "JwtAuth",
|
|
1503
|
+
requestId,
|
|
1504
|
+
exp: payload.exp,
|
|
1505
|
+
now,
|
|
1506
|
+
difference: now - payload.exp,
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
isExpired = true;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// Token not-before check
|
|
1513
|
+
if (payload.nbf > now) {
|
|
1514
|
+
logger.warn("Token validation failed - Token not yet valid", {
|
|
1515
|
+
component: "JwtAuth",
|
|
1516
|
+
requestId,
|
|
1517
|
+
nbf: payload.nbf,
|
|
1518
|
+
now,
|
|
1519
|
+
difference: payload.nbf - now,
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
throw new Error("Error 006: Token is not yet valid");
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// Function to normalize URL by removing port
|
|
1526
|
+
const normalizeUrlWithoutPort = (url) => {
|
|
1527
|
+
if (!url) return '';
|
|
1528
|
+
try {
|
|
1529
|
+
// Use URL constructor to parse the URL
|
|
1530
|
+
const parsedUrl = new URL(url);
|
|
1531
|
+
// Remove the port
|
|
1532
|
+
parsedUrl.port = '';
|
|
1533
|
+
// Return the normalized URL as a string
|
|
1534
|
+
return parsedUrl.toString();
|
|
1535
|
+
} catch (e) {
|
|
1536
|
+
// If URL parsing fails, return the original URL
|
|
1537
|
+
return url;
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
|
|
1541
|
+
// Normalize both URLs for comparison
|
|
1542
|
+
const normalizedTokenIssuer = normalizeUrlWithoutPort(payload.iss);
|
|
1543
|
+
const normalizedExpectedIssuer = normalizeUrlWithoutPort(rodit.metadata.subjectuniqueidentifier_url);
|
|
1544
|
+
|
|
1545
|
+
// Issuer check with enhanced logging
|
|
1546
|
+
logger.debug("Detailed issuer validation information", {
|
|
1547
|
+
component: "JwtAuth",
|
|
1548
|
+
method: "validate_jwt_token_be",
|
|
1549
|
+
requestId,
|
|
1550
|
+
tokenIssuer: payload.iss,
|
|
1551
|
+
expectedIssuer: rodit.metadata.subjectuniqueidentifier_url,
|
|
1552
|
+
normalizedTokenIssuer,
|
|
1553
|
+
normalizedExpectedIssuer,
|
|
1554
|
+
roditId: rodit.token_id,
|
|
1555
|
+
roditOwnerId: rodit.owner_id,
|
|
1556
|
+
hasMetadata: !!rodit.metadata,
|
|
1557
|
+
metadataKeys: rodit.metadata ? Object.keys(rodit.metadata) : [],
|
|
1558
|
+
payloadKeys: Object.keys(payload),
|
|
1559
|
+
rawIssuerMatch: payload.iss === rodit.metadata.subjectuniqueidentifier_url,
|
|
1560
|
+
normalizedIssuerMatch: normalizedTokenIssuer === normalizedExpectedIssuer
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
// Compare normalized URLs instead of raw URLs
|
|
1564
|
+
if (normalizedTokenIssuer !== normalizedExpectedIssuer) {
|
|
1565
|
+
logger.warn("Token validation failed - Invalid issuer", {
|
|
1566
|
+
component: "JwtAuth",
|
|
1567
|
+
method: "validate_jwt_token_be",
|
|
1568
|
+
requestId,
|
|
1569
|
+
tokenIssuer: payload.iss,
|
|
1570
|
+
expectedIssuer: rodit.metadata.subjectuniqueidentifier_url,
|
|
1571
|
+
normalizedTokenIssuer,
|
|
1572
|
+
normalizedExpectedIssuer,
|
|
1573
|
+
roditId: rodit.token_id,
|
|
1574
|
+
// Check for common URL variations that might cause mismatch
|
|
1575
|
+
issuerHasTrailingSlash: payload.iss?.endsWith('/'),
|
|
1576
|
+
expectedHasTrailingSlash: rodit.metadata.subjectuniqueidentifier_url?.endsWith('/'),
|
|
1577
|
+
issuerHasProtocol: payload.iss?.startsWith('http'),
|
|
1578
|
+
expectedHasProtocol: rodit.metadata.subjectuniqueidentifier_url?.startsWith('http')
|
|
1579
|
+
});
|
|
1580
|
+
|
|
1581
|
+
throw new Error("Error 005: Invalid issuer");
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// Check if this might be a peer-to-peer authentication attempt
|
|
1585
|
+
const isPossiblePeerAuth =
|
|
1586
|
+
(payload.auth_mode === 'peer-to-peer') ||
|
|
1587
|
+
(payload.auth_context && payload.auth_context.mode === 'peer-to-peer') ||
|
|
1588
|
+
payload.aud.startsWith('peer:') ||
|
|
1589
|
+
/^[a-zA-Z0-9]{1,64}\.[a-zA-Z0-9]{1,64}$/.test(payload.aud) ||
|
|
1590
|
+
/^[a-z0-9_-]{2,64}(\.near)?$/.test(payload.aud);
|
|
1591
|
+
|
|
1592
|
+
if (payload.aud !== rodit.owner_id) {
|
|
1593
|
+
logger.warn("Token validation failed - Invalid audience", {
|
|
1594
|
+
component: "JwtAuth",
|
|
1595
|
+
method: "validate_jwt_token_be",
|
|
1596
|
+
requestId,
|
|
1597
|
+
tokenAudience: payload.aud,
|
|
1598
|
+
expectedAudience: rodit.owner_id,
|
|
1599
|
+
isPossiblePeerAuth
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
throw new Error("Error 004: Invalid audience");
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const totalDuration = Date.now() - startTime;
|
|
1606
|
+
|
|
1607
|
+
logger.info("JWT token validation successful", {
|
|
1608
|
+
component: "JwtAuth",
|
|
1609
|
+
method: "validate_jwt_token_be",
|
|
1610
|
+
requestId,
|
|
1611
|
+
duration: totalDuration,
|
|
1612
|
+
jti: payload.jti,
|
|
1613
|
+
roditId: payload.rodit_id,
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
// Add metric for successful validations
|
|
1617
|
+
logger.metric &&
|
|
1618
|
+
logger.metric("jwt_token_validation", totalDuration, {
|
|
1619
|
+
result: "success",
|
|
1620
|
+
rodit_id: payload.rodit_id,
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
// Extract user data from payload for middleware
|
|
1624
|
+
const user = {
|
|
1625
|
+
id: payload.sub,
|
|
1626
|
+
roditId: payload.rodit_id,
|
|
1627
|
+
ownerId: payload.rodit_owner,
|
|
1628
|
+
session: {
|
|
1629
|
+
id: payload.session_id,
|
|
1630
|
+
status: payload.session_status,
|
|
1631
|
+
createdAt: payload.session_iat
|
|
1632
|
+
? new Date(payload.session_iat * 1000).toISOString()
|
|
1633
|
+
: "unknown",
|
|
1634
|
+
expiresAt: payload.session_exp
|
|
1635
|
+
? new Date(payload.session_exp * 1000).toISOString()
|
|
1636
|
+
: "unknown",
|
|
1637
|
+
},
|
|
1638
|
+
permissions: {
|
|
1639
|
+
maxRequests: payload.rodit_maxrequests,
|
|
1640
|
+
maxRequestWindow: payload.rodit_maxrqwindow,
|
|
1641
|
+
permissionedRoutes: payload.rodit_permissionedroutes,
|
|
1642
|
+
allowedCidr: payload.rodit_allowedcidr,
|
|
1643
|
+
allowedIso3166List: payload.rodit_allowediso3166list
|
|
1644
|
+
},
|
|
1645
|
+
webhookUrl: payload.rodit_webhookurl
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1648
|
+
let newToken = null;
|
|
1649
|
+
if (!options.allowExpiredToken) {
|
|
1650
|
+
// Check if token needs renewal or is expired
|
|
1651
|
+
const renewalResult = await checkandrenew_jwt_token(
|
|
1652
|
+
payload,
|
|
1653
|
+
Math.floor(Date.now() / 1000),
|
|
1654
|
+
requestId,
|
|
1655
|
+
isExpired
|
|
1656
|
+
);
|
|
1657
|
+
newToken = renewalResult.newToken;
|
|
1658
|
+
|
|
1659
|
+
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");
|
|
1668
|
+
}
|
|
1669
|
+
} else if (isExpired) {
|
|
1670
|
+
logger.info("Allowing signature-valid expired token for special flow", {
|
|
1671
|
+
component: "JwtAuth",
|
|
1672
|
+
method: "validate_jwt_token_be",
|
|
1673
|
+
requestId,
|
|
1674
|
+
jti: payload.jti,
|
|
1675
|
+
reason: "allowExpiredToken option enabled"
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return {
|
|
1680
|
+
payload,
|
|
1681
|
+
peer_rodit,
|
|
1682
|
+
valid: true, // This matches what authenticate_apicall expects
|
|
1683
|
+
user,
|
|
1684
|
+
newToken
|
|
1685
|
+
};
|
|
1686
|
+
} catch (error) {
|
|
1687
|
+
const duration = Date.now() - startTime;
|
|
1688
|
+
|
|
1689
|
+
logger.error("JWT token validation failed", {
|
|
1690
|
+
component: "JwtAuth",
|
|
1691
|
+
method: "validate_jwt_token_be",
|
|
1692
|
+
requestId,
|
|
1693
|
+
duration,
|
|
1694
|
+
errorCode: error.code,
|
|
1695
|
+
error: {
|
|
1696
|
+
message: error.message,
|
|
1697
|
+
stack: error.stack,
|
|
1698
|
+
name: error.name,
|
|
1699
|
+
},
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
// Add metrics for validation errors
|
|
1703
|
+
logger.metric &&
|
|
1704
|
+
logger.metric("jwt_token_validation_errors", 1, {
|
|
1705
|
+
error_type: error.name || "Unknown",
|
|
1706
|
+
error_code: error.code || "none",
|
|
1707
|
+
});
|
|
1708
|
+
|
|
1709
|
+
logger.metric &&
|
|
1710
|
+
logger.metric("jwt_token_validation", duration, {
|
|
1711
|
+
result: "failure",
|
|
1712
|
+
error_type: error.name || "Unknown",
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
throw new Error(`JWT token validation failed: ${error.message}`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
/**
|
|
1720
|
+
* Brief validation of a JWT token
|
|
1721
|
+
*
|
|
1722
|
+
* @param {Object} token - Token payload
|
|
1723
|
+
* @returns {Promise<Object>} Validation result
|
|
1724
|
+
*/
|
|
1725
|
+
async function brief_validate_jwt_token_be(token) {
|
|
1726
|
+
const requestId = ulid();
|
|
1727
|
+
const startTime = Date.now();
|
|
1728
|
+
|
|
1729
|
+
try {
|
|
1730
|
+
const tokenFetchStart = Date.now();
|
|
1731
|
+
const peer_rodit =
|
|
1732
|
+
await nearorg_rpc_tokensfromaccountid(
|
|
1733
|
+
|
|
1734
|
+
token.aud
|
|
1735
|
+
);
|
|
1736
|
+
const tokenFetchDuration = Date.now() - tokenFetchStart;
|
|
1737
|
+
|
|
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;
|
|
1744
|
+
|
|
1745
|
+
const totalDuration = Date.now() - startTime;
|
|
1746
|
+
|
|
1747
|
+
if (isValid) {
|
|
1748
|
+
logger.info("Brief token validation successful", {
|
|
1749
|
+
component: "JwtAuth",
|
|
1750
|
+
method: "brief_validate_jwt_token_be",
|
|
1751
|
+
requestId,
|
|
1752
|
+
duration: totalDuration,
|
|
1753
|
+
tokenFetchDuration,
|
|
1754
|
+
tokenJti: token.jti,
|
|
1755
|
+
peerRoditId: peer_rodit.token_id,
|
|
1756
|
+
notAfter: peer_rodit.metadata.not_after,
|
|
1757
|
+
});
|
|
1758
|
+
|
|
1759
|
+
// Add metrics for successful brief validations
|
|
1760
|
+
logger.metric("jwt_brief_validation", totalDuration, {
|
|
1761
|
+
result: "success",
|
|
1762
|
+
token_jti: token.jti || "unknown",
|
|
1763
|
+
});
|
|
1764
|
+
} else {
|
|
1765
|
+
logger.warn("Brief token validation failed", {
|
|
1766
|
+
component: "JwtAuth",
|
|
1767
|
+
method: "brief_validate_jwt_token_be",
|
|
1768
|
+
requestId,
|
|
1769
|
+
duration: totalDuration,
|
|
1770
|
+
tokenFetchDuration,
|
|
1771
|
+
tokenJti: token.jti,
|
|
1772
|
+
peerRoditId: peer_rodit.token_id,
|
|
1773
|
+
extractedSub,
|
|
1774
|
+
tokenAud: token.aud,
|
|
1775
|
+
peerRoditOwnerId: peer_rodit.owner_id,
|
|
1776
|
+
idMatch: peer_rodit.token_id === extractedSub,
|
|
1777
|
+
ownerMatch: peer_rodit.owner_id === token.aud,
|
|
1778
|
+
});
|
|
1779
|
+
|
|
1780
|
+
// Add metrics for failed brief validations
|
|
1781
|
+
logger.metric("jwt_brief_validation", totalDuration, {
|
|
1782
|
+
result: "failure",
|
|
1783
|
+
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",
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
return {
|
|
1790
|
+
isValid,
|
|
1791
|
+
notAfter: peer_rodit.metadata.not_after,
|
|
1792
|
+
};
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
const duration = Date.now() - startTime;
|
|
1795
|
+
|
|
1796
|
+
logger.error("Brief token validation failed with error", {
|
|
1797
|
+
component: "JwtAuth",
|
|
1798
|
+
method: "brief_validate_jwt_token_be",
|
|
1799
|
+
requestId,
|
|
1800
|
+
duration,
|
|
1801
|
+
tokenAud: token?.aud,
|
|
1802
|
+
tokenJti: token?.jti,
|
|
1803
|
+
error: {
|
|
1804
|
+
message: error.message,
|
|
1805
|
+
stack: error.stack,
|
|
1806
|
+
name: error.name,
|
|
1807
|
+
},
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
// Add metrics for brief validation errors
|
|
1811
|
+
logger.metric("jwt_brief_validation_errors", 1, {
|
|
1812
|
+
error_type: error.name || "Unknown",
|
|
1813
|
+
token_jti: token.jti || "unknown",
|
|
1814
|
+
});
|
|
1815
|
+
|
|
1816
|
+
return {
|
|
1817
|
+
isValid: false,
|
|
1818
|
+
notAfter: null,
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/**
|
|
1824
|
+
* Thoroughly validates a JWT token by verifying the associated RODiT
|
|
1825
|
+
* Uses a comprehensive verification process with detailed error handling and metrics
|
|
1826
|
+
* NOTE: Not checking if the sessions is closed or expired yet.
|
|
1827
|
+
* @param {Object} token - The JWT token to validate
|
|
1828
|
+
* @returns {Object} - Validation result with isValid flag, notAfter timestamp, and optional verification details
|
|
1829
|
+
*/
|
|
1830
|
+
async function thorough_validate_jwt_token_be(token, requestId = ulid()) {
|
|
1831
|
+
const startTime = performance.now(); // More precise timing measurement
|
|
1832
|
+
|
|
1833
|
+
try {
|
|
1834
|
+
// Fetch configuration with better timing measurements
|
|
1835
|
+
const configStart = performance.now();
|
|
1836
|
+
const config_own_rodit = await stateManager.getConfigOwnRodit();
|
|
1837
|
+
const configDuration = performance.now() - configStart;
|
|
1838
|
+
|
|
1839
|
+
// Fetch peer RODiT with clearer logging
|
|
1840
|
+
const tokenFetchStart = performance.now();
|
|
1841
|
+
const peer_rodit = await nearorg_rpc_tokenfromroditid(token.rodit_id);
|
|
1842
|
+
const tokenFetchDuration = performance.now() - tokenFetchStart;
|
|
1843
|
+
|
|
1844
|
+
if (!peer_rodit) {
|
|
1845
|
+
logger.error("Failed to retrieve peer RODiT data", {
|
|
1846
|
+
component: "JwtAuth",
|
|
1847
|
+
requestId,
|
|
1848
|
+
duration: performance.now() - startTime,
|
|
1849
|
+
tokenRoditId: token?.rodit_id,
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
// Add metrics for failed token fetch
|
|
1853
|
+
logger.metric &&
|
|
1854
|
+
logger.metric("jwt_thorough_validation", performance.now() - startTime, {
|
|
1855
|
+
result: "rodit_fetch_failed",
|
|
1856
|
+
token_jti: token.jti || "unknown",
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
return {
|
|
1860
|
+
isValid: false,
|
|
1861
|
+
notAfter: null,
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
if (!peer_rodit.metadata) {
|
|
1866
|
+
logger.error("Peer RODiT missing metadata", {
|
|
1867
|
+
component: "JwtAuth",
|
|
1868
|
+
requestId,
|
|
1869
|
+
duration: performance.now() - startTime,
|
|
1870
|
+
tokenRoditId: token?.rodit_id,
|
|
1871
|
+
peerRoditId: peer_rodit.token_id,
|
|
1872
|
+
peerRoditOwnerId: peer_rodit.owner_id,
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
// Add metrics for missing metadata
|
|
1876
|
+
logger.metric &&
|
|
1877
|
+
logger.metric("jwt_thorough_validation", performance.now() - startTime, {
|
|
1878
|
+
result: "missing_metadata",
|
|
1879
|
+
token_jti: token.jti || "unknown",
|
|
1880
|
+
peer_rodit_id: peer_rodit.token_id,
|
|
1881
|
+
});
|
|
1882
|
+
|
|
1883
|
+
return {
|
|
1884
|
+
isValid: false,
|
|
1885
|
+
notAfter: null,
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// Starting verification with more detailed logging
|
|
1890
|
+
logger.debug("Starting verification checks", {
|
|
1891
|
+
requestId,
|
|
1892
|
+
checks: ["match", "live", "active", "trusted"],
|
|
1893
|
+
serviceProviderId: config_own_rodit.own_rodit.metadata.serviceprovider_id,
|
|
1894
|
+
});
|
|
1895
|
+
|
|
1896
|
+
// Import verification functions dynamically to avoid circular dependencies
|
|
1897
|
+
const {
|
|
1898
|
+
verify_rodit_isamatch,
|
|
1899
|
+
verify_rodit_islive,
|
|
1900
|
+
verify_rodit_isactive,
|
|
1901
|
+
verify_rodit_istrusted_issuingsmartcontract
|
|
1902
|
+
} = require("./authentication");
|
|
1903
|
+
|
|
1904
|
+
// Perform match verification
|
|
1905
|
+
const matchStart = performance.now();
|
|
1906
|
+
const matchResult = await verify_rodit_isamatch(
|
|
1907
|
+
config_own_rodit.own_rodit.metadata.serviceprovider_id,
|
|
1908
|
+
peer_rodit
|
|
1909
|
+
);
|
|
1910
|
+
const matchDuration = performance.now() - matchStart;
|
|
1911
|
+
|
|
1912
|
+
logger.debug("Match verification completed", {
|
|
1913
|
+
requestId,
|
|
1914
|
+
matchDuration,
|
|
1915
|
+
isMatch: matchResult.isMatch,
|
|
1916
|
+
verificationType: matchResult.verificationType,
|
|
1917
|
+
failureReason: matchResult.failureReason,
|
|
1918
|
+
serviceProviderId: config_own_rodit.own_rodit.metadata.serviceprovider_id,
|
|
1919
|
+
peerServiceProviderId: peer_rodit.metadata.serviceprovider_id,
|
|
1920
|
+
});
|
|
1921
|
+
|
|
1922
|
+
if (!matchResult.isMatch) {
|
|
1923
|
+
logger.warn("RODiT match verification failed", {
|
|
1924
|
+
component: "JwtAuth",
|
|
1925
|
+
method: "thorough_validate_jwt_token_be",
|
|
1926
|
+
requestId,
|
|
1927
|
+
duration: performance.now() - startTime,
|
|
1928
|
+
failureReason: matchResult.failureReason,
|
|
1929
|
+
failureMessage: matchResult.failureMessage,
|
|
1930
|
+
serviceProviderId: config_own_rodit.own_rodit.metadata.serviceprovider_id,
|
|
1931
|
+
peerServiceProviderId: peer_rodit.metadata.serviceprovider_id,
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
// Add metrics for failed match verification
|
|
1935
|
+
logger.metric &&
|
|
1936
|
+
logger.metric("jwt_thorough_validation", performance.now() - startTime, {
|
|
1937
|
+
result: "match_failed",
|
|
1938
|
+
failure_reason: matchResult.failureReason,
|
|
1939
|
+
token_jti: token.jti || "unknown",
|
|
1940
|
+
peer_rodit_id: peer_rodit.token_id,
|
|
1941
|
+
});
|
|
1942
|
+
|
|
1943
|
+
return {
|
|
1944
|
+
isValid: false,
|
|
1945
|
+
notAfter: null,
|
|
1946
|
+
error: "RODiT match verification failed",
|
|
1947
|
+
errorCode: matchResult.failureReason || "SERVER_RODIT_FAMILY_MISMATCH",
|
|
1948
|
+
errorMessage: matchResult.failureMessage || "Server's RODiT does not belong to the same family as the client"
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Perform live verification
|
|
1953
|
+
const liveStart = performance.now();
|
|
1954
|
+
const isLive = await verify_rodit_islive(
|
|
1955
|
+
peer_rodit.metadata.not_after,
|
|
1956
|
+
peer_rodit.metadata.not_before
|
|
1957
|
+
);
|
|
1958
|
+
const liveDuration = performance.now() - liveStart;
|
|
1959
|
+
|
|
1960
|
+
logger.debug("Live verification completed", {
|
|
1961
|
+
requestId,
|
|
1962
|
+
liveDuration,
|
|
1963
|
+
isLive,
|
|
1964
|
+
notAfter: peer_rodit.metadata.not_after,
|
|
1965
|
+
notBefore: peer_rodit.metadata.not_before,
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
if (!isLive) {
|
|
1969
|
+
logger.warn("RODiT live verification failed", {
|
|
1970
|
+
component: "JwtAuth",
|
|
1971
|
+
method: "thorough_validate_jwt_token_be",
|
|
1972
|
+
requestId,
|
|
1973
|
+
duration: performance.now() - startTime,
|
|
1974
|
+
notAfter: peer_rodit.metadata.not_after,
|
|
1975
|
+
notBefore: peer_rodit.metadata.not_before,
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
// Add metrics for failed live verification
|
|
1979
|
+
logger.metric &&
|
|
1980
|
+
logger.metric("jwt_thorough_validation", performance.now() - startTime, {
|
|
1981
|
+
result: "live_failed",
|
|
1982
|
+
token_jti: token.jti || "unknown",
|
|
1983
|
+
peer_rodit_id: peer_rodit.token_id,
|
|
1984
|
+
});
|
|
1985
|
+
|
|
1986
|
+
return {
|
|
1987
|
+
isValid: false,
|
|
1988
|
+
notAfter: null,
|
|
1989
|
+
error: "RODiT live verification failed",
|
|
1990
|
+
errorCode: "SERVER_RODIT_NOT_LIVE",
|
|
1991
|
+
errorMessage: "Server's RODiT is expired or not yet valid"
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
// Perform active verification
|
|
1996
|
+
const activeStart = performance.now();
|
|
1997
|
+
const isActive = await verify_rodit_isactive(
|
|
1998
|
+
peer_rodit.token_id,
|
|
1999
|
+
config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url
|
|
2000
|
+
);
|
|
2001
|
+
const activeDuration = performance.now() - activeStart;
|
|
2002
|
+
|
|
2003
|
+
logger.debug("Active verification completed", {
|
|
2004
|
+
requestId,
|
|
2005
|
+
activeDuration,
|
|
2006
|
+
isActive,
|
|
2007
|
+
tokenId: peer_rodit.token_id,
|
|
2008
|
+
url: config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url,
|
|
2009
|
+
});
|
|
2010
|
+
|
|
2011
|
+
if (!isActive) {
|
|
2012
|
+
logger.warn("RODiT active verification failed", {
|
|
2013
|
+
component: "JwtAuth",
|
|
2014
|
+
method: "thorough_validate_jwt_token_be",
|
|
2015
|
+
requestId,
|
|
2016
|
+
duration: performance.now() - startTime,
|
|
2017
|
+
tokenId: peer_rodit.token_id,
|
|
2018
|
+
url: config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url,
|
|
2019
|
+
});
|
|
2020
|
+
|
|
2021
|
+
// Add metrics for failed active verification
|
|
2022
|
+
logger.metric &&
|
|
2023
|
+
logger.metric("jwt_thorough_validation", performance.now() - startTime, {
|
|
2024
|
+
result: "active_failed",
|
|
2025
|
+
token_jti: token.jti || "unknown",
|
|
2026
|
+
peer_rodit_id: peer_rodit.token_id,
|
|
2027
|
+
});
|
|
2028
|
+
|
|
2029
|
+
return {
|
|
2030
|
+
isValid: false,
|
|
2031
|
+
notAfter: null,
|
|
2032
|
+
error: "RODiT active verification failed",
|
|
2033
|
+
errorCode: "SERVER_RODIT_REVOKED",
|
|
2034
|
+
errorMessage: "Server's RODiT has been revoked"
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
// Perform trusted verification
|
|
2039
|
+
const trustedStart = performance.now();
|
|
2040
|
+
const isTrusted = await verify_rodit_istrusted_issuingsmartcontract(
|
|
2041
|
+
config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url
|
|
2042
|
+
);
|
|
2043
|
+
const trustedDuration = performance.now() - trustedStart;
|
|
2044
|
+
|
|
2045
|
+
logger.debug("Trust verification completed", {
|
|
2046
|
+
requestId,
|
|
2047
|
+
trustedDuration,
|
|
2048
|
+
isTrusted,
|
|
2049
|
+
url: config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url,
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
if (!isTrusted) {
|
|
2053
|
+
logger.warn("RODiT trust verification failed", {
|
|
2054
|
+
component: "JwtAuth",
|
|
2055
|
+
method: "thorough_validate_jwt_token_be",
|
|
2056
|
+
requestId,
|
|
2057
|
+
duration: performance.now() - startTime,
|
|
2058
|
+
url: config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url,
|
|
2059
|
+
});
|
|
2060
|
+
|
|
2061
|
+
// Add metrics for failed trust verification
|
|
2062
|
+
logger.metric &&
|
|
2063
|
+
logger.metric("jwt_thorough_validation", performance.now() - startTime, {
|
|
2064
|
+
result: "trust_failed",
|
|
2065
|
+
token_jti: token.jti || "unknown",
|
|
2066
|
+
peer_rodit_id: peer_rodit.token_id,
|
|
2067
|
+
});
|
|
2068
|
+
|
|
2069
|
+
return {
|
|
2070
|
+
isValid: false,
|
|
2071
|
+
notAfter: null,
|
|
2072
|
+
error: "RODiT trust verification failed",
|
|
2073
|
+
errorCode: "SERVER_SMART_CONTRACT_NOT_TRUSTED",
|
|
2074
|
+
errorMessage: "Server's issuing smart contract is not trusted by this client"
|
|
2075
|
+
};
|
|
2076
|
+
}
|
|
2077
|
+
|
|
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", {
|
|
2083
|
+
requestId,
|
|
2084
|
+
extractedSub,
|
|
2085
|
+
tokenSub: token.sub,
|
|
2086
|
+
peerRoditId: peer_rodit.token_id,
|
|
2087
|
+
peerRoditOwnerId: peer_rodit.owner_id,
|
|
2088
|
+
tokenAud: token.aud,
|
|
2089
|
+
});
|
|
2090
|
+
|
|
2091
|
+
// Additional identity checks
|
|
2092
|
+
const idMatch = peer_rodit.token_id === extractedSub;
|
|
2093
|
+
const ownerMatch = peer_rodit.owner_id === token.aud;
|
|
2094
|
+
const isValid = idMatch && ownerMatch;
|
|
2095
|
+
|
|
2096
|
+
const totalDuration = performance.now() - startTime;
|
|
2097
|
+
|
|
2098
|
+
if (isValid) {
|
|
2099
|
+
logger.info("Thorough token validation successful", {
|
|
2100
|
+
component: "JwtAuth",
|
|
2101
|
+
method: "thorough_validate_jwt_token_be",
|
|
2102
|
+
requestId,
|
|
2103
|
+
duration: totalDuration,
|
|
2104
|
+
tokenJti: token.jti,
|
|
2105
|
+
peerRoditId: peer_rodit.token_id,
|
|
2106
|
+
notAfter: peer_rodit.metadata.not_after,
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
// Add metrics for successful thorough validations
|
|
2110
|
+
logger.metric &&
|
|
2111
|
+
logger.metric("jwt_thorough_validation", totalDuration, {
|
|
2112
|
+
result: "success",
|
|
2113
|
+
token_jti: token.jti || "unknown",
|
|
2114
|
+
peer_rodit_id: peer_rodit.token_id,
|
|
2115
|
+
});
|
|
2116
|
+
} else {
|
|
2117
|
+
const failedIdentityChecks = [];
|
|
2118
|
+
if (!idMatch) failedIdentityChecks.push("token_id_mismatch");
|
|
2119
|
+
if (!ownerMatch) failedIdentityChecks.push("owner_id_mismatch");
|
|
2120
|
+
|
|
2121
|
+
logger.warn("Token identity verification failed", {
|
|
2122
|
+
component: "JwtAuth",
|
|
2123
|
+
method: "thorough_validate_jwt_token_be",
|
|
2124
|
+
requestId,
|
|
2125
|
+
duration: totalDuration,
|
|
2126
|
+
tokenJti: token.jti,
|
|
2127
|
+
extractedSub,
|
|
2128
|
+
peerRoditId: peer_rodit.token_id,
|
|
2129
|
+
tokenAud: token.aud,
|
|
2130
|
+
peerRoditOwnerId: peer_rodit.owner_id,
|
|
2131
|
+
idMatch,
|
|
2132
|
+
ownerMatch,
|
|
2133
|
+
failedIdentityChecks,
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
// Add metrics for identity mismatch with more details
|
|
2137
|
+
logger.metric &&
|
|
2138
|
+
logger.metric("jwt_thorough_validation", totalDuration, {
|
|
2139
|
+
result: "identity_mismatch",
|
|
2140
|
+
token_jti: token.jti || "unknown",
|
|
2141
|
+
id_match: idMatch ? "true" : "false",
|
|
2142
|
+
owner_match: ownerMatch ? "true" : "false",
|
|
2143
|
+
failed_checks: failedIdentityChecks.join(","),
|
|
2144
|
+
peer_rodit_id: peer_rodit.token_id,
|
|
2145
|
+
});
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
return {
|
|
2149
|
+
isValid,
|
|
2150
|
+
notAfter: peer_rodit.metadata.not_after,
|
|
2151
|
+
error: !isValid ? "Token identity verification failed" : undefined,
|
|
2152
|
+
errorCode: !isValid ? "SERVER_TOKEN_IDENTITY_MISMATCH" : undefined,
|
|
2153
|
+
errorMessage: !isValid ? `Server token identity mismatch: ${failedIdentityChecks.join(", ")}` : undefined
|
|
2154
|
+
};
|
|
2155
|
+
} catch (error) {
|
|
2156
|
+
const duration = performance.now() - startTime;
|
|
2157
|
+
|
|
2158
|
+
logger.error("Thorough token validation failed with error", {
|
|
2159
|
+
component: "JwtAuth",
|
|
2160
|
+
method: "thorough_validate_jwt_token_be",
|
|
2161
|
+
requestId,
|
|
2162
|
+
duration,
|
|
2163
|
+
tokenRoditId: token?.rodit_id,
|
|
2164
|
+
tokenJti: token?.jti,
|
|
2165
|
+
error: {
|
|
2166
|
+
message: error.message,
|
|
2167
|
+
stack: error.stack,
|
|
2168
|
+
name: error.name,
|
|
2169
|
+
code: error.code || 'unknown',
|
|
2170
|
+
},
|
|
2171
|
+
});
|
|
2172
|
+
|
|
2173
|
+
// Add more detailed metrics for thorough validation errors
|
|
2174
|
+
logger.metric &&
|
|
2175
|
+
logger.metric("jwt_thorough_validation", duration, {
|
|
2176
|
+
result: "error",
|
|
2177
|
+
error_type: error.name || "Unknown",
|
|
2178
|
+
error_code: error.code || "unknown",
|
|
2179
|
+
token_jti: token?.jti || "unknown",
|
|
2180
|
+
});
|
|
2181
|
+
|
|
2182
|
+
return {
|
|
2183
|
+
isValid: false,
|
|
2184
|
+
notAfter: null,
|
|
2185
|
+
error: error.message,
|
|
2186
|
+
};
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* Check if a token needs renewal and renew if necessary
|
|
2192
|
+
*
|
|
2193
|
+
* @param {Object} payload - Token payload
|
|
2194
|
+
* @param {number} timestamp - Current timestamp
|
|
2195
|
+
* @param {string} requestId - Request ID for tracking
|
|
2196
|
+
* @returns {Promise<Object>} Renewal result with new token if renewed
|
|
2197
|
+
*/
|
|
2198
|
+
async function checkandrenew_jwt_token(payload, timestamp, requestId, forceRenewal = false) {
|
|
2199
|
+
const startTime = Date.now();
|
|
2200
|
+
const config_own_rodit = await stateManager.getConfigOwnRodit();
|
|
2201
|
+
|
|
2202
|
+
// Get token renewal configuration from SDK config (infrastructure settings)
|
|
2203
|
+
const config = require('../../services/configsdk');
|
|
2204
|
+
const LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY = parseFloat(
|
|
2205
|
+
config.get('SECURITY_OPTIONS.LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY', '0.80')
|
|
2206
|
+
);
|
|
2207
|
+
const THRESHOLD_VALIDATION_TYPE = parseFloat(
|
|
2208
|
+
config.get('SECURITY_OPTIONS.THRESHOLD_VALIDATION_TYPE', '0.10')
|
|
2209
|
+
);
|
|
2210
|
+
const DURATIONRAMP = parseFloat(
|
|
2211
|
+
config.get('SECURITY_OPTIONS.DURATIONRAMP', '0.85')
|
|
2212
|
+
);
|
|
2213
|
+
|
|
2214
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
2215
|
+
const timeLeft = payload.exp - currentTime;
|
|
2216
|
+
const currentDuration = payload.exp - payload.iat;
|
|
2217
|
+
const durationLeftpct = (timeLeft / currentDuration) * 100;
|
|
2218
|
+
const newduration = currentDuration * DURATIONRAMP;
|
|
2219
|
+
|
|
2220
|
+
// Log session information
|
|
2221
|
+
const sessionInfo = {
|
|
2222
|
+
sessionId: payload.session_id || "none",
|
|
2223
|
+
sessionStatus: payload.session_status || "unknown",
|
|
2224
|
+
sessionCreatedAt: payload.session_iat
|
|
2225
|
+
? new Date(payload.session_iat * 1000).toISOString()
|
|
2226
|
+
: "unknown",
|
|
2227
|
+
sessionAge: payload.session_iat
|
|
2228
|
+
? Math.floor(currentTime - payload.session_iat)
|
|
2229
|
+
: "unknown",
|
|
2230
|
+
};
|
|
2231
|
+
|
|
2232
|
+
// No renewal needed if above threshold and not forced
|
|
2233
|
+
if (
|
|
2234
|
+
!forceRenewal &&
|
|
2235
|
+
durationLeftpct / 100 >=
|
|
2236
|
+
1.0 - LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY
|
|
2237
|
+
) {
|
|
2238
|
+
const renewThresholdPercent = (
|
|
2239
|
+
100 - (LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY * 100)
|
|
2240
|
+
).toFixed(1);
|
|
2241
|
+
const renewThresholdSeconds =
|
|
2242
|
+
currentDuration * (1 - LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY);
|
|
2243
|
+
const secondsUntilEligibility = Math.max(
|
|
2244
|
+
0,
|
|
2245
|
+
timeLeft - renewThresholdSeconds
|
|
2246
|
+
);
|
|
2247
|
+
const eligibilityTimestamp = new Date(
|
|
2248
|
+
(currentTime + secondsUntilEligibility) * 1000
|
|
2249
|
+
).toISOString();
|
|
2250
|
+
|
|
2251
|
+
const duration = Date.now() - startTime;
|
|
2252
|
+
logger.metric("token_renewal_check_duration_ms", duration, {
|
|
2253
|
+
component: "TokenRenewalService",
|
|
2254
|
+
renewalNeeded: false,
|
|
2255
|
+
session_status: payload.session_status || "unknown",
|
|
2256
|
+
});
|
|
2257
|
+
logger.metric("tokens_not_renewed_total", 1, {
|
|
2258
|
+
component: "TokenRenewalService",
|
|
2259
|
+
reason: "sufficient_lifetime",
|
|
2260
|
+
session_status: payload.session_status || "unknown",
|
|
2261
|
+
seconds_until_eligibility: secondsUntilEligibility,
|
|
2262
|
+
});
|
|
2263
|
+
return { newToken: null };
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
// Token needs renewal
|
|
2267
|
+
logger.info(forceRenewal ? "Token expired, attempting renewal" : "Token eligible for proactive renewal", {
|
|
2268
|
+
component: "TokenRenewalService",
|
|
2269
|
+
method: "checkandrenew_jwt_token",
|
|
2270
|
+
requestId,
|
|
2271
|
+
timeLeftPercent: durationLeftpct.toFixed(1),
|
|
2272
|
+
renewThreshold: (
|
|
2273
|
+
100 - (LAPSED_LIFETIME_PROPORTION_4RENEWAL_ELIGIBILITY * 100)
|
|
2274
|
+
).toFixed(1),
|
|
2275
|
+
...sessionInfo,
|
|
2276
|
+
});
|
|
2277
|
+
|
|
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));
|
|
2285
|
+
|
|
2286
|
+
const verificationStartTime = Date.now();
|
|
2287
|
+
|
|
2288
|
+
// Determine verification level for renewal
|
|
2289
|
+
const verification_level = shouldDoFullVerification ? "full" : "light";
|
|
2290
|
+
|
|
2291
|
+
try {
|
|
2292
|
+
let isValid = false;
|
|
2293
|
+
let notAfter = null;
|
|
2294
|
+
|
|
2295
|
+
if (shouldDoFullVerification) {
|
|
2296
|
+
const validationResult = await thorough_validate_jwt_token_be(
|
|
2297
|
+
payload,
|
|
2298
|
+
requestId
|
|
2299
|
+
);
|
|
2300
|
+
|
|
2301
|
+
isValid = validationResult.isValid;
|
|
2302
|
+
notAfter = validationResult.notAfter;
|
|
2303
|
+
|
|
2304
|
+
const verificationDuration = Date.now() - verificationStartTime;
|
|
2305
|
+
logger.metric("token_verification_duration_ms", verificationDuration, {
|
|
2306
|
+
component: "TokenRenewalService",
|
|
2307
|
+
verificationType: "thorough",
|
|
2308
|
+
success: isValid,
|
|
2309
|
+
});
|
|
2310
|
+
} else {
|
|
2311
|
+
// Light verification path
|
|
2312
|
+
const validationResult = await brief_validate_jwt_token_be(
|
|
2313
|
+
payload,
|
|
2314
|
+
);
|
|
2315
|
+
|
|
2316
|
+
isValid = validationResult.isValid;
|
|
2317
|
+
notAfter = validationResult.notAfter;
|
|
2318
|
+
|
|
2319
|
+
const verificationDuration = Date.now() - verificationStartTime;
|
|
2320
|
+
logger.metric("token_verification_duration_ms", verificationDuration, {
|
|
2321
|
+
component: "TokenRenewalService",
|
|
2322
|
+
verificationType: "brief",
|
|
2323
|
+
success: isValid,
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
if (isValid) {
|
|
2328
|
+
const renewalStartTime = Date.now();
|
|
2329
|
+
const newToken = await generate_jwt_token_fromtoken(
|
|
2330
|
+
payload,
|
|
2331
|
+
newduration,
|
|
2332
|
+
notAfter,
|
|
2333
|
+
timestamp,
|
|
2334
|
+
shouldDoFullVerification ? "full" : "light"
|
|
2335
|
+
);
|
|
2336
|
+
|
|
2337
|
+
const renewalDuration = Date.now() - renewalStartTime;
|
|
2338
|
+
const totalDuration = Date.now() - startTime;
|
|
2339
|
+
|
|
2340
|
+
logger.info("Proactive token renewal successful", {
|
|
2341
|
+
component: "TokenRenewalService",
|
|
2342
|
+
method: "checkandrenew_jwt_token",
|
|
2343
|
+
requestId,
|
|
2344
|
+
verificationType: shouldDoFullVerification ? "thorough" : "brief",
|
|
2345
|
+
renewalDuration,
|
|
2346
|
+
totalDuration,
|
|
2347
|
+
newDuration: newduration,
|
|
2348
|
+
sessionStatus: shouldDoFullVerification
|
|
2349
|
+
? "renewed_full_verification"
|
|
2350
|
+
: "renewed_light_verification",
|
|
2351
|
+
});
|
|
2352
|
+
|
|
2353
|
+
// Emit metrics for successful renewal
|
|
2354
|
+
logger.metric("token_renewal_duration_ms", renewalDuration, {
|
|
2355
|
+
component: "TokenRenewalService",
|
|
2356
|
+
success: true,
|
|
2357
|
+
verificationType: shouldDoFullVerification ? "thorough" : "brief",
|
|
2358
|
+
verification_level: shouldDoFullVerification ? "full" : "light",
|
|
2359
|
+
session_status: shouldDoFullVerification
|
|
2360
|
+
? "renewed_full_verification"
|
|
2361
|
+
: "renewed_light_verification",
|
|
2362
|
+
});
|
|
2363
|
+
|
|
2364
|
+
return {
|
|
2365
|
+
newToken,
|
|
2366
|
+
logInfo: {
|
|
2367
|
+
newDuration: newduration,
|
|
2368
|
+
reason: shouldDoFullVerification
|
|
2369
|
+
? "Thorough verification"
|
|
2370
|
+
: "Brief verification",
|
|
2371
|
+
notAfter: notAfter,
|
|
2372
|
+
renewalDuration,
|
|
2373
|
+
totalDuration,
|
|
2374
|
+
verificationLevel: shouldDoFullVerification ? "full" : "light",
|
|
2375
|
+
sessionStatus: shouldDoFullVerification
|
|
2376
|
+
? "renewed_full_verification"
|
|
2377
|
+
: "renewed_light_verification",
|
|
2378
|
+
},
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
} catch (error) {
|
|
2382
|
+
logger.error("Token renewal failed", {
|
|
2383
|
+
component: "TokenRenewalService",
|
|
2384
|
+
method: "checkandrenew_jwt_token",
|
|
2385
|
+
requestId,
|
|
2386
|
+
error: error.message,
|
|
2387
|
+
});
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// If we reach here, renewal wasn't successful
|
|
2391
|
+
const totalDuration = Date.now() - startTime;
|
|
2392
|
+
logger.debug("Token renewal not performed", {
|
|
2393
|
+
component: "TokenRenewalService",
|
|
2394
|
+
method: "checkandrenew_jwt_token",
|
|
2395
|
+
requestId,
|
|
2396
|
+
totalDuration,
|
|
2397
|
+
sessionId: payload.session_id || "none",
|
|
2398
|
+
});
|
|
2399
|
+
|
|
2400
|
+
logger.metric("token_renewal_check_duration_ms", totalDuration, {
|
|
2401
|
+
component: "TokenRenewalService",
|
|
2402
|
+
renewalNeeded: true,
|
|
2403
|
+
success: false,
|
|
2404
|
+
session_status: payload.session_status || "unknown",
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2407
|
+
return { newToken: null };
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
|
|
2411
|
+
// Export the class directly (will be instantiated in rodit.js)
|
|
2412
|
+
module.exports = {generate_jwt_token,base64url2jwk_public_key,
|
|
2413
|
+
checkandrenew_jwt_token,
|
|
2414
|
+
thorough_validate_jwt_token_be,
|
|
2415
|
+
brief_validate_jwt_token_be,
|
|
2416
|
+
generate_jwt_token_fromtoken,
|
|
2417
|
+
verify_jwt_token,validate_jwt_token_be, generate_session_termination_token
|
|
2418
|
+
};
|