@rodit/rodit-auth-be 9.11.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +3543 -0
- package/index.js +1884 -0
- package/lib/auth/authentication.js +1971 -0
- package/lib/auth/roditmanager.js +627 -0
- package/lib/auth/sessionmanager.js +1302 -0
- package/lib/auth/tokenservice.js +2418 -0
- package/lib/blockchain/blockchainservice.js +1715 -0
- package/lib/blockchain/statemanager.js +1614 -0
- package/lib/middleware/authenticationmw.js +2301 -0
- package/lib/middleware/environcredentialstoremw.js +176 -0
- package/lib/middleware/filecredentialstoremw.js +158 -0
- package/lib/middleware/loggingmw.js +82 -0
- package/lib/middleware/performanceexamplemw.js +58 -0
- package/lib/middleware/performancemw.js +172 -0
- package/lib/middleware/ratelimitmw.js +171 -0
- package/lib/middleware/validatepermissionsmw.js +439 -0
- package/lib/middleware/vaultcredentialstoremw.js +617 -0
- package/lib/middleware/versioningmw.js +142 -0
- package/lib/middleware/webhookhandlermw.js +1388 -0
- package/package.json +57 -0
- package/services/configsdk.js +588 -0
- package/services/env.js +34 -0
- package/services/error-response.js +29 -0
- package/services/logger.js +160 -0
- package/services/performanceservice.js +568 -0
- package/services/utils.js +1024 -0
- package/services/versionmanager.js +81 -0
|
@@ -0,0 +1,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};
|