@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,2301 @@
1
+ /**
2
+ * Authentication middleware for web API
3
+ * Copyright (c) 2026 Discernible IO. All rights reserved.
4
+ */
5
+
6
+ const { ulid } = require("ulid");
7
+ const config = require('../../services/configsdk');
8
+ const { isStrictEnvironment } = require('../../services/env');
9
+ const logger = require("../../services/logger");
10
+ const { sendError } = require("../../services/error-response");
11
+ const { createLogContext, logErrorWithMetrics } = logger;
12
+ const nacl = require("tweetnacl");
13
+ // Import specific functions directly to avoid circular dependencies
14
+ const {
15
+ validate_jwt_token_be,
16
+ generate_jwt_token,
17
+ tokenService
18
+ } = require("../auth/tokenservice");
19
+ // Import specific functions from authentication.js to avoid circular dependencies
20
+ // Import specific functions from authentication.js to avoid circular dependencies
21
+ const {
22
+ resolve_peer_rodit_for_login,
23
+ verify_peer_rodit,
24
+ verify_rodit_ownership_withnep413
25
+ } = require("../auth/authentication");
26
+ const {
27
+ nearorg_rpc_tokenfromroditid
28
+ } = require("../blockchain/blockchainservice");
29
+ // Direct import from statemanager to avoid circular dependencies
30
+ const stateManager = require("../blockchain/statemanager");
31
+ const utils = require("../../services/utils");
32
+ const { unixTimeToDateString } = utils;
33
+ // Import sessionManager singleton - ensure we get the same instance used everywhere
34
+ const { sessionManager } = require("../auth/sessionmanager");
35
+
36
+ // Log which SessionManager instance is being used
37
+ logger.infoWithContext("AuthenticationMW using SessionManager instance", {
38
+ component: "AuthenticationMW",
39
+ event: "sessionManager_import",
40
+ sessionManagerInstanceId: sessionManager._instanceId,
41
+ timestamp: new Date().toISOString()
42
+ });
43
+
44
+ // Dynamic import for ESM 'jose' in CommonJS context
45
+ let _josePromise;
46
+ async function getJose() {
47
+ if (!_josePromise) {
48
+ _josePromise = import("jose");
49
+ }
50
+ return _josePromise;
51
+ }
52
+
53
+ // Portal/outbound login only: skip server session registration when relaxed (default).
54
+ // API auth does not pass these options and always enforces stored session + expiresAt.
55
+ const RELAXED_SESSION_VALIDATION_OPTIONS = Object.freeze({
56
+ enforceSessionRegistration: !config.get(
57
+ "SECURITY_OPTIONS.RELAXED_SESSION_VALIDATION",
58
+ true
59
+ ),
60
+ });
61
+
62
+ // Import validation utilities or define them if not available
63
+ const validationResult = { isEmpty: () => true }; // Default implementation if not available
64
+
65
+ /**
66
+ * Verify sessionManager is properly initialized
67
+ * @throws {Error} If sessionManager is not properly initialized
68
+ */
69
+ function verifySessionManager() {
70
+ if (!sessionManager || !sessionManager.storage) {
71
+ throw new Error("SessionManager not properly initialized in authentication middleware");
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Middleware for handling authentication in routes
77
+ */
78
+
79
+ /**
80
+ * Authenticates a client using RODiT credentials and generates a JWT jwt_token
81
+ *
82
+ * @param {Object} req - Express request object
83
+ * @param {Object} res - Express response object
84
+ * @returns {Object} - JSON response with jwt_token or error
85
+ */
86
+ function normalizeOptionalLoginString(v) {
87
+ if (v === undefined || v === null) {
88
+ return "";
89
+ }
90
+ return String(v).trim();
91
+ }
92
+
93
+ /** Legacy login payload keys that must not appear (wire compat uses roditid_base64url_signature alias below). */
94
+ function loginBodyHasDeprecatedKeys(body) {
95
+ if (!body || typeof body !== "object") {
96
+ return false;
97
+ }
98
+ return (
99
+ Object.prototype.hasOwnProperty.call(body, "signature") ||
100
+ Object.prototype.hasOwnProperty.call(body, "account_id")
101
+ );
102
+ }
103
+
104
+ /** Both modern and legacy signature field names filled — ambiguous; login_server sends only legacy field name. */
105
+ function loginBodyHasDuplicateSignatureFields(body) {
106
+ if (!body || typeof body !== "object") {
107
+ return false;
108
+ }
109
+ const a =
110
+ typeof body.base64url_signature === "string"
111
+ ? body.base64url_signature.trim()
112
+ : "";
113
+ const b =
114
+ typeof body.roditid_base64url_signature === "string"
115
+ ? body.roditid_base64url_signature.trim()
116
+ : "";
117
+ return a.length > 0 && b.length > 0;
118
+ }
119
+
120
+ function extractLoginBase64UrlSignature(body) {
121
+ const fromNew =
122
+ typeof body.base64url_signature === "string"
123
+ ? body.base64url_signature.trim()
124
+ : "";
125
+ const fromLegacy =
126
+ typeof body.roditid_base64url_signature === "string"
127
+ ? body.roditid_base64url_signature.trim()
128
+ : "";
129
+ return fromNew || fromLegacy;
130
+ }
131
+
132
+ function parseRequiredLoginTimestamp(rawTimestamp) {
133
+ if (rawTimestamp === undefined || rawTimestamp === null || rawTimestamp === "") {
134
+ return null;
135
+ }
136
+ const parsed = Number(rawTimestamp);
137
+ if (!Number.isInteger(parsed) || parsed <= 0) {
138
+ return null;
139
+ }
140
+ return parsed;
141
+ }
142
+
143
+ function parseRequiredServerLoginTimestamp(rawTimestamp) {
144
+ const parsed = Number(rawTimestamp);
145
+ if (!Number.isInteger(parsed) || parsed <= 0) {
146
+ return null;
147
+ }
148
+ return parsed;
149
+ }
150
+
151
+ function normalizeOptionalServerAccountId(rawAccountId) {
152
+ if (rawAccountId === undefined || rawAccountId === null) {
153
+ return "";
154
+ }
155
+ return String(rawAccountId).trim();
156
+ }
157
+
158
+ function buildLoginUrl(apiendpoint, loginPath = "/api/login") {
159
+ return `${String(apiendpoint).replace(/\/$/, "")}${loginPath.startsWith("/") ? loginPath : `/${loginPath}`}`;
160
+ }
161
+
162
+ async function resolveServerLoginTimestamp(apiendpoint, options = {}) {
163
+ const explicit = parseRequiredServerLoginTimestamp(options.timestamp);
164
+ if (explicit !== null) {
165
+ return { timestamp: explicit };
166
+ }
167
+
168
+ const timestampPath =
169
+ options.timestampPath ??
170
+ config.get("LOGIN_TIMESTAMP_PATH", "/api/login/timestamp");
171
+ const timestampUrl = buildLoginUrl(apiendpoint, timestampPath);
172
+
173
+ try {
174
+ const response = await fetch(timestampUrl, {
175
+ method: "GET",
176
+ headers: {
177
+ "Accept": "application/json",
178
+ "User-Agent": "RODiT-SDK",
179
+ },
180
+ });
181
+
182
+ if (!response.ok) {
183
+ return {
184
+ timestamp: null,
185
+ errorCode: "LOGIN_TIMESTAMP_FETCH_FAILED",
186
+ error: `Failed to fetch login timestamp challenge: HTTP ${response.status}`,
187
+ };
188
+ }
189
+
190
+ const data = await response.json();
191
+ const parsed = parseRequiredServerLoginTimestamp(data?.timestamp);
192
+ if (parsed === null) {
193
+ return {
194
+ timestamp: null,
195
+ errorCode: "INVALID_LOGIN_TIMESTAMP",
196
+ error: "Login timestamp challenge response is missing a valid timestamp",
197
+ };
198
+ }
199
+
200
+ return { timestamp: parsed };
201
+ } catch (error) {
202
+ return {
203
+ timestamp: null,
204
+ errorCode: "LOGIN_TIMESTAMP_FETCH_FAILED",
205
+ error: `Failed to fetch login timestamp challenge: ${error.message}`,
206
+ };
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Login validation failures have a special contract:
212
+ * - Silent mode: return no response body at all
213
+ * - Non-silent mode: keep legacy flat error payload shape
214
+ */
215
+ function respondLoginValidationFailure(res, { silenceLoginFailures, statusCode = 400, code, message, requestId }) {
216
+ if (silenceLoginFailures) {
217
+ return;
218
+ }
219
+ return sendError(res, {
220
+ statusCode,
221
+ requestId,
222
+ code,
223
+ message
224
+ });
225
+ }
226
+
227
+ async function login_client(req, res) {
228
+ const requestId = ulid();
229
+ const startTime = Date.now();
230
+
231
+ // Create a base context for this function
232
+ const baseContext = createLogContext(
233
+ "RoditAuth",
234
+ "login_client",
235
+ {
236
+ requestId,
237
+ ip: req.ip,
238
+ userAgent: req.headers["user-agent"]
239
+ }
240
+ );
241
+
242
+ logger.infoWithContext("Client login request received", baseContext); // Function call log
243
+ // Determines whether login failures should be silent, configurable via SECURITY_OPTIONS.SILENT_LOGIN_FAILURES
244
+ let silenceLoginFailures = false;
245
+
246
+ try {
247
+ const body = req.body && typeof req.body === "object" ? req.body : {};
248
+ silenceLoginFailures = config.get('SECURITY_OPTIONS.SILENT_LOGIN_FAILURES');
249
+
250
+ if (loginBodyHasDeprecatedKeys(body)) {
251
+ const duration = Date.now() - startTime;
252
+ logger.metric("login_attempt_duration_ms", duration, {
253
+ component: "RoditAuth",
254
+ success: false,
255
+ result: "failure",
256
+ reason: "deprecated_login_payload",
257
+ error: "LOGIN_PAYLOAD_DEPRECATED",
258
+ });
259
+ logger.metric("failed_login_attempts_total", 1, {
260
+ component: "RoditAuth",
261
+ result: "failure",
262
+ reason: "LOGIN_PAYLOAD_DEPRECATED",
263
+ });
264
+ return respondLoginValidationFailure(res, {
265
+ silenceLoginFailures,
266
+ statusCode: 400,
267
+ requestId,
268
+ code: "LOGIN_PAYLOAD_DEPRECATED",
269
+ message:
270
+ "Remove signature and account_id. Send roditid and accountid (one empty), timestamp, and base64url_signature (or roditid_base64url_signature for the same value - not both).",
271
+ });
272
+ }
273
+
274
+ if (loginBodyHasDuplicateSignatureFields(body)) {
275
+ const duration = Date.now() - startTime;
276
+ logger.metric("login_attempt_duration_ms", duration, {
277
+ component: "RoditAuth",
278
+ success: false,
279
+ result: "failure",
280
+ reason: "duplicate_signature_fields",
281
+ error: "LOGIN_PAYLOAD_DEPRECATED",
282
+ });
283
+ logger.metric("failed_login_attempts_total", 1, {
284
+ component: "RoditAuth",
285
+ result: "failure",
286
+ reason: "LOGIN_PAYLOAD_DEPRECATED",
287
+ });
288
+ return respondLoginValidationFailure(res, {
289
+ silenceLoginFailures,
290
+ statusCode: 400,
291
+ requestId,
292
+ code: "LOGIN_PAYLOAD_DEPRECATED",
293
+ message:
294
+ "Send exactly one signature field: base64url_signature or roditid_base64url_signature (same bytes), not both non-empty.",
295
+ });
296
+ }
297
+
298
+ const roditid = normalizeOptionalLoginString(body.roditid);
299
+ const accountid = normalizeOptionalLoginString(body.accountid);
300
+ const hasRoditId = roditid.length > 0;
301
+ const hasAccountId = accountid.length > 0;
302
+ const peer_timestamp = parseRequiredLoginTimestamp(body.timestamp);
303
+ const base64url_signature = extractLoginBase64UrlSignature(body);
304
+
305
+ logger.infoWithContext("Login request identifiers (sanitized)", {
306
+ ...baseContext,
307
+ roditid: roditid || undefined,
308
+ accountid: accountid || undefined,
309
+ login_mode: hasRoditId ? "roditid" : hasAccountId ? "accountid" : "none",
310
+ timestamp: peer_timestamp,
311
+ has_base64url_signature: base64url_signature.length > 0,
312
+ });
313
+
314
+ if (hasRoditId && hasAccountId) {
315
+ const duration = Date.now() - startTime;
316
+ logger.debugWithContext("Ambiguous login identifiers (both roditid and accountid non-empty)", {
317
+ ...baseContext,
318
+ duration,
319
+ result: "failure",
320
+ reason: "login_identifier_ambiguous",
321
+ bodyKeys: Object.keys(body),
322
+ });
323
+ logger.metric("login_attempt_duration_ms", duration, {
324
+ component: "RoditAuth",
325
+ success: false,
326
+ result: "failure",
327
+ reason: "login_identifier_ambiguous",
328
+ error: "LOGIN_IDENTIFIER_AMBIGUOUS",
329
+ });
330
+ logger.metric("failed_login_attempts_total", 1, {
331
+ component: "RoditAuth",
332
+ result: "failure",
333
+ reason: "LOGIN_IDENTIFIER_AMBIGUOUS",
334
+ });
335
+ return respondLoginValidationFailure(res, {
336
+ silenceLoginFailures,
337
+ statusCode: 400,
338
+ requestId,
339
+ code: "LOGIN_IDENTIFIER_AMBIGUOUS",
340
+ message:
341
+ "Send exactly one of roditid or accountid non-empty; the other must be empty. Signature verifies against that single identifier.",
342
+ });
343
+ }
344
+
345
+ if (!hasRoditId && !hasAccountId) {
346
+ const duration = Date.now() - startTime;
347
+
348
+ logger.debugWithContext("Missing login identifier in login request", {
349
+ ...baseContext,
350
+ duration,
351
+ result: "failure",
352
+ reason: "missing_login_identifier",
353
+ bodyKeys: Object.keys(body),
354
+ });
355
+ logger.metric("login_attempt_duration_ms", duration, {
356
+ component: "RoditAuth",
357
+ success: false,
358
+ result: "failure",
359
+ reason: "missing_login_identifier",
360
+ error: "MISSING_LOGIN_IDENTIFIER",
361
+ });
362
+ logger.metric("failed_login_attempts_total", 1, {
363
+ component: "RoditAuth",
364
+ result: "failure",
365
+ reason: "MISSING_LOGIN_IDENTIFIER",
366
+ });
367
+
368
+ return respondLoginValidationFailure(res, {
369
+ silenceLoginFailures,
370
+ statusCode: 400,
371
+ requestId,
372
+ code: "MISSING_LOGIN_IDENTIFIER",
373
+ message:
374
+ "Provide roditid (token id) or accountid (64-character hex NEAR implicit account); include both keys with exactly one non-empty value.",
375
+ });
376
+ }
377
+
378
+ if (peer_timestamp === null) {
379
+ const duration = Date.now() - startTime;
380
+
381
+ logger.debugWithContext("Missing or invalid timestamp in login request", {
382
+ ...baseContext,
383
+ duration,
384
+ result: "failure",
385
+ reason: "invalid_login_timestamp",
386
+ providedTimestampType: typeof body.timestamp,
387
+ });
388
+ logger.metric("login_attempt_duration_ms", duration, {
389
+ component: "RoditAuth",
390
+ success: false,
391
+ result: "failure",
392
+ reason: "invalid_login_timestamp",
393
+ error: "INVALID_LOGIN_TIMESTAMP",
394
+ });
395
+ logger.metric("failed_login_attempts_total", 1, {
396
+ component: "RoditAuth",
397
+ result: "failure",
398
+ reason: "INVALID_LOGIN_TIMESTAMP",
399
+ });
400
+
401
+ return respondLoginValidationFailure(res, {
402
+ silenceLoginFailures,
403
+ statusCode: 400,
404
+ requestId,
405
+ code: "INVALID_LOGIN_TIMESTAMP",
406
+ message:
407
+ "Provide a valid Unix-seconds `timestamp` for POST /api/login (from the same GET /api/login/timestamp login challenge as your signature). The login signing payload is UTF-8 identifier + canonical timestamp_iso from that response.",
408
+ });
409
+ }
410
+
411
+ const peer_roditid = hasRoditId ? roditid : accountid;
412
+
413
+ if (!base64url_signature) {
414
+ const duration = Date.now() - startTime;
415
+
416
+ logger.debugWithContext("Missing base64url_signature in login request", {
417
+ ...baseContext,
418
+ duration,
419
+ result: "failure",
420
+ reason: "missing_base64url_signature",
421
+ bodyKeys: Object.keys(body),
422
+ });
423
+ logger.metric("login_attempt_duration_ms", duration, {
424
+ component: "RoditAuth",
425
+ success: false,
426
+ result: "failure",
427
+ reason: "missing_base64url_signature",
428
+ error: "MISSING_BASE64URL_SIGNATURE",
429
+ });
430
+ logger.metric("failed_login_attempts_total", 1, {
431
+ component: "RoditAuth",
432
+ result: "failure",
433
+ reason: "MISSING_BASE64URL_SIGNATURE",
434
+ });
435
+
436
+ return respondLoginValidationFailure(res, {
437
+ silenceLoginFailures,
438
+ statusCode: 400,
439
+ requestId,
440
+ code: "MISSING_BASE64URL_SIGNATURE",
441
+ message:
442
+ "Provide base64url_signature (or roditid_base64url_signature): base64url-encoded Ed25519 signature over the login signing payload — UTF-8 concatenation of your roditid or accountid with the canonical timestamp_iso from GET /api/login/timestamp (same login challenge as the Unix timestamp you send).",
443
+ });
444
+ }
445
+
446
+ logger.debugWithContext("Login parameters extracted", {
447
+ ...baseContext,
448
+ hasRoditId: roditid.length > 0,
449
+ hasAccountId: accountid.length > 0,
450
+ hasTimestamp: peer_timestamp !== undefined && peer_timestamp !== null,
451
+ has_base64url_signature: base64url_signature.length > 0,
452
+ });
453
+
454
+ logger.debugWithContext("Retrieving server configuration", baseContext);
455
+
456
+ // Import stateManager only when needed to avoid circular dependencies
457
+ const stateManager = require("../blockchain/statemanager");
458
+ const config_own_rodit = await stateManager.getConfigOwnRodit();
459
+
460
+ if (!config_own_rodit) {
461
+ const duration = Date.now() - startTime;
462
+
463
+ logErrorWithMetrics(
464
+ "Server configuration not initialized",
465
+ {
466
+ ...baseContext,
467
+ duration,
468
+ errorCode: "CONFIG_NOT_INITIALIZED"
469
+ },
470
+ new Error("Server configuration not initialized"),
471
+ "login_error",
472
+ { error_type: "config_error" }
473
+ );
474
+
475
+ // Emit metrics for dashboards
476
+ logger.metric("login_attempt_duration_ms", duration, {
477
+ component: "RoditAuth",
478
+ success: false,
479
+ error: "CONFIG_NOT_INITIALIZED",
480
+ });
481
+ logger.metric("failed_login_attempts_total", 1, {
482
+ component: "RoditAuth",
483
+ reason: "CONFIG_NOT_INITIALIZED",
484
+ });
485
+
486
+ throw new Error("Error 0112: Server configuration not initialized");
487
+ }
488
+
489
+ logger.debugWithContext("Verifying peer RODiT credentials", {
490
+ ...baseContext,
491
+ hasRoditId,
492
+ hasAccountId,
493
+ peerRoditIdForVerify: peer_roditid,
494
+ signature_covers: hasRoditId ? "roditid" : "accountid",
495
+ });
496
+
497
+ logger.debugWithContext("Resolving and verifying peer RODiT", {
498
+ ...baseContext,
499
+ peerRoditId: peer_roditid,
500
+ });
501
+ const result = await verify_peer_rodit(
502
+ await resolve_peer_rodit_for_login(roditid, accountid),
503
+ peer_roditid,
504
+ peer_timestamp,
505
+ base64url_signature
506
+ );
507
+
508
+ const { peer_rodit, goodrodit: isRoditValid, failureReason, failureMessage } = result;
509
+
510
+ if (!isRoditValid) {
511
+ const duration = Date.now() - startTime;
512
+
513
+ logger.debugWithContext("Invalid RODiT credentials", {
514
+ ...baseContext,
515
+ duration,
516
+ result: 'failure',
517
+ reason: failureReason || 'Invalid credentials',
518
+ failureMessage: failureMessage || 'Unknown failure',
519
+ roditId: peer_roditid
520
+ });
521
+ // Emit metrics for dashboards
522
+ logger.metric("login_attempt_duration_ms", duration, {
523
+ component: "RoditAuth",
524
+ success: false,
525
+ result: 'failure',
526
+ reason: failureReason || 'Invalid credentials',
527
+ error: failureReason || "INVALID_CREDENTIALS",
528
+ });
529
+ logger.metric("failed_login_attempts_total", 1, {
530
+ component: "RoditAuth",
531
+ result: 'failure',
532
+ reason: failureReason || "Invalid credentials",
533
+ });
534
+
535
+ if (!silenceLoginFailures) {
536
+ return sendError(res, {
537
+ statusCode: 401,
538
+ requestId,
539
+ code: failureReason || "INVALID_CREDENTIALS",
540
+ message: `Error 102: Login attempt failed: ${failureMessage || 'Invalid RODiT or Signature'}`,
541
+ details: {
542
+ failureReason: failureReason || null,
543
+ failureMessage: failureMessage || null
544
+ }
545
+ });
546
+ }
547
+ // Completely silent - no response at all
548
+ return;
549
+ }
550
+
551
+ const jwt_token = await generate_jwt_token(
552
+ peer_rodit,
553
+ peer_timestamp,
554
+ config_own_rodit.own_rodit,
555
+ config_own_rodit.own_rodit_bytes_private_key
556
+ );
557
+
558
+ const duration = Date.now() - startTime;
559
+ logger.infoWithContext("Issued login JWT token", {
560
+ ...baseContext,
561
+ decision: "issued",
562
+ reason: "login_client authentication succeeded",
563
+ jwtTokenLength: jwt_token?.length
564
+ });
565
+ logger.infoWithContext("Login successful", {
566
+ ...baseContext,
567
+ duration,
568
+ result: 'success',
569
+ reason: 'Authenticated successfully',
570
+ roditId: peer_rodit.token_id
571
+ });
572
+ // Emit metrics for dashboards
573
+ logger.metric("login_attempt_duration_ms", duration, {
574
+ component: "RoditAuth",
575
+ success: true,
576
+ result: 'success',
577
+ reason: 'Authenticated successfully'
578
+ });
579
+ logger.metric("successful_logins_total", 1, {
580
+ component: "RoditAuth",
581
+ result: 'success',
582
+ reason: 'Authenticated successfully'
583
+ });
584
+
585
+ // Set the jwt_token in the response header
586
+ res.setHeader('New-Token', jwt_token);
587
+
588
+ return res.json({
589
+ jwt_token,
590
+ requestId
591
+ });
592
+ } catch (error) {
593
+ const duration = Date.now() - startTime;
594
+
595
+ logErrorWithMetrics(
596
+ "Login authentication failed",
597
+ {
598
+ ...baseContext,
599
+ duration,
600
+ result: 'failure',
601
+ reason: error.message || error.code || 'Unknown error',
602
+ errorCode: error.code || "UNKNOWN_ERROR"
603
+ },
604
+ error,
605
+ "login_error",
606
+ { error_type: "authentication_error" }
607
+ );
608
+ // Emit metrics for dashboards
609
+ logger.metric("login_attempt_duration_ms", duration, {
610
+ component: "RoditAuth",
611
+ success: false,
612
+ result: 'failure',
613
+ reason: error.message || error.code || 'Unknown error',
614
+ error: error.code || "UNKNOWN_ERROR",
615
+ });
616
+ logger.metric("failed_login_attempts_total", 1, {
617
+ component: "RoditAuth",
618
+ result: 'failure',
619
+ reason: error.message || error.code || 'Unknown error',
620
+ });
621
+
622
+ if (!silenceLoginFailures) {
623
+ return sendError(res, {
624
+ statusCode: 401,
625
+ requestId,
626
+ code: "LOGIN_ERROR",
627
+ message: `Error 105: Login attempt failed: ${error.message}`
628
+ });
629
+ }
630
+ // Completely silent - no response at all
631
+ return;
632
+ }
633
+ }
634
+
635
+
636
+ /**
637
+ * Extract jwt_token from authorization header
638
+ *
639
+ * @param {string} authHeader - Authorization header
640
+ * @returns {string|null} Extracted jwt_token or null
641
+ */
642
+ function extractTokenFromHeader(authHeader) {
643
+ const startTime = Date.now();
644
+ const requestId = ulid();
645
+
646
+ // Create a base context for this function
647
+ const baseContext = createLogContext(
648
+ "TokenExtractor",
649
+ "extractTokenFromHeader",
650
+ { requestId }
651
+ );
652
+
653
+ if (!authHeader) {
654
+ logger.debugWithContext("No authorization header present", baseContext);
655
+ return null;
656
+ }
657
+
658
+ const [bearer, jwt_token] = authHeader.split(" ");
659
+
660
+ if (bearer.toLowerCase() !== "bearer" || !jwt_token) {
661
+ logger.debugWithContext("Invalid authorization header format", {
662
+ ...baseContext,
663
+ headerFormat: authHeader ? authHeader.substring(0, 50) + '...' : 'null',
664
+ bearerPart: bearer,
665
+ hasToken: !!jwt_token
666
+ });
667
+ return null;
668
+ }
669
+
670
+ return jwt_token;
671
+ }
672
+
673
+ /**
674
+ * Middleware to authenticate API calls
675
+ *
676
+ * @param {Object} req - Express request object
677
+ * @param {Object} res - Express response object
678
+ * @param {Function} next - Next middleware function
679
+ */
680
+ async function authenticate_apicall(req, res, next) {
681
+ const startTime = Date.now();
682
+ const requestId = ulid();
683
+
684
+ // Debug: Log incoming request details
685
+ logger.debugWithContext("Authentication middleware called", {
686
+ component: "AuthMiddleware",
687
+ method: "authenticate_apicall",
688
+ requestId,
689
+ path: req.path,
690
+ httpMethod: req.method,
691
+ hasAuthHeader: !!req.headers.authorization,
692
+ allHeaders: Object.keys(req.headers)
693
+ });
694
+
695
+ const jwt_token = extractTokenFromHeader(req.headers.authorization);
696
+
697
+ // Create a base context for this function
698
+ const baseContext = createLogContext(
699
+ "AuthMiddleware",
700
+ "authenticate_apicall",
701
+ {
702
+ requestId,
703
+ path: req.path,
704
+ method: req.method
705
+ }
706
+ );
707
+
708
+ logger.infoWithContext("API authentication started", {
709
+ ...baseContext,
710
+ hasToken: !!jwt_token,
711
+ result: 'call',
712
+ reason: 'API authentication started'
713
+ }); // Function call log
714
+
715
+ try {
716
+ // Verify sessionManager is properly initialized before using it
717
+ verifySessionManager();
718
+
719
+ if (!jwt_token) {
720
+ // Add metric for missing jwt_token
721
+ logger.metric('auth_operations', Date.now() - startTime, {
722
+ operation: 'authenticate_apicall',
723
+ result: 'failure',
724
+ reason: 'No jwt_token provided'
725
+ });
726
+ return sendError(res, {
727
+ statusCode: 401,
728
+ requestId,
729
+ code: "MISSING_TOKEN",
730
+ message: "No jwt_token provided"
731
+ });
732
+ }
733
+
734
+ // Check if token is valid by checking session state
735
+ const isTokenInvalid = await sessionManager.isTokenInvalidated(jwt_token);
736
+
737
+ if (isTokenInvalid) {
738
+ const invalidationInfo = await sessionManager.getTokenInvalidationInfo(jwt_token);
739
+
740
+ // Add metric for invalid token
741
+ logger.metric('auth_operations', Date.now() - startTime, {
742
+ operation: 'authenticate_apicall',
743
+ result: 'failure',
744
+ reason: invalidationInfo?.reason || 'Session not active'
745
+ });
746
+
747
+ return sendError(res, {
748
+ statusCode: 401,
749
+ requestId,
750
+ code: "INVALIDATED_TOKEN",
751
+ message: "Token has been invalidated",
752
+ details: {
753
+ reason: invalidationInfo?.reason || "session_inactive",
754
+ invalidatedAt: invalidationInfo?.timestamp
755
+ }
756
+ });
757
+ }
758
+
759
+ // Get own RODiT configuration first
760
+ const config_own_rodit = await stateManager.getConfigOwnRodit();
761
+
762
+ if (!config_own_rodit || !config_own_rodit.own_rodit) {
763
+ logErrorWithMetrics(
764
+ "Server configuration not initialized",
765
+ {
766
+ ...baseContext,
767
+ hasConfig: !!config_own_rodit
768
+ },
769
+ new Error("Server configuration not initialized"),
770
+ "auth_error",
771
+ { error_type: "config_error" }
772
+ );
773
+ return sendError(res, {
774
+ statusCode: 500,
775
+ requestId,
776
+ code: "SERVER_CONFIG_ERROR",
777
+ message: "Server configuration not initialized"
778
+ });
779
+ }
780
+
781
+ // Use the jwt_token service to validate the jwt_token WITH the own_rodit parameter
782
+ let validationResult;
783
+ try {
784
+ // Decode opportunistically so malformed tokens fail fast before full validation.
785
+ try {
786
+ const { decodeJwt } = await getJose();
787
+ decodeJwt(jwt_token);
788
+ } catch (_decodeError) {}
789
+
790
+ validationResult = await validate_jwt_token_be(
791
+ jwt_token,
792
+ config_own_rodit.own_rodit
793
+ );
794
+ } catch (validationError) {
795
+ // Handle specific validation errors
796
+ // Add metric for jwt_token validation failure
797
+ logger.metric('auth_operations', Date.now() - startTime, {
798
+ operation: 'authenticate_apicall',
799
+ result: 'failure',
800
+ reason: validationError.message || 'Token validation failed'
801
+ });
802
+ return sendError(res, {
803
+ statusCode: 403,
804
+ requestId,
805
+ code: validationError.code || "INVALID_TOKEN",
806
+ message: validationError.message || "Invalid jwt_token"
807
+ });
808
+ }
809
+
810
+ if (!validationResult.valid) {
811
+ // Add metric for invalid jwt_token
812
+ logger.metric('auth_operations', Date.now() - startTime, {
813
+ operation: 'authenticate_apicall',
814
+ result: 'failure',
815
+ reason: validationResult.error || 'Invalid jwt_token'
816
+ });
817
+ // Return 403 for invalid jwt_tokens
818
+ return sendError(res, {
819
+ statusCode: 403,
820
+ requestId,
821
+ code: validationResult.errorCode || "INVALID_TOKEN",
822
+ message: "Invalid jwt_token",
823
+ details: validationResult.error ? { error: validationResult.error } : undefined
824
+ });
825
+ }
826
+
827
+ // IMPORTANT: Attach the raw payload to req.user to maintain exact compatibility
828
+ // with digital signature verification processes
829
+ req.user = validationResult.payload;
830
+
831
+ // Store the jwt_token for potential use in the request
832
+ req.jwt_token = jwt_token;
833
+
834
+ // Check if a new jwt_token was generated during validation
835
+ if (validationResult.newToken) {
836
+ // Add the new jwt_token to the response headers ONLY (no cookies)
837
+ res.setHeader('New-Token', validationResult.newToken);
838
+ }
839
+
840
+ const duration = Date.now() - startTime;
841
+ logger.infoWithContext("Authentication successful", {
842
+ ...baseContext,
843
+ userId: req.user.sub, // Use sub from raw payload
844
+ duration,
845
+ decision: "accepted",
846
+ result: 'success',
847
+ reason: 'Authentication successful'
848
+ });
849
+ // Add metric for successful authentication
850
+ logger.metric('auth_operations', duration, {
851
+ operation: 'authenticate_apicall',
852
+ result: 'success',
853
+ reason: 'Authentication successful'
854
+ });
855
+
856
+ next();
857
+ } catch (error) {
858
+ const duration = Date.now() - startTime;
859
+ logger.debugWithContext("Authentication rejected by exception", {
860
+ ...baseContext,
861
+ decision: "rejected",
862
+ reason: error.message || "Authentication failed",
863
+ errorName: error.name,
864
+ errorCode: error.code
865
+ });
866
+ logErrorWithMetrics(
867
+ "Authentication error",
868
+ {
869
+ ...baseContext,
870
+ duration,
871
+ result: 'failure',
872
+ reason: error.message || 'Authentication failed'
873
+ },
874
+ error,
875
+ "auth_error",
876
+ { error_type: "authentication_error" }
877
+ );
878
+ // Add metric for authentication error
879
+ logger.metric('auth_operations', duration, {
880
+ operation: 'authenticate_apicall',
881
+ result: 'failure',
882
+ reason: error.message || 'Authentication failed'
883
+ });
884
+
885
+ return sendError(res, {
886
+ statusCode: 500,
887
+ requestId,
888
+ code: "AUTH_ERROR",
889
+ message: "Authentication failed",
890
+ details: !isStrictEnvironment() ? { cause: error.message } : undefined
891
+ });
892
+ }
893
+ }
894
+
895
+ /**
896
+ * Middleware to authenticate logout calls.
897
+ * Allows signature-valid expired tokens so sessions can be closed cleanly.
898
+ *
899
+ * @param {Object} req - Express request object
900
+ * @param {Object} res - Express response object
901
+ * @param {Function} next - Next middleware function
902
+ */
903
+ async function authenticate_logout(req, res, next) {
904
+ const requestId = ulid();
905
+ const startTime = Date.now();
906
+ const baseContext = createLogContext(
907
+ "AuthMiddleware",
908
+ "authenticate_logout",
909
+ {
910
+ requestId,
911
+ path: req.path,
912
+ method: req.method
913
+ }
914
+ );
915
+
916
+ try {
917
+ verifySessionManager();
918
+ const jwt_token = extractTokenFromHeader(req.headers.authorization);
919
+ if (!jwt_token) {
920
+ return sendError(res, {
921
+ statusCode: 401,
922
+ requestId,
923
+ code: "MISSING_TOKEN",
924
+ message: "No jwt_token provided"
925
+ });
926
+ }
927
+
928
+ const config_own_rodit = await stateManager.getConfigOwnRodit();
929
+ if (!config_own_rodit || !config_own_rodit.own_rodit) {
930
+ return sendError(res, {
931
+ statusCode: 500,
932
+ requestId,
933
+ code: "SERVER_CONFIG_ERROR",
934
+ message: "Server configuration not initialized"
935
+ });
936
+ }
937
+
938
+ // Logout-specific auth: signature and claims must be valid, expiration is tolerated.
939
+ const validationResult = await validate_jwt_token_be(
940
+ jwt_token,
941
+ config_own_rodit.own_rodit,
942
+ { allowExpiredToken: true }
943
+ );
944
+
945
+ if (!validationResult.valid) {
946
+ return sendError(res, {
947
+ statusCode: 403,
948
+ requestId,
949
+ code: validationResult.errorCode || "INVALID_TOKEN",
950
+ message: validationResult.error || "Invalid jwt_token"
951
+ });
952
+ }
953
+
954
+ req.user = validationResult.payload;
955
+ req.jwt_token = jwt_token;
956
+
957
+ logger.infoWithContext("Logout authentication successful", {
958
+ ...baseContext,
959
+ duration: Date.now() - startTime,
960
+ userId: req.user?.sub
961
+ });
962
+ return next();
963
+ } catch (error) {
964
+ logger.debugWithContext("Logout authentication failed", {
965
+ ...baseContext,
966
+ duration: Date.now() - startTime,
967
+ error: error.message
968
+ });
969
+ return sendError(res, {
970
+ statusCode: 403,
971
+ requestId,
972
+ code: error.code || "INVALID_TOKEN",
973
+ message: error.message || "Invalid jwt_token"
974
+ });
975
+ }
976
+ }
977
+
978
+ /**
979
+ * Handle client logout
980
+ *
981
+ * @param {Object} req - Express request object
982
+ * @param {Object} res - Express response object
983
+ * @returns {Object} Response object
984
+ */
985
+ async function logout_client(req, res) {
986
+ const requestId = ulid();
987
+ const startTime = Date.now();
988
+
989
+ // Create a base context for this function
990
+ const baseContext = createLogContext(
991
+ "AuthenticationService",
992
+ "logout_client",
993
+ {
994
+ requestId,
995
+ path: req.path,
996
+ method: req.method,
997
+ ip: req.ip
998
+ }
999
+ );
1000
+
1001
+ logger.infoWithContext("Logout request received", {
1002
+ ...baseContext,
1003
+ userAgent: req.get("User-Agent")
1004
+ });
1005
+
1006
+ try {
1007
+ // Verify sessionManager is properly initialized before using it
1008
+ verifySessionManager();
1009
+
1010
+ // Extract jwt_token from authorization header
1011
+ const jwt_token =
1012
+ req.headers.authorization &&
1013
+ req.headers.authorization.startsWith("Bearer ")
1014
+ ? req.headers.authorization.substring(7)
1015
+ : null;
1016
+
1017
+ if (!jwt_token) {
1018
+ const duration = Date.now() - startTime;
1019
+
1020
+ // Emit metrics for unauthorized logout attempts
1021
+ logger.metric &&
1022
+ logger.metric("logout_attempts", 1, {
1023
+ component: "AuthenticationService",
1024
+ result: "no_jwt_token",
1025
+ });
1026
+
1027
+ return sendError(res, {
1028
+ statusCode: 401,
1029
+ requestId,
1030
+ code: "MISSING_TOKEN",
1031
+ message: "No authentication jwt_token provided"
1032
+ });
1033
+ }
1034
+
1035
+ // Decode the jwt_token to get session information
1036
+ // We're just decoding, not verifying, since even if the jwt_token is expired
1037
+ // we still want to be able to log the user out
1038
+ let decodedToken;
1039
+ try {
1040
+ // Split the jwt_token and decode the payload (middle part)
1041
+ const parts = jwt_token.split(".");
1042
+ if (parts.length !== 3) {
1043
+ throw new Error("Invalid jwt_token format");
1044
+ }
1045
+
1046
+ const payload = Buffer.from(parts[1], "base64url").toString();
1047
+ decodedToken = JSON.parse(payload);
1048
+ } catch (decodeError) {
1049
+ logErrorWithMetrics(
1050
+ "Failed to decode jwt_token for logout",
1051
+ {
1052
+ ...baseContext,
1053
+ jwt_tokenLength: jwt_token?.length
1054
+ },
1055
+ decodeError,
1056
+ "logout_error",
1057
+ { error_type: "jwt_token_decode_error" }
1058
+ );
1059
+
1060
+ // Continue with a partial logout even if jwt_token can't be decoded
1061
+ decodedToken = {};
1062
+ }
1063
+
1064
+ // Track success for metrics
1065
+ let logoutSuccess = false;
1066
+ let sessionClosed = false;
1067
+ let sessionStatus = "unknown";
1068
+ let jwt_tokenInvalidated = null;
1069
+ let finalToken = null;
1070
+
1071
+ // Close the session if session_id is available
1072
+ if (decodedToken.session_id) {
1073
+ try {
1074
+ // Get the reason from request body or use default
1075
+ const reason = (req.body && req.body.reason) || "user_logout";
1076
+
1077
+ // Invalidate the jwt_token by closing its session
1078
+ jwt_tokenInvalidated = await sessionManager.invalidateToken(jwt_token, reason, decodedToken.session_id);
1079
+
1080
+ logger.infoWithContext("Token invalidation result (session-based)", {
1081
+ ...baseContext,
1082
+ jwt_tokenInvalidated,
1083
+ jwt_tokenLength: jwt_token.length,
1084
+ reason,
1085
+ sessionId: decodedToken.session_id,
1086
+ method: "session_closure"
1087
+ });
1088
+
1089
+ // Verify the token was actually invalidated by checking session state
1090
+ const verifyInvalidation = await sessionManager.isTokenInvalidated(jwt_token);
1091
+ const invalidationInfo = await sessionManager.getTokenInvalidationInfo(jwt_token);
1092
+
1093
+ logger.infoWithContext("Token invalidation verification (session-based)", {
1094
+ ...baseContext,
1095
+ verifyInvalidation,
1096
+ expectedInvalidated: true,
1097
+ invalidationWorking: verifyInvalidation === true,
1098
+ sessionId: decodedToken.session_id,
1099
+ invalidationInfo: invalidationInfo ? {
1100
+ reason: invalidationInfo.reason,
1101
+ invalidatedAt: invalidationInfo.invalidatedAt,
1102
+ sessionId: invalidationInfo.sessionId
1103
+ } : null
1104
+ });
1105
+
1106
+ // Critical security check - log if invalidation failed
1107
+ if (!verifyInvalidation) {
1108
+ logger.errorWithContext("CRITICAL: Token invalidation failed - security risk!", {
1109
+ ...baseContext,
1110
+ jwt_tokenInvalidated,
1111
+ verifyInvalidation,
1112
+ securityIssue: true
1113
+ });
1114
+ } else {
1115
+ logger.infoWithContext("SECURITY: Token successfully invalidated", {
1116
+ ...baseContext,
1117
+ securityConfirmed: true
1118
+ });
1119
+ }
1120
+
1121
+ // Then close the session
1122
+ sessionClosed = await sessionManager.closeSession(
1123
+ decodedToken.session_id,
1124
+ reason,
1125
+ null // Don't pass jwt_token here since we've already invalidated it
1126
+ );
1127
+
1128
+ logger.infoWithContext("Session closure result", {
1129
+ ...baseContext,
1130
+ sessionClosed
1131
+ });
1132
+
1133
+ // Update tracking variables for metrics and response
1134
+ // Primary requirement: JWT token must be invalidated for security
1135
+ // Secondary requirement: Session closure (but not critical if session was already cleaned up)
1136
+ logoutSuccess = jwt_tokenInvalidated; // Token invalidation is the critical security requirement
1137
+
1138
+ logger.infoWithContext("Logout success calculation", {
1139
+ ...baseContext,
1140
+ jwt_tokenInvalidated,
1141
+ sessionClosed,
1142
+ logoutSuccess,
1143
+ primaryRequirement: "jwt_token_invalidated",
1144
+ secondaryRequirement: "session_closed",
1145
+ securitySatisfied: jwt_tokenInvalidated
1146
+ });
1147
+
1148
+ // Determine the overall session status
1149
+ if (jwt_tokenInvalidated && sessionClosed) {
1150
+ sessionStatus = "closed_complete";
1151
+ } else if (jwt_tokenInvalidated) {
1152
+ sessionStatus = "closed_jwt_token_only";
1153
+ } else if (sessionClosed) {
1154
+ sessionStatus = "closed_session_only";
1155
+ } else {
1156
+ sessionStatus = "close_failed";
1157
+ }
1158
+
1159
+ // Generate a final jwt_token with session_status="closed"
1160
+ try {
1161
+ // Import the tokenservice dynamically to avoid circular dependencies
1162
+ const jwt_tokenService = require('../auth/tokenservice');
1163
+
1164
+ // Generate a final jwt_token with very short expiration (1 minute)
1165
+ // This jwt_token is just for status communication, not for authentication
1166
+ finalToken = await jwt_tokenService.generate_session_termination_token(
1167
+ decodedToken,
1168
+ 60 // 1 minute duration
1169
+ );
1170
+
1171
+ logger.infoWithContext("Generated final jwt_token with closed status", {
1172
+ ...baseContext,
1173
+ hasToken: !!finalToken
1174
+ });
1175
+ } catch (jwt_tokenError) {
1176
+ logErrorWithMetrics(
1177
+ "Failed to generate final jwt_token",
1178
+ baseContext,
1179
+ jwt_tokenError,
1180
+ "logout_error",
1181
+ { error_type: "jwt_token_generation_error" }
1182
+ );
1183
+ }
1184
+ } catch (sessionError) {
1185
+ logErrorWithMetrics(
1186
+ "Error closing session",
1187
+ {
1188
+ ...baseContext,
1189
+ sessionId: decodedToken.session_id
1190
+ },
1191
+ sessionError,
1192
+ "logout_error",
1193
+ { error_type: "session_closure_error" }
1194
+ );
1195
+
1196
+ // Continue with logout process even if session closing fails
1197
+ }
1198
+ } else {
1199
+ // We still consider this a success since there's no session to log out from
1200
+ logoutSuccess = true;
1201
+ }
1202
+
1203
+ // Clear auth headers if they exist
1204
+ if (typeof res.removeHeader === 'function') {
1205
+ res.removeHeader("Authorization");
1206
+ }
1207
+
1208
+ // Set the final jwt_token in the response header if available
1209
+ if (finalToken) {
1210
+ res.set("New-Token", finalToken);
1211
+ }
1212
+
1213
+ const duration = Date.now() - startTime;
1214
+ logger.infoWithContext("Logout completed", {
1215
+ ...baseContext,
1216
+ duration,
1217
+ success: logoutSuccess,
1218
+ sessionClosed,
1219
+ hasSessionId: !!decodedToken.session_id
1220
+ });
1221
+
1222
+ // Emit metrics for logout
1223
+ logger.metric &&
1224
+ logger.metric("logout_duration_ms", duration, {
1225
+ component: "AuthenticationService",
1226
+ success: logoutSuccess,
1227
+ session_closed: sessionClosed,
1228
+ session_status: sessionStatus
1229
+ });
1230
+
1231
+ logger.metric &&
1232
+ logger.metric("logout_attempts", 1, {
1233
+ component: "AuthenticationService",
1234
+ result: logoutSuccess ? "success" : "failure",
1235
+ session_closed: sessionClosed,
1236
+ session_status: sessionStatus
1237
+ });
1238
+
1239
+ return res.json({
1240
+ message: "Logout successful",
1241
+ sessionClosed,
1242
+ sessionStatus,
1243
+ jwt_tokenInvalidated,
1244
+ requestId,
1245
+ });
1246
+ } catch (error) {
1247
+ const duration = Date.now() - startTime;
1248
+
1249
+ logErrorWithMetrics(
1250
+ "Logout process failed",
1251
+ {
1252
+ ...baseContext,
1253
+ duration
1254
+ },
1255
+ error,
1256
+ "logout_error",
1257
+ { error_type: "general_logout_error" }
1258
+ );
1259
+
1260
+ // Emit metrics for logout errors
1261
+ logger.metric &&
1262
+ logger.metric("logout_duration_ms", duration, {
1263
+ component: "AuthenticationService",
1264
+ success: false,
1265
+ error: error.constructor.name,
1266
+ });
1267
+
1268
+ logger.metric &&
1269
+ logger.metric("logout_errors", 1, {
1270
+ component: "AuthenticationService",
1271
+ error: error.constructor.name,
1272
+ });
1273
+
1274
+ return sendError(res, {
1275
+ statusCode: 500,
1276
+ requestId,
1277
+ code: "LOGOUT_ERROR",
1278
+ message: "Internal server error during logout",
1279
+ details: !isStrictEnvironment() ? { error: error.message } : undefined
1280
+ });
1281
+ }
1282
+ }
1283
+
1284
+ /**
1285
+ * Handle client login with NEP-413 standard
1286
+ *
1287
+ * @param {Object} req - Express request object
1288
+ * @param {Object} res - Express response object
1289
+ * @param {Object} config_own_rodit - Own RODiT configuration
1290
+ * @returns {Object} Response with JWT jwt_token or error
1291
+ */
1292
+ async function login_client_withnep413(req, res, config_own_rodit = null) {
1293
+ const requestId = ulid();
1294
+ const startTime = Date.now();
1295
+
1296
+ logger.info("NEP-413 login request received", {
1297
+ component: "AuthenticationService",
1298
+ method: "login_client_withnep413",
1299
+ requestId,
1300
+ });
1301
+
1302
+ try {
1303
+ const { signature, message, nonce, recipient, callbackUrl } = req.body;
1304
+
1305
+ logger.debug("Received NEP-413 login parameters", {
1306
+ component: "AuthenticationService",
1307
+ method: "login_client_withnep413",
1308
+ requestId,
1309
+ message,
1310
+ recipient,
1311
+ hasSignature: !!signature,
1312
+ hasNonce: !!nonce,
1313
+ hasCallbackUrl: !!callbackUrl,
1314
+ });
1315
+
1316
+ if (!config_own_rodit) {
1317
+ const duration = Date.now() - startTime;
1318
+
1319
+ logger.error("Server configuration not initialized for NEP-413 login", {
1320
+ component: "AuthenticationService",
1321
+ method: "login_client_withnep413",
1322
+ requestId,
1323
+ duration,
1324
+ errorCode: "CONFIG_NOT_INITIALIZED",
1325
+ });
1326
+
1327
+ // Emit metrics for dashboards
1328
+ logger.metric("nep413_login_duration_ms", duration, {
1329
+ component: "AuthenticationService",
1330
+ success: false,
1331
+ error: "CONFIG_NOT_INITIALIZED",
1332
+ });
1333
+ logger.metric("failed_nep413_logins_total", 1, {
1334
+ component: "AuthenticationService",
1335
+ reason: "CONFIG_NOT_INITIALIZED",
1336
+ });
1337
+
1338
+ throw new Error("Error 0114: Server configuration not initialized");
1339
+ }
1340
+
1341
+ logger.debug("Verifying NEP-413 RODiT credentials", {
1342
+ component: "AuthenticationService",
1343
+ method: "login_client_withnep413",
1344
+ requestId,
1345
+ });
1346
+
1347
+ // Declare peer_rodit outside the try block so it's accessible throughout the function
1348
+ let peer_rodit;
1349
+
1350
+ try {
1351
+ // First, fetch the peer RODiT using message (which contains the RODiT)
1352
+ peer_rodit = await nearorg_rpc_tokenfromroditid(message);
1353
+
1354
+ if (!peer_rodit || !peer_rodit.token_id) {
1355
+ logger.error("Failed to retrieve peer RODiT data", {
1356
+ component: "AuthenticationService",
1357
+ method: "login_client_withnep413",
1358
+ requestId,
1359
+ message
1360
+ });
1361
+ throw new Error("Error 0115: Invalid RODiT");
1362
+ }
1363
+
1364
+ // Now verify the signature using NEP-413 parameters
1365
+ const isRoditValid = await verify_rodit_ownership_withnep413(
1366
+ message,
1367
+ nonce,
1368
+ recipient,
1369
+ callbackUrl,
1370
+ signature,
1371
+ peer_rodit
1372
+ );
1373
+
1374
+ if (!isRoditValid) {
1375
+ const duration = Date.now() - startTime;
1376
+
1377
+ logger.warn("NEP-413 login failed - Invalid RODiT credentials", {
1378
+ component: "AuthenticationService",
1379
+ method: "login_client_withnep413",
1380
+ requestId,
1381
+ duration,
1382
+ message,
1383
+ });
1384
+
1385
+ // Emit metrics for dashboards
1386
+ logger.metric("nep413_login_duration_ms", duration, {
1387
+ component: "AuthenticationService",
1388
+ success: false,
1389
+ error: "INVALID_CREDENTIALS",
1390
+ });
1391
+ logger.metric("failed_nep413_logins_total", 1, {
1392
+ component: "AuthenticationService",
1393
+ reason: "INVALID_CREDENTIALS",
1394
+ });
1395
+
1396
+ return sendError(res, {
1397
+ statusCode: 401,
1398
+ requestId,
1399
+ code: "INVALID_CREDENTIALS",
1400
+ message:
1401
+ "Error 106: Login attempt failed: Invalid RODiT or Signature"
1402
+ });
1403
+ }
1404
+
1405
+ } catch (innerError) {
1406
+ const duration = Date.now() - startTime;
1407
+ logger.error(`NEP-413 verification error: ${innerError.message}`, {
1408
+ component: "AuthenticationService",
1409
+ method: "login_client_withnep413",
1410
+ requestId,
1411
+ duration,
1412
+ error: innerError.message,
1413
+ });
1414
+
1415
+ return sendError(res, {
1416
+ statusCode: 401,
1417
+ requestId,
1418
+ code: "LOGIN_VERIFICATION_FAILED",
1419
+ message: `Error 107: Login verification failed: ${innerError.message}`
1420
+ });
1421
+ }
1422
+
1423
+ const jwt_token = await generate_jwt_token(
1424
+ peer_rodit,
1425
+ Math.floor(Date.now() / 1000),
1426
+ config_own_rodit.own_rodit,
1427
+ config_own_rodit.own_rodit_bytes_private_key
1428
+ );
1429
+
1430
+ const duration = Date.now() - startTime;
1431
+ logger.info("NEP-413 login successful", {
1432
+ component: "AuthenticationService",
1433
+ method: "login_client_withnep413",
1434
+ requestId,
1435
+ duration,
1436
+ roditId: peer_rodit.token_id,
1437
+ });
1438
+
1439
+ // Emit metrics for dashboards
1440
+ logger.metric("nep413_login_duration_ms", duration, {
1441
+ component: "AuthenticationService",
1442
+ success: true,
1443
+ });
1444
+ logger.metric("successful_nep413_logins_total", 1, {
1445
+ component: "AuthenticationService",
1446
+ });
1447
+
1448
+ // Log the response being sent to frontend
1449
+ logger.info("Sending NEP-413 login response to frontend", {
1450
+ component: "AuthenticationService",
1451
+ method: "login_client_withnep413",
1452
+ requestId,
1453
+ response: {
1454
+ requestId: requestId,
1455
+ jwt_token_length: jwt_token ? jwt_token.length : 0
1456
+ }
1457
+ });
1458
+
1459
+ return res.json({
1460
+ jwt_token,
1461
+ requestId,
1462
+ });
1463
+ } catch (error) {
1464
+ const duration = Date.now() - startTime;
1465
+
1466
+ logger.error("NEP-413 login failed", {
1467
+ component: "AuthenticationService",
1468
+ method: "login_client_withnep413",
1469
+ requestId,
1470
+ duration,
1471
+ errorMessage: error.message,
1472
+ errorCode: error.code || "UNKNOWN_ERROR",
1473
+ stack: error.stack,
1474
+ });
1475
+
1476
+ // Emit metrics for dashboards
1477
+ logger.metric("nep413_login_duration_ms", duration, {
1478
+ component: "AuthenticationService",
1479
+ success: false,
1480
+ error: error.code || "UNKNOWN_ERROR",
1481
+ });
1482
+ logger.metric("failed_nep413_logins_total", 1, {
1483
+ component: "AuthenticationService",
1484
+ reason: error.code || "UNKNOWN_ERROR",
1485
+ });
1486
+
1487
+ return sendError(res, {
1488
+ statusCode: 500,
1489
+ requestId,
1490
+ code: error.code || "NEP413_LOGIN_ERROR",
1491
+ message: `Error 175c: Login attempt failed: ${error.message}`
1492
+ });
1493
+ }
1494
+ }
1495
+
1496
+ /**
1497
+ * Login the server to a RODiT portal
1498
+ *
1499
+ * @param {Object} config_own_rodit - Configuration object containing own_rodit and other settings
1500
+ * @param {number} port - Optional port number for the portal URL
1501
+ * @param {Object} [options] - Optional settings
1502
+ * @param {number} [options.timestamp] - Unix seconds used for signature generation (if omitted, local current time is used)
1503
+ * @param {string} [options.accountId] - Explicit NEAR account for outbound login when token id absent
1504
+ * @param {string} [options.loginPath] - HTTP path (default /api/login)
1505
+ * @returns {Promise<Object>} Login result
1506
+ */
1507
+ async function login_portal(config_own_rodit, port, options = {}) {
1508
+ const requestId = ulid();
1509
+ const startTime = Date.now();
1510
+
1511
+ // Access the own_rodit object from the config
1512
+ const own_rodit = config_own_rodit.own_rodit;
1513
+
1514
+ logger.info("Starting portal login process", {
1515
+ component: "AuthenticationService",
1516
+ method: "login_portal",
1517
+ requestId,
1518
+ roditId: own_rodit?.token_id,
1519
+ });
1520
+
1521
+ try {
1522
+ logger.debug("Using provided configuration", {
1523
+ component: "AuthenticationService",
1524
+ method: "login_portal",
1525
+ requestId,
1526
+ hasConfig: !!config_own_rodit,
1527
+ api_ep: config_own_rodit?.apiendpoint,
1528
+ });
1529
+
1530
+ if (!config_own_rodit) {
1531
+ const duration = Date.now() - startTime;
1532
+
1533
+ logger.error("Client configuration not initialized", {
1534
+ component: "AuthenticationService",
1535
+ method: "login_portal",
1536
+ requestId,
1537
+ duration,
1538
+ errorCode: "CONFIG_NOT_INITIALIZED",
1539
+ });
1540
+
1541
+ // Emit metrics for dashboards
1542
+ logger.metric("portal_login_duration_ms", duration, {
1543
+ component: "AuthenticationService",
1544
+ success: false,
1545
+ error: "CONFIG_NOT_INITIALIZED",
1546
+ });
1547
+ logger.metric("portal_login_errors_total", 1, {
1548
+ component: "AuthenticationService",
1549
+ error: "CONFIG_NOT_INITIALIZED",
1550
+ });
1551
+
1552
+ return {
1553
+ error: "Client configuration not initialized",
1554
+ requestId,
1555
+ };
1556
+ }
1557
+
1558
+ // Check RODiT metadata
1559
+ if (!own_rodit.metadata || !own_rodit.metadata.serviceprovider_id) {
1560
+ const duration = Date.now() - startTime;
1561
+
1562
+ logger.error("Missing serviceprovider_id in RODiT", {
1563
+ component: "AuthenticationService",
1564
+ method: "login_portal",
1565
+ requestId,
1566
+ duration,
1567
+ roditId: own_rodit?.token_id,
1568
+ hasMetadata: !!own_rodit?.metadata,
1569
+ });
1570
+
1571
+ // Emit metrics for dashboards
1572
+ logger.metric("portal_login_duration_ms", duration, {
1573
+ component: "AuthenticationService",
1574
+ success: false,
1575
+ error: "MISSING_METADATA",
1576
+ });
1577
+ logger.metric("portal_login_errors_total", 1, {
1578
+ component: "AuthenticationService",
1579
+ error: "MISSING_METADATA",
1580
+ });
1581
+
1582
+ return {
1583
+ error: "Missing serviceprovider_id in RODiT",
1584
+ requestId,
1585
+ };
1586
+ }
1587
+
1588
+ // Use stateManager's getPortalUrl method to get API endpoint
1589
+ const serviceProviderId = own_rodit.metadata.serviceprovider_id;
1590
+ const apiendpoint = stateManager.getPortalUrl(
1591
+ serviceProviderId,
1592
+ port
1593
+ );
1594
+
1595
+ logger.info("Using portal endpoint", {
1596
+ component: "AuthenticationService",
1597
+ method: "login_portal",
1598
+ requestId,
1599
+ api_ep: apiendpoint,
1600
+ });
1601
+
1602
+ // Prepare authentication data using the same payload contract as login_client.
1603
+ const roditid = normalizeOptionalLoginString(own_rodit?.token_id);
1604
+ const accountid = normalizeOptionalServerAccountId(options.accountId);
1605
+ const timestamp = parseRequiredServerLoginTimestamp(options.timestamp)
1606
+ ?? Math.floor(Date.now() / 1000);
1607
+ if (timestamp === null) {
1608
+ return {
1609
+ error: "Missing or invalid options.timestamp",
1610
+ errorCode: "INVALID_LOGIN_TIMESTAMP",
1611
+ failureReason: "INVALID_LOGIN_TIMESTAMP",
1612
+ requestId
1613
+ };
1614
+ }
1615
+
1616
+ const hasRoditId = roditid.length > 0;
1617
+ const hasAccountId = accountid.length > 0;
1618
+ if (hasRoditId === hasAccountId) {
1619
+ return {
1620
+ error: "Provide exactly one signing identifier: own_rodit.token_id or options.accountId",
1621
+ errorCode: "LOGIN_IDENTIFIER_AMBIGUOUS",
1622
+ failureReason: "LOGIN_IDENTIFIER_AMBIGUOUS",
1623
+ requestId
1624
+ };
1625
+ }
1626
+
1627
+ const timeString = await unixTimeToDateString(timestamp);
1628
+ const signatureIdentifier = hasRoditId ? roditid : accountid;
1629
+ const signatureIdentifierandtimestamp = new TextEncoder().encode(
1630
+ signatureIdentifier + timeString
1631
+ );
1632
+
1633
+ logger.debug("Generating authentication signature", {
1634
+ component: "AuthenticationService",
1635
+ method: "login_portal",
1636
+ requestId,
1637
+ roditId: roditid,
1638
+ accountId: accountid,
1639
+ timestamp,
1640
+ });
1641
+
1642
+ // Create signature
1643
+ const own_rodit_bytes_signature = nacl.sign.detached(
1644
+ signatureIdentifierandtimestamp,
1645
+ config_own_rodit.own_rodit_bytes_private_key
1646
+ );
1647
+ const roditid_base64url_signature = Buffer.from(
1648
+ own_rodit_bytes_signature
1649
+ ).toString("base64url");
1650
+
1651
+ const loginPath =
1652
+ options.loginPath ??
1653
+ config_own_rodit.login_rodit_path ??
1654
+ config.get("LOGIN_RODIT_PATH", "/api/login");
1655
+ const fetchUrl = buildLoginUrl(apiendpoint, loginPath);
1656
+
1657
+ logger.debug("Sending login request to portal", {
1658
+ component: "AuthenticationService",
1659
+ method: "login_portal",
1660
+ requestId,
1661
+ apiEndpoint: fetchUrl,
1662
+ });
1663
+
1664
+ try {
1665
+ const response = await fetch(fetchUrl, {
1666
+ method: "POST",
1667
+ headers: {
1668
+ "Content-Type": "application/json",
1669
+ },
1670
+ body: JSON.stringify({
1671
+ ...(hasRoditId ? { roditid } : {}),
1672
+ ...(hasAccountId ? { accountid } : {}),
1673
+ timestamp,
1674
+ roditid_base64url_signature,
1675
+ }),
1676
+ });
1677
+
1678
+ if (!response.ok) {
1679
+ const duration = Date.now() - startTime;
1680
+
1681
+ // Enhanced error logging with clear cause and effect
1682
+ logger.error(`Portal login request failed: HTTP ${response.status} response from SignPortal`, {
1683
+ component: "AuthenticationService",
1684
+ method: "login_portal",
1685
+ requestId,
1686
+ duration,
1687
+ status: response.status,
1688
+ statusText: response.statusText,
1689
+ apiEndpoint: fetchUrl,
1690
+ reason: `SignPortal server returned error status ${response.status} (${response.statusText})`,
1691
+ impact: "Cannot obtain authentication jwt_token due to server-side error"
1692
+ });
1693
+
1694
+ // Emit metrics for dashboards
1695
+ logger.metric("portal_login_duration_ms", duration, {
1696
+ component: "AuthenticationService",
1697
+ success: false,
1698
+ error: "HTTP_ERROR",
1699
+ status: response.status,
1700
+ });
1701
+ logger.metric("portal_login_errors_total", 1, {
1702
+ component: "AuthenticationService",
1703
+ error: "HTTP_ERROR",
1704
+ status: response.status,
1705
+ });
1706
+
1707
+ throw new Error(
1708
+ `Error 040: Portal login failed with status ${response.status}`
1709
+ );
1710
+ }
1711
+
1712
+ const data = await response.json();
1713
+ let jwt_token = data.jwt_token;
1714
+
1715
+ // Validate JWT jwt_token
1716
+ try {
1717
+ // First, decode the JWT without verification to get the rodit_id
1718
+ const { decodeJwt } = await getJose();
1719
+ const unverifiedPayload = decodeJwt(jwt_token);
1720
+ const peerRoditId = unverifiedPayload.rodit_id;
1721
+
1722
+ // Fetch the peer RODiT information directly from the blockchain
1723
+ const peer_rodit = await nearorg_rpc_tokenfromroditid(peerRoditId);
1724
+
1725
+ logger.debug("Fetched peer RODiT for validation", {
1726
+ component: "AuthenticationService",
1727
+ method: "login_portal",
1728
+ requestId,
1729
+ peer_rodit: {
1730
+ token_id: peer_rodit?.token_id,
1731
+ owner_id: peer_rodit?.owner_id,
1732
+ metadata: {
1733
+ serviceprovider_id: peer_rodit?.metadata?.serviceprovider_id
1734
+ }
1735
+ }
1736
+ });
1737
+
1738
+ // Now perform the full validation
1739
+ const validationResult = await validate_jwt_token_be(
1740
+ jwt_token,
1741
+ peer_rodit,
1742
+ RELAXED_SESSION_VALIDATION_OPTIONS
1743
+ );
1744
+
1745
+ } catch (validationError) {
1746
+ const duration = Date.now() - startTime;
1747
+
1748
+ // Enhanced error logging with clear cause and effect
1749
+ logger.error("JWT jwt_token validation failed: Token received from portal is invalid", {
1750
+ component: "AuthenticationService",
1751
+ method: "login_portal",
1752
+ requestId,
1753
+ duration,
1754
+ errorMessage: validationError.message,
1755
+ errorType: validationError.name,
1756
+ stack: validationError.stack,
1757
+ reason: `JWT validation error: ${validationError.message}`,
1758
+ impact: "Cannot use the received jwt_token for authentication"
1759
+ });
1760
+
1761
+ // Emit metrics for dashboards
1762
+ logger.metric("portal_login_duration_ms", duration, {
1763
+ component: "AuthenticationService",
1764
+ success: false,
1765
+ error: "JWT_VALIDATION_FAILED",
1766
+ });
1767
+ logger.metric("portal_login_errors_total", 1, {
1768
+ component: "AuthenticationService",
1769
+ error: "JWT_VALIDATION_FAILED",
1770
+ });
1771
+
1772
+ throw new Error(
1773
+ `Error 039: Portal server validation failed: ${validationError.message}`
1774
+ );
1775
+ }
1776
+
1777
+ const duration = Date.now() - startTime;
1778
+ logger.info("Portal login successful", {
1779
+ component: "AuthenticationService",
1780
+ method: "login_portal",
1781
+ requestId,
1782
+ duration,
1783
+ api_ep: apiendpoint,
1784
+ });
1785
+
1786
+ // Emit metrics for dashboards
1787
+ logger.metric("portal_login_duration_ms", duration, {
1788
+ component: "AuthenticationService",
1789
+ success: true,
1790
+ });
1791
+ logger.metric("successful_portal_logins_total", 1, {
1792
+ component: "AuthenticationService",
1793
+ apiEndpoint: apiendpoint,
1794
+ });
1795
+
1796
+ return {
1797
+ jwt_token,
1798
+ apiendpoint,
1799
+ requestId,
1800
+ };
1801
+ } catch (fetchError) {
1802
+ const duration = Date.now() - startTime;
1803
+
1804
+ // Enhanced error logging with clear cause and effect
1805
+ logger.error("Portal fetch operation failed: Unable to connect to SignPortal endpoint", {
1806
+ component: "AuthenticationService",
1807
+ method: "login_portal",
1808
+ requestId,
1809
+ duration,
1810
+ errorMessage: fetchError.message,
1811
+ errorType: fetchError.name,
1812
+ stack: fetchError.stack,
1813
+ apiEndpoint: fetchUrl,
1814
+ reason: "Network connectivity issue or service unavailable",
1815
+ impact: "Authentication process cannot proceed without portal connection"
1816
+ });
1817
+
1818
+ // Emit metrics for dashboards
1819
+ logger.metric("portal_login_duration_ms", duration, {
1820
+ component: "AuthenticationService",
1821
+ success: false,
1822
+ error: "FETCH_FAILED",
1823
+ });
1824
+ logger.metric("portal_login_errors_total", 1, {
1825
+ component: "AuthenticationService",
1826
+ error: "FETCH_FAILED",
1827
+ apiEndpoint: fetchUrl,
1828
+ });
1829
+
1830
+ throw fetchError;
1831
+ }
1832
+ } catch (error) {
1833
+ const duration = Date.now() - startTime;
1834
+
1835
+ // Enhanced error logging with clear cause and effect
1836
+ const errorType = error.name || error.constructor.name;
1837
+ const errorReason = error.message || 'Unknown error';
1838
+
1839
+ logger.error(`Portal login process failed: ${errorType}`, {
1840
+ component: "AuthenticationService",
1841
+ method: "login_portal",
1842
+ requestId,
1843
+ duration,
1844
+ errorMessage: error.message,
1845
+ errorType: errorType,
1846
+ stack: error.stack,
1847
+ roditId: own_rodit?.token_id,
1848
+ reason: errorReason,
1849
+ impact: "Unable to authenticate with SignPortal, client operations requiring authentication will fail"
1850
+ });
1851
+
1852
+ // Emit metrics for dashboards
1853
+ logger.metric("portal_login_duration_ms", duration, {
1854
+ component: "AuthenticationService",
1855
+ success: false,
1856
+ error: error.constructor.name,
1857
+ });
1858
+ logger.metric("portal_login_errors_total", 1, {
1859
+ component: "AuthenticationService",
1860
+ error: error.constructor.name,
1861
+ });
1862
+
1863
+ // Return structured error information
1864
+ return {
1865
+ error: `Failed to login to portal: ${error.message}`,
1866
+ reason: error.name || error.constructor.name,
1867
+ details: error.message,
1868
+ impact: "Authentication with SignPortal failed, client operations requiring authentication will fail",
1869
+ requestId,
1870
+ };
1871
+ }
1872
+ }
1873
+
1874
+ /**
1875
+ * Login to a peer API (POST /api/login shape expected by the peer). Signs roditid+timestamp when
1876
+ * own_rodit.token_id is set; otherwise signs NEAR account id + timestamp when options/config supply an account.
1877
+ * Body uses roditid_base64url_signature (stable wire field name). Peer login_client accepts this field or base64url_signature (same bytes).
1878
+ *
1879
+ * @param {Object} config_own_rodit - Configuration object containing own_rodit and private key
1880
+ * @param {Object} [options] - Optional settings
1881
+ * @param {string} [options.loginPath] - HTTP path (default /api/login)
1882
+ * @param {number} [options.timestamp] - Unix seconds used for signature generation (if omitted, fetched from peer /api/login/timestamp)
1883
+ * @param {string} [options.accountId] - Explicit NEAR account for outbound login when token id absent
1884
+ * @param {string} [options.timestampPath] - Timestamp endpoint path (default /api/login/timestamp)
1885
+ * @returns {Promise<Object>} Login result
1886
+ */
1887
+ async function login_server(config_own_rodit, options = {}) {
1888
+ const requestId = ulid();
1889
+ const startTime = Date.now();
1890
+ const method = "login_server";
1891
+
1892
+ const own_rodit = config_own_rodit?.own_rodit;
1893
+
1894
+ logger.info("Starting login_server process", {
1895
+ component: "AuthenticationService",
1896
+ method,
1897
+ requestId,
1898
+ roditId: own_rodit?.token_id,
1899
+ });
1900
+
1901
+ try {
1902
+ logger.debug("Retrieved config from state manager", {
1903
+ component: "AuthenticationService",
1904
+ method,
1905
+ requestId,
1906
+ hasConfig: !!config_own_rodit,
1907
+ api_ep: config_own_rodit?.apiendpoint,
1908
+ });
1909
+
1910
+ if (!config_own_rodit) {
1911
+ const duration = Date.now() - startTime;
1912
+
1913
+ logger.error("Client configuration not initialized", {
1914
+ component: "AuthenticationService",
1915
+ method,
1916
+ requestId,
1917
+ duration,
1918
+ errorCode: "CONFIG_NOT_INITIALIZED",
1919
+ });
1920
+
1921
+ logger.metric("login_duration_ms", duration, {
1922
+ component: "AuthenticationService",
1923
+ success: false,
1924
+ error: "CONFIG_NOT_INITIALIZED",
1925
+ });
1926
+ logger.metric("login_errors_total", 1, {
1927
+ component: "AuthenticationService",
1928
+ error: "CONFIG_NOT_INITIALIZED",
1929
+ });
1930
+
1931
+ return { error: "Error 0111: Client configuration not initialized" };
1932
+ }
1933
+
1934
+ const apiendpoint = config_own_rodit.own_rodit?.metadata?.subjectuniqueidentifier_url;
1935
+ const loginPath =
1936
+ options.loginPath ??
1937
+ config_own_rodit.login_rodit_path ??
1938
+ config.get("LOGIN_RODIT_PATH", "/api/login");
1939
+ const loginUrl = buildLoginUrl(apiendpoint, loginPath);
1940
+
1941
+ logger.info("Resolved API endpoint for login_server", {
1942
+ component: "AuthenticationService",
1943
+ method,
1944
+ requestId,
1945
+ apiEndpoint: apiendpoint,
1946
+ loginUrl,
1947
+ source: config_own_rodit.own_rodit?.metadata?.subjectuniqueidentifier_url ? "metadata" : "config",
1948
+ });
1949
+
1950
+ const roditid = normalizeOptionalLoginString(own_rodit?.token_id);
1951
+ const { timestamp, error: timestampError, errorCode: timestampErrorCode } =
1952
+ await resolveServerLoginTimestamp(apiendpoint, options);
1953
+ const accountid = normalizeOptionalServerAccountId(options.accountId);
1954
+
1955
+ if (timestamp === null) {
1956
+ return {
1957
+ error: timestampError || "Missing or invalid options.timestamp",
1958
+ errorCode: timestampErrorCode || "INVALID_LOGIN_TIMESTAMP",
1959
+ failureReason: timestampErrorCode || "INVALID_LOGIN_TIMESTAMP",
1960
+ requestId
1961
+ };
1962
+ }
1963
+
1964
+ const hasRoditId = roditid.length > 0;
1965
+ const hasAccountId = accountid.length > 0;
1966
+
1967
+ if (hasRoditId === hasAccountId) {
1968
+ return {
1969
+ error: "Provide exactly one signing identifier: own_rodit.token_id or options.accountId",
1970
+ errorCode: "LOGIN_IDENTIFIER_AMBIGUOUS",
1971
+ failureReason: "LOGIN_IDENTIFIER_AMBIGUOUS",
1972
+ requestId
1973
+ };
1974
+ }
1975
+
1976
+ logger.debug("Preparing authentication data", {
1977
+ component: "AuthenticationService",
1978
+ method,
1979
+ requestId,
1980
+ api_ep: apiendpoint,
1981
+ roditId: roditid,
1982
+ accountId: accountid,
1983
+ timestamp,
1984
+ });
1985
+
1986
+ const timeString = await unixTimeToDateString(timestamp);
1987
+
1988
+ const signatureIdentifier = hasRoditId ? roditid : accountid;
1989
+ const signatureIdentifierandtimestamp = new TextEncoder().encode(
1990
+ signatureIdentifier + timeString
1991
+ );
1992
+
1993
+ logger.debug("Generating signature", {
1994
+ component: "AuthenticationService",
1995
+ method,
1996
+ requestId,
1997
+ hasPrivateKey: !!config_own_rodit.own_rodit_bytes_private_key,
1998
+ signatureIdentifier,
1999
+ });
2000
+
2001
+ const own_rodit_bytes_signature = nacl.sign.detached(
2002
+ signatureIdentifierandtimestamp,
2003
+ config_own_rodit.own_rodit_bytes_private_key
2004
+ );
2005
+
2006
+ const roditid_base64url_signature = Buffer.from(
2007
+ own_rodit_bytes_signature
2008
+ ).toString("base64url");
2009
+
2010
+ const requestBody = {
2011
+ timestamp,
2012
+ roditid_base64url_signature,
2013
+ };
2014
+
2015
+ if (hasRoditId) {
2016
+ requestBody.roditid = roditid;
2017
+ }
2018
+
2019
+ if (hasAccountId) {
2020
+ requestBody.accountid = accountid;
2021
+ }
2022
+
2023
+ logger.debug("Sending login request", {
2024
+ component: "AuthenticationService",
2025
+ method,
2026
+ requestId,
2027
+ roditid,
2028
+ accountId: accountid,
2029
+ timestamp,
2030
+ signatureLength: roditid_base64url_signature?.length,
2031
+ apiEndpoint: loginUrl,
2032
+ });
2033
+
2034
+ const response = await fetch(loginUrl, {
2035
+ method: "POST",
2036
+ headers: {
2037
+ "Content-Type": "application/json",
2038
+ "User-Agent": "RODiT-SDK",
2039
+ },
2040
+ body: JSON.stringify(requestBody),
2041
+ });
2042
+
2043
+ if (!response.ok) {
2044
+ const duration = Date.now() - startTime;
2045
+
2046
+ let errorDetails = null;
2047
+ let responseText = '';
2048
+ try {
2049
+ const text = await response.text();
2050
+ responseText = text;
2051
+ errorDetails = JSON.parse(text);
2052
+ } catch (parseError) {
2053
+ // If JSON parsing fails, continue with basic error
2054
+ logger.debug("Failed to parse error response as JSON", {
2055
+ component: "AuthenticationService",
2056
+ method: "login_server",
2057
+ requestId,
2058
+ responseText: responseText.substring(0, 500),
2059
+ parseError: parseError.message
2060
+ });
2061
+ }
2062
+
2063
+ const apiNested = errorDetails?.error && typeof errorDetails.error === "object"
2064
+ ? errorDetails.error
2065
+ : null;
2066
+ const resolvedCode =
2067
+ apiNested?.code ||
2068
+ apiNested?.details?.failureReason ||
2069
+ errorDetails?.errorCode ||
2070
+ errorDetails?.failureReason ||
2071
+ errorDetails?.code;
2072
+ const resolvedMessage =
2073
+ apiNested?.message ||
2074
+ errorDetails?.message ||
2075
+ errorDetails?.failureMessage ||
2076
+ "Login failed";
2077
+
2078
+ logger.error("Login request failed", {
2079
+ component: "AuthenticationService",
2080
+ method: "login_server",
2081
+ requestId,
2082
+ duration,
2083
+ status: response.status,
2084
+ statusText: response.statusText,
2085
+ errorCode: resolvedCode,
2086
+ errorMessage: resolvedMessage,
2087
+ failureReason: apiNested?.details?.failureReason || errorDetails?.failureReason,
2088
+ responseText: responseText.substring(0, 500),
2089
+ fullErrorDetails: errorDetails
2090
+ });
2091
+
2092
+ logger.metric("login_duration_ms", duration, {
2093
+ component: "AuthenticationService",
2094
+ success: false,
2095
+ error: resolvedCode || "HTTP_ERROR",
2096
+ status: response.status,
2097
+ });
2098
+ logger.metric("login_errors_total", 1, {
2099
+ component: "AuthenticationService",
2100
+ error: resolvedCode || "HTTP_ERROR",
2101
+ status: response.status,
2102
+ });
2103
+
2104
+ return {
2105
+ error: resolvedMessage,
2106
+ errorCode: resolvedCode || "HTTP_ERROR",
2107
+ failureReason: apiNested?.details?.failureReason || errorDetails?.failureReason,
2108
+ status: response.status,
2109
+ requestId
2110
+ };
2111
+ }
2112
+
2113
+ const data = await response.json();
2114
+ let jwt_token = data.jwt_token;
2115
+
2116
+ try {
2117
+ const { decodeJwt } = await getJose();
2118
+ const unverifiedPayload = decodeJwt(jwt_token);
2119
+ const peerRoditId = unverifiedPayload.rodit_id;
2120
+
2121
+ const peer_rodit = await nearorg_rpc_tokenfromroditid(peerRoditId);
2122
+
2123
+ const validationResult = await validate_jwt_token_be(
2124
+ jwt_token,
2125
+ peer_rodit,
2126
+ RELAXED_SESSION_VALIDATION_OPTIONS
2127
+ );
2128
+
2129
+ if (!validationResult.valid && validationResult.errorCode) {
2130
+ const duration = Date.now() - startTime;
2131
+
2132
+ logger.error("Server JWT validation failed with detailed error", {
2133
+ component: "AuthenticationService",
2134
+ method,
2135
+ requestId,
2136
+ duration,
2137
+ errorCode: validationResult.errorCode,
2138
+ errorMessage: validationResult.errorMessage,
2139
+ error: validationResult.error,
2140
+ });
2141
+
2142
+ logger.metric("login_duration_ms", duration, {
2143
+ component: "AuthenticationService",
2144
+ success: false,
2145
+ error: validationResult.errorCode,
2146
+ });
2147
+ logger.metric("login_errors_total", 1, {
2148
+ component: "AuthenticationService",
2149
+ error: validationResult.errorCode,
2150
+ });
2151
+
2152
+ return {
2153
+ error: validationResult.errorMessage || validationResult.error || "Server validation failed",
2154
+ errorCode: validationResult.errorCode,
2155
+ failureReason: validationResult.errorCode,
2156
+ validationError: validationResult.error,
2157
+ requestId
2158
+ };
2159
+ }
2160
+
2161
+ const peer_base64url_jwk_public_key = Buffer.from(peer_rodit.owner_id, "hex").toString("base64url");
2162
+ await stateManager.setPeerBase64urlJwkPublicKey(peer_base64url_jwk_public_key);
2163
+
2164
+ logger.debug("Peer public key set in state manager", {
2165
+ component: "AuthenticationService",
2166
+ method,
2167
+ requestId,
2168
+ peerRoditId: peer_rodit.token_id,
2169
+ keyLength: peer_base64url_jwk_public_key.length
2170
+ });
2171
+ } catch (validationError) {
2172
+ const duration = Date.now() - startTime;
2173
+
2174
+ logger.error("JWT validation failed", {
2175
+ component: "AuthenticationService",
2176
+ method,
2177
+ requestId,
2178
+ duration,
2179
+ errorMessage: validationError.message,
2180
+ stack: validationError.stack,
2181
+ });
2182
+
2183
+ logger.metric("login_duration_ms", duration, {
2184
+ component: "AuthenticationService",
2185
+ success: false,
2186
+ error: "JWT_VALIDATION_FAILED",
2187
+ });
2188
+ logger.metric("login_errors_total", 1, {
2189
+ component: "AuthenticationService",
2190
+ error: "JWT_VALIDATION_FAILED",
2191
+ });
2192
+
2193
+ throw new Error(
2194
+ `Error 039: Server validation failed: ${validationError.message}`
2195
+ );
2196
+ }
2197
+
2198
+ const duration = Date.now() - startTime;
2199
+ logger.info("Login successful", {
2200
+ component: "AuthenticationService",
2201
+ method,
2202
+ requestId,
2203
+ duration,
2204
+ api_ep: apiendpoint,
2205
+ });
2206
+
2207
+ logger.metric("login_duration_ms", duration, {
2208
+ component: "AuthenticationService",
2209
+ success: true,
2210
+ });
2211
+ logger.metric("successful_logins_total", 1, {
2212
+ component: "AuthenticationService",
2213
+ apiEndpoint: apiendpoint,
2214
+ });
2215
+
2216
+ return {
2217
+ jwt_token,
2218
+ apiendpoint,
2219
+ requestId,
2220
+ };
2221
+ } catch (error) {
2222
+ const duration = Date.now() - startTime;
2223
+
2224
+ logger.error("Login failed", {
2225
+ component: "AuthenticationService",
2226
+ method,
2227
+ requestId,
2228
+ duration,
2229
+ errorMessage: error.message,
2230
+ stack: error.stack,
2231
+ });
2232
+
2233
+ logger.metric("login_duration_ms", duration, {
2234
+ component: "AuthenticationService",
2235
+ success: false,
2236
+ error: error.constructor.name,
2237
+ });
2238
+ logger.metric("login_errors_total", 1, {
2239
+ component: "AuthenticationService",
2240
+ error: error.constructor.name,
2241
+ });
2242
+
2243
+ return {
2244
+ error: "Failed to login to server",
2245
+ requestId,
2246
+ };
2247
+ }
2248
+ }
2249
+
2250
+ /**
2251
+ * Handle server logout - invalidates JWT token and closes session
2252
+ *
2253
+ * @param {string} jwt_token - JWT token to invalidate
2254
+ * @returns {Promise<Object>} Logout result with termination token
2255
+ */
2256
+ async function logout_server(jwt_token) {
2257
+ const requestId = ulid();
2258
+ const startTime = Date.now();
2259
+
2260
+ // 1. Validate JWT token parameter
2261
+ if (!jwt_token) {
2262
+ return { success: false, error: "No JWT token provided", requestId };
2263
+ }
2264
+
2265
+ // 2. Get API endpoint (same as login_server / account-based server login)
2266
+ const config_own_rodit = stateManager.getConfigOwnRodit();
2267
+ const apiendpoint = config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url;
2268
+
2269
+ // 3. Make fetch call to external server
2270
+ const response = await fetch(apiendpoint + "/api/sessions/logout", {
2271
+ method: "POST",
2272
+ headers: {
2273
+ "Content-Type": "application/json",
2274
+ "Authorization": `Bearer ${jwt_token}`,
2275
+ "User-Agent": "RODiT-SDK",
2276
+ },
2277
+ body: JSON.stringify({
2278
+ reason: "User initiated logout"
2279
+ }),
2280
+ });
2281
+
2282
+ // 4. Handle response
2283
+ if (!response.ok) {
2284
+ return {
2285
+ success: false,
2286
+ error: `Logout request failed: ${response.status} ${response.statusText}`,
2287
+ requestId
2288
+ };
2289
+ }
2290
+
2291
+ // 5. Return server response
2292
+ const logoutData = await response.json();
2293
+ return {
2294
+ ...logoutData,
2295
+ requestId
2296
+ };
2297
+ }
2298
+
2299
+
2300
+ // Export the class directly (will be instantiated in rodit.js)
2301
+ module.exports = {authenticate_apicall,authenticate_logout,login_server,login_portal,login_client,login_client_withnep413,logout_client,logout_server};