@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.
@@ -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
+ };