@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,1971 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication service for RODiT authentication
|
|
3
|
+
* Copyright (c) 2026 Discernible IO. All rights reserved.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { ulid } = require("ulid");
|
|
7
|
+
const logger = require("../../services/logger");
|
|
8
|
+
const { createLogContext, logErrorWithMetrics } = logger;
|
|
9
|
+
const nacl = require("tweetnacl");
|
|
10
|
+
nacl.util = require("tweetnacl-util");
|
|
11
|
+
const crypto = require("crypto");
|
|
12
|
+
const { Resolver } = require("dns").promises;
|
|
13
|
+
const { calculateCanonicalHash, unixTimeToDateString } = require("../../services/utils");
|
|
14
|
+
const stateManager = require("../blockchain/statemanager");
|
|
15
|
+
const borsh = require("borsh");
|
|
16
|
+
const {
|
|
17
|
+
nearorg_rpc_timestamp,
|
|
18
|
+
nearorg_rpc_tokenfromroditid,
|
|
19
|
+
nearorg_rpc_tokensfromaccountid,
|
|
20
|
+
nearorg_rpc_fetchpublickeybytes,
|
|
21
|
+
RODiT,
|
|
22
|
+
PayloadNEP413,
|
|
23
|
+
PayloadNEP413Schema,
|
|
24
|
+
CONSTANTS,
|
|
25
|
+
} = require("../blockchain/blockchainservice");
|
|
26
|
+
|
|
27
|
+
async function verify_rodit_ownership(
|
|
28
|
+
peerroditid,
|
|
29
|
+
peertimestamp,
|
|
30
|
+
peerroditid_base64url_signature,
|
|
31
|
+
peer_rodit
|
|
32
|
+
) {
|
|
33
|
+
const requestId = ulid();
|
|
34
|
+
const startTime = Date.now();
|
|
35
|
+
|
|
36
|
+
// Create a base context that will be used throughout this function
|
|
37
|
+
const baseContext = createLogContext(
|
|
38
|
+
"AuthServices",
|
|
39
|
+
"verify_rodit_ownership",
|
|
40
|
+
{
|
|
41
|
+
requestId,
|
|
42
|
+
peerRoditId: peerroditid,
|
|
43
|
+
timestamp: peertimestamp,
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
logger.infoWithContext("Starting RODiT ownership verification", baseContext);
|
|
48
|
+
|
|
49
|
+
logger.debugWithContext("Verification parameters", {
|
|
50
|
+
...baseContext,
|
|
51
|
+
signatureLength: peerroditid_base64url_signature?.length,
|
|
52
|
+
signatureValue: peerroditid_base64url_signature?.substring(0, 20) + '...',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// DO NOT DELETE THE FOLLOWING COMMENT
|
|
57
|
+
/* Maybe for NEP413 compatibility, the following line added "NEAR" before peerroditid */
|
|
58
|
+
|
|
59
|
+
// Match legacy implementation exactly
|
|
60
|
+
const timeString = await unixTimeToDateString(peertimestamp);
|
|
61
|
+
const roditidandtimestamp = new TextEncoder().encode(
|
|
62
|
+
peerroditid + timeString
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
logger.debugWithContext("Encoded roditid and timestamp", {
|
|
66
|
+
...baseContext,
|
|
67
|
+
timeString,
|
|
68
|
+
combinedString: peerroditid + timeString,
|
|
69
|
+
bufferLength: roditidandtimestamp.length,
|
|
70
|
+
bufferHex: Buffer.from(roditidandtimestamp).toString('hex'),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Check if signature is defined before proceeding
|
|
74
|
+
if (!peerroditid_base64url_signature) {
|
|
75
|
+
const duration = Date.now() - startTime;
|
|
76
|
+
|
|
77
|
+
logger.debugWithContext("Missing signature in authentication request", {
|
|
78
|
+
...baseContext,
|
|
79
|
+
duration
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Emit metrics for dashboards
|
|
83
|
+
logger.metric("rodit_ownership_verification_ms", duration, {
|
|
84
|
+
component: "AuthServices",
|
|
85
|
+
success: false,
|
|
86
|
+
error: "MISSING_SIGNATURE"
|
|
87
|
+
});
|
|
88
|
+
logger.metric("failed_verification_attempts_total", 1, {
|
|
89
|
+
component: "AuthServices",
|
|
90
|
+
reason: "MISSING_SIGNATURE"
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
logErrorWithMetrics(
|
|
94
|
+
"Missing signature in authentication request",
|
|
95
|
+
baseContext,
|
|
96
|
+
new Error("Missing signature in authentication request"),
|
|
97
|
+
"auth_error",
|
|
98
|
+
{ error_type: "missing_signature" }
|
|
99
|
+
);
|
|
100
|
+
throw new Error("Missing signature in authentication request");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const bytes_ed25519_signature = new Uint8Array(
|
|
104
|
+
Buffer.from(peerroditid_base64url_signature, "base64url")
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
logger.debugWithContext("Decoded signature using base64url", {
|
|
108
|
+
...baseContext,
|
|
109
|
+
signatureLength: bytes_ed25519_signature.length,
|
|
110
|
+
signatureHex: Buffer.from(bytes_ed25519_signature).toString('hex'),
|
|
111
|
+
expectedLength: 64, // Ed25519 signatures should be 64 bytes
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const peer_bytes_ed25519_public_key =
|
|
115
|
+
await nearorg_rpc_fetchpublickeybytes(
|
|
116
|
+
peer_rodit.owner_id
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
logger.debugWithContext("Retrieved public key", {
|
|
120
|
+
...baseContext,
|
|
121
|
+
ownerId: peer_rodit.owner_id,
|
|
122
|
+
keyLength: peer_bytes_ed25519_public_key?.length || 0,
|
|
123
|
+
keyHex: peer_bytes_ed25519_public_key ? Buffer.from(peer_bytes_ed25519_public_key).toString('hex') : 'null',
|
|
124
|
+
expectedLength: 32, // Ed25519 public keys should be 32 bytes
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Add more detailed debugging for verification inputs
|
|
128
|
+
logger.debugWithContext("Verification inputs", {
|
|
129
|
+
...baseContext,
|
|
130
|
+
messageLength: roditidandtimestamp.length,
|
|
131
|
+
messageContent: peerroditid + timeString,
|
|
132
|
+
signatureLength: bytes_ed25519_signature.length,
|
|
133
|
+
publicKeyLength: peer_bytes_ed25519_public_key?.length,
|
|
134
|
+
messageHex: Buffer.from(roditidandtimestamp).toString('hex'),
|
|
135
|
+
signatureHex: Buffer.from(bytes_ed25519_signature).toString('hex'),
|
|
136
|
+
publicKeyHex: peer_bytes_ed25519_public_key ? Buffer.from(peer_bytes_ed25519_public_key).toString('hex') : 'null'
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const isaMatch = nacl.sign.detached.verify(
|
|
140
|
+
roditidandtimestamp,
|
|
141
|
+
bytes_ed25519_signature,
|
|
142
|
+
peer_bytes_ed25519_public_key
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const duration = Date.now() - startTime;
|
|
146
|
+
|
|
147
|
+
if (isaMatch) {
|
|
148
|
+
// Use infoWithContext for successful verification
|
|
149
|
+
logger.infoWithContext("Peer RODiT ownership check successful", {
|
|
150
|
+
...baseContext,
|
|
151
|
+
duration,
|
|
152
|
+
ownerId: peer_rodit.owner_id,
|
|
153
|
+
outcome: "success"
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Emit metrics for dashboards
|
|
157
|
+
logger.metric("rodit_ownership_verification_ms", duration, {
|
|
158
|
+
component: "AuthServices",
|
|
159
|
+
success: true,
|
|
160
|
+
roditId: peerroditid
|
|
161
|
+
});
|
|
162
|
+
logger.metric("successful_verification_attempts_total", 1, {
|
|
163
|
+
component: "AuthServices",
|
|
164
|
+
roditId: peerroditid
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return true;
|
|
168
|
+
} else {
|
|
169
|
+
// Use logErrorWithMetrics for failed verification
|
|
170
|
+
logger.warnWithContext("Peer RODiT ownership check failed", {
|
|
171
|
+
...baseContext,
|
|
172
|
+
duration,
|
|
173
|
+
ownerId: peer_rodit.owner_id,
|
|
174
|
+
outcome: "failed"
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Emit metrics for dashboards
|
|
178
|
+
logger.metric("rodit_ownership_verification_ms", duration, {
|
|
179
|
+
component: "AuthServices",
|
|
180
|
+
success: false,
|
|
181
|
+
error: "SIGNATURE_VERIFICATION_FAILED",
|
|
182
|
+
roditId: peerroditid
|
|
183
|
+
});
|
|
184
|
+
logger.metric("failed_verification_attempts_total", 1, {
|
|
185
|
+
component: "AuthServices",
|
|
186
|
+
reason: "SIGNATURE_VERIFICATION_FAILED",
|
|
187
|
+
roditId: peerroditid
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
logErrorWithMetrics(
|
|
191
|
+
"Peer RODiT ownership check failed",
|
|
192
|
+
{
|
|
193
|
+
...baseContext,
|
|
194
|
+
duration,
|
|
195
|
+
ownerId: peer_rodit.owner_id,
|
|
196
|
+
outcome: "failed"
|
|
197
|
+
},
|
|
198
|
+
new Error("Error 035: PeerEd25519SignatureVerificationFailure"),
|
|
199
|
+
"rodit_ownership_verification",
|
|
200
|
+
{
|
|
201
|
+
result: "failure",
|
|
202
|
+
peer_rodit_id: peerroditid,
|
|
203
|
+
duration
|
|
204
|
+
}
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
throw new Error("Error 035: PeerEd25519SignatureVerificationFailure");
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
const duration = Date.now() - startTime;
|
|
211
|
+
|
|
212
|
+
// Emit metrics for dashboards
|
|
213
|
+
logger.metric("rodit_ownership_verification_ms", duration, {
|
|
214
|
+
component: "AuthServices",
|
|
215
|
+
success: false,
|
|
216
|
+
error: error.name || "Unknown",
|
|
217
|
+
roditId: peerroditid
|
|
218
|
+
});
|
|
219
|
+
logger.metric("failed_verification_attempts_total", 1, {
|
|
220
|
+
component: "AuthServices",
|
|
221
|
+
reason: error.name || "Unknown",
|
|
222
|
+
roditId: peerroditid
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
logErrorWithMetrics(
|
|
226
|
+
"RODiT ownership verification failed",
|
|
227
|
+
{
|
|
228
|
+
...baseContext,
|
|
229
|
+
duration,
|
|
230
|
+
peerRoditId: peerroditid
|
|
231
|
+
},
|
|
232
|
+
error,
|
|
233
|
+
"rodit_ownership_verification",
|
|
234
|
+
{
|
|
235
|
+
result: "error",
|
|
236
|
+
error_type: error.name || "Unknown",
|
|
237
|
+
peer_rodit_id: peerroditid,
|
|
238
|
+
duration
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function verify_rodit_ownership_withnep413(
|
|
247
|
+
message,
|
|
248
|
+
nonce,
|
|
249
|
+
recipient,
|
|
250
|
+
callbackUrl,
|
|
251
|
+
signature,
|
|
252
|
+
peer_rodit
|
|
253
|
+
) {
|
|
254
|
+
const requestId = ulid();
|
|
255
|
+
const startTime = Date.now();
|
|
256
|
+
|
|
257
|
+
// Create a base context that will be used throughout this function
|
|
258
|
+
const baseContext = createLogContext(
|
|
259
|
+
"AuthServices",
|
|
260
|
+
"verify_rodit_ownership_withnep413",
|
|
261
|
+
{
|
|
262
|
+
requestId,
|
|
263
|
+
messageLength: message?.length,
|
|
264
|
+
recipientId: recipient
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
logger.debugWithContext("Starting NEP-413 signature verification", baseContext);
|
|
270
|
+
|
|
271
|
+
// Ensure nonce is correctly formatted
|
|
272
|
+
let nonceArray;
|
|
273
|
+
if (typeof nonce === "string") {
|
|
274
|
+
// Handle base64url encoded nonce
|
|
275
|
+
nonceArray = new Uint8Array(Buffer.from(nonce, "base64url"));
|
|
276
|
+
} else if (Array.isArray(nonce)) {
|
|
277
|
+
nonceArray = new Uint8Array(nonce);
|
|
278
|
+
} else if (typeof nonce === "object" && nonce !== null) {
|
|
279
|
+
nonceArray = new Uint8Array(Object.values(nonce));
|
|
280
|
+
} else {
|
|
281
|
+
throw new Error(`Invalid nonce format: ${typeof nonce}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (nonceArray.length !== 32) {
|
|
285
|
+
const error = new Error(`Invalid nonce length: ${nonceArray.length}, expected 32`);
|
|
286
|
+
logErrorWithMetrics(
|
|
287
|
+
"Invalid nonce length in NEP-413 verification",
|
|
288
|
+
{ ...baseContext, nonceLength: nonceArray.length },
|
|
289
|
+
error,
|
|
290
|
+
"nep413_verification_error",
|
|
291
|
+
{ error_type: "invalid_nonce_length" }
|
|
292
|
+
);
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const payload = new PayloadNEP413({
|
|
297
|
+
tag: 2147484061,
|
|
298
|
+
message,
|
|
299
|
+
nonce: nonceArray,
|
|
300
|
+
recipient,
|
|
301
|
+
callbackUrl,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const serializedPayload = borsh.serialize(PayloadNEP413Schema, payload);
|
|
305
|
+
const payloadHash = crypto
|
|
306
|
+
.createHash("sha256")
|
|
307
|
+
.update(serializedPayload)
|
|
308
|
+
.digest();
|
|
309
|
+
|
|
310
|
+
// Convert base64url signature to standard base64
|
|
311
|
+
const standardBase64 = signature
|
|
312
|
+
.replace(/-/g, "+")
|
|
313
|
+
.replace(/_/g, "/")
|
|
314
|
+
.padEnd(signature.length + ((4 - (signature.length % 4)) % 4), "=");
|
|
315
|
+
const signatureBytes = nacl.util.decodeBase64(standardBase64);
|
|
316
|
+
|
|
317
|
+
// Get public key bytes
|
|
318
|
+
const publicKeyBytes = await nearorg_rpc_fetchpublickeybytes(
|
|
319
|
+
peer_rodit.owner_id
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Perform verification
|
|
323
|
+
const isaMatch = nacl.sign.detached.verify(
|
|
324
|
+
payloadHash,
|
|
325
|
+
signatureBytes,
|
|
326
|
+
publicKeyBytes
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const duration = Date.now() - startTime;
|
|
330
|
+
|
|
331
|
+
if (isaMatch) {
|
|
332
|
+
logger.infoWithContext("Peer RODiT possession check successful", {
|
|
333
|
+
...baseContext,
|
|
334
|
+
duration,
|
|
335
|
+
outcome: "success"
|
|
336
|
+
});
|
|
337
|
+
return true;
|
|
338
|
+
} else {
|
|
339
|
+
const error = new Error("PeerEd25519SignatureVerificationFailure");
|
|
340
|
+
logErrorWithMetrics(
|
|
341
|
+
"Peer RODiT possession check failed",
|
|
342
|
+
{
|
|
343
|
+
...baseContext,
|
|
344
|
+
duration,
|
|
345
|
+
outcome: "failed"
|
|
346
|
+
},
|
|
347
|
+
error,
|
|
348
|
+
"rodit_ownership_verification",
|
|
349
|
+
{ error_type: "signature_verification_failure" }
|
|
350
|
+
);
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
} catch (error) {
|
|
354
|
+
const duration = Date.now() - startTime;
|
|
355
|
+
logErrorWithMetrics(
|
|
356
|
+
"RODiT ownership verification failed",
|
|
357
|
+
{
|
|
358
|
+
...baseContext,
|
|
359
|
+
duration,
|
|
360
|
+
error: error.message
|
|
361
|
+
},
|
|
362
|
+
error,
|
|
363
|
+
"rodit_ownership_verification",
|
|
364
|
+
{ error_type: "verification_error" }
|
|
365
|
+
);
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Authenticate a webhook request
|
|
372
|
+
*
|
|
373
|
+
* @param {string} payload - Webhook payload
|
|
374
|
+
* @param {string} signature_hex_ofpayload - Signature of payload
|
|
375
|
+
* @param {number} timestamp - Request timestamp
|
|
376
|
+
* @param {string} server_public_key_base64url - Server's public key from RODiT in base64url format
|
|
377
|
+
* @returns {Promise<Object>} Authentication result
|
|
378
|
+
*/
|
|
379
|
+
async function authenticate_webhook(
|
|
380
|
+
payload,
|
|
381
|
+
signature_hex_ofpayload,
|
|
382
|
+
timestamp,
|
|
383
|
+
server_public_key_base64url
|
|
384
|
+
) {
|
|
385
|
+
const requestId = ulid();
|
|
386
|
+
const startTime = Date.now();
|
|
387
|
+
|
|
388
|
+
// Create a base context that will be used throughout this function
|
|
389
|
+
const baseContext = createLogContext(
|
|
390
|
+
"AuthServices",
|
|
391
|
+
"authenticate_webhook",
|
|
392
|
+
{
|
|
393
|
+
requestId,
|
|
394
|
+
timestamp
|
|
395
|
+
}
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
logger.debugWithContext("Starting webhook authentication", {
|
|
399
|
+
...baseContext,
|
|
400
|
+
hasPayload: !!payload,
|
|
401
|
+
hasSignature: !!signature_hex_ofpayload,
|
|
402
|
+
hasTimestamp: !!timestamp,
|
|
403
|
+
hasServerPublicKey: !!server_public_key_base64url,
|
|
404
|
+
serverKeyLength: server_public_key_base64url?.length,
|
|
405
|
+
payloadLength: payload?.length || 0,
|
|
406
|
+
signatureLength: signature_hex_ofpayload?.length || 0,
|
|
407
|
+
timestampValue: timestamp,
|
|
408
|
+
signatureFirstChars: signature_hex_ofpayload ? signature_hex_ofpayload.substring(0, 15) + '...' : 'null',
|
|
409
|
+
serverKeyFirstChars: server_public_key_base64url ? server_public_key_base64url.substring(0, 15) + '...' : 'null'
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Only log detailed debugging info at debug level
|
|
413
|
+
logger.debugWithContext("Starting webhook authentication process", baseContext);
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
const currentTime = Date.now();
|
|
417
|
+
const parsedTimestamp = parseInt(timestamp);
|
|
418
|
+
const timeThreshold = 5 * 60 * 1000; // 5 minutes
|
|
419
|
+
|
|
420
|
+
// Check if timestamp is too old
|
|
421
|
+
if (currentTime - parsedTimestamp > timeThreshold) {
|
|
422
|
+
const duration = Date.now() - startTime;
|
|
423
|
+
|
|
424
|
+
logger.warnWithContext("Webhook authentication failed - timestamp too old", {
|
|
425
|
+
...baseContext,
|
|
426
|
+
duration,
|
|
427
|
+
timestampAge: (currentTime - parsedTimestamp) / 1000,
|
|
428
|
+
threshold: timeThreshold / 1000
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
// Emit metrics for dashboards
|
|
432
|
+
logger.metric("webhook_authentication_duration_ms", duration, {
|
|
433
|
+
component: "AuthServices",
|
|
434
|
+
success: false,
|
|
435
|
+
reason: "TIMESTAMP_EXPIRED",
|
|
436
|
+
});
|
|
437
|
+
logger.metric("webhook_authentication_failures_total", 1, {
|
|
438
|
+
component: "AuthServices",
|
|
439
|
+
reason: "TIMESTAMP_EXPIRED",
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
isValid: false,
|
|
444
|
+
error: {
|
|
445
|
+
code: "TIMESTAMP_EXPIRED",
|
|
446
|
+
message: "Webhook timestamp is too old",
|
|
447
|
+
requestId,
|
|
448
|
+
},
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
logger.debugWithContext("Calculating payload hash for verification", {
|
|
453
|
+
...baseContext,
|
|
454
|
+
payloadSize: payload.length
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// IMPORTANT: The server normalizes the payload before signing
|
|
458
|
+
// We must use the raw payload as received without additional normalization
|
|
459
|
+
|
|
460
|
+
// Log the raw payload for complete visibility with detailed format information
|
|
461
|
+
logger.debugWithContext("Raw payload for verification", {
|
|
462
|
+
...baseContext,
|
|
463
|
+
payload: payload, // Log the full payload
|
|
464
|
+
payloadSize: payload.length,
|
|
465
|
+
payloadType: typeof payload,
|
|
466
|
+
payloadIsString: typeof payload === 'string',
|
|
467
|
+
payloadFirstChars: payload.substring(0, 100) + (payload.length > 100 ? '...' : '')
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Create the string to hash: payload + timestamp (same as in send_webhook)
|
|
471
|
+
// Use the raw payload without normalization
|
|
472
|
+
const payloadWithTimestamp = payload + timestamp.toString();
|
|
473
|
+
|
|
474
|
+
logger.debugWithContext("Creating payload+timestamp string for verification", {
|
|
475
|
+
...baseContext,
|
|
476
|
+
payloadSize: payload.length,
|
|
477
|
+
timestampLength: timestamp.toString().length,
|
|
478
|
+
combinedLength: payloadWithTimestamp.length,
|
|
479
|
+
wasNormalized: false,
|
|
480
|
+
// Check if timestamp is properly appended
|
|
481
|
+
endsWithTimestamp: payloadWithTimestamp.endsWith(timestamp.toString())
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Calculate hash of payload+timestamp
|
|
485
|
+
const sha256_ofpayload = crypto
|
|
486
|
+
.createHash("sha256")
|
|
487
|
+
.update(payloadWithTimestamp)
|
|
488
|
+
.digest();
|
|
489
|
+
|
|
490
|
+
// Log the hash in hex format for debugging
|
|
491
|
+
const sha256_hex = Buffer.from(sha256_ofpayload).toString('hex');
|
|
492
|
+
logger.debugWithContext("Calculated hash for verification", {
|
|
493
|
+
...baseContext,
|
|
494
|
+
sha256_hex: sha256_hex,
|
|
495
|
+
hashLength: sha256_ofpayload.length
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
logger.debugWithContext("Converting signature to buffer", {
|
|
501
|
+
...baseContext,
|
|
502
|
+
signatureHex: signature_hex_ofpayload,
|
|
503
|
+
signatureHexLength: signature_hex_ofpayload.length,
|
|
504
|
+
// Check if signature is valid hex (should be even length and only hex chars)
|
|
505
|
+
isValidHex: /^[0-9a-fA-F]+$/.test(signature_hex_ofpayload) && signature_hex_ofpayload.length % 2 === 0
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Convert the hex signature to a Uint8Array for verification
|
|
509
|
+
// This matches how signatures are created in send_webhook
|
|
510
|
+
const buffer_signature_ofpayload = new Uint8Array(
|
|
511
|
+
Buffer.from(signature_hex_ofpayload, "hex")
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
logger.debugWithContext("Signature converted to buffer", {
|
|
515
|
+
...baseContext,
|
|
516
|
+
bufferLength: buffer_signature_ofpayload.length,
|
|
517
|
+
// Log first few bytes of the buffer for verification
|
|
518
|
+
bufferFirstBytes: Array.from(buffer_signature_ofpayload.slice(0, 4)),
|
|
519
|
+
// Log last few bytes of the buffer for verification
|
|
520
|
+
bufferLastBytes: Array.from(buffer_signature_ofpayload.slice(-4))
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// Log the server public key before conversion for debugging
|
|
524
|
+
logger.debugWithContext("Server public key before conversion", {
|
|
525
|
+
...baseContext,
|
|
526
|
+
serverKeyBase64Url: server_public_key_base64url,
|
|
527
|
+
serverKeyBase64UrlLength: server_public_key_base64url.length,
|
|
528
|
+
// Check if key is valid base64url (no +, /, or =)
|
|
529
|
+
isValidBase64Url: /^[A-Za-z0-9_-]*$/.test(server_public_key_base64url)
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Convert base64url encoded key to bytes for use with nacl
|
|
533
|
+
const server_public_key = new Uint8Array(
|
|
534
|
+
Buffer.from(server_public_key_base64url, "base64url")
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
logger.debugWithContext("Using server public key for verification", {
|
|
538
|
+
...baseContext,
|
|
539
|
+
serverKeyLength: server_public_key.length,
|
|
540
|
+
// Log the key in different formats for comparison with server logs
|
|
541
|
+
serverKeyHex: Buffer.from(server_public_key).toString('hex'),
|
|
542
|
+
serverKeyBase64: Buffer.from(server_public_key).toString('base64'),
|
|
543
|
+
serverKeyBase64Url: Buffer.from(server_public_key).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
|
544
|
+
serverKeyHexShort: Buffer.from(server_public_key).toString('hex').substring(0, 16) + '...',
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Log detailed information about the verification inputs
|
|
548
|
+
logger.debugWithContext("Signature verification details", {
|
|
549
|
+
...baseContext,
|
|
550
|
+
payloadHashHex: Buffer.from(sha256_ofpayload).toString('hex'),
|
|
551
|
+
signatureHex: signature_hex_ofpayload,
|
|
552
|
+
signatureLength: buffer_signature_ofpayload.length,
|
|
553
|
+
serverKeyHex: Buffer.from(server_public_key).toString('hex'),
|
|
554
|
+
serverKeyBase64: Buffer.from(server_public_key).toString('base64'),
|
|
555
|
+
serverKeyBase64url: server_public_key_base64url
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// Verify signature using the server's public key
|
|
559
|
+
const verificationStartTime = Date.now();
|
|
560
|
+
|
|
561
|
+
// Log all verification inputs in detail with multiple encoding formats
|
|
562
|
+
logger.debugWithContext("Detailed verification inputs", {
|
|
563
|
+
...baseContext,
|
|
564
|
+
// Hash in different formats
|
|
565
|
+
hashHex: Buffer.from(sha256_ofpayload).toString('hex'),
|
|
566
|
+
hashBase64: Buffer.from(sha256_ofpayload).toString('base64'),
|
|
567
|
+
hashBase64Url: Buffer.from(sha256_ofpayload).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
|
568
|
+
hashLength: sha256_ofpayload.length,
|
|
569
|
+
// Signature in different formats
|
|
570
|
+
signatureHex: Buffer.from(buffer_signature_ofpayload).toString('hex'),
|
|
571
|
+
signatureBase64: Buffer.from(buffer_signature_ofpayload).toString('base64'),
|
|
572
|
+
signatureBase64Url: Buffer.from(buffer_signature_ofpayload).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
|
573
|
+
signatureLength: buffer_signature_ofpayload.length,
|
|
574
|
+
// Public key in different formats
|
|
575
|
+
publicKeyHex: Buffer.from(server_public_key).toString('hex'),
|
|
576
|
+
publicKeyBase64: Buffer.from(server_public_key).toString('base64'),
|
|
577
|
+
publicKeyBase64Url: Buffer.from(server_public_key).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''),
|
|
578
|
+
publicKeyLength: server_public_key.length
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
// Perform standard signature verification
|
|
582
|
+
let isValid = false;
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
// Use the standard verification method only
|
|
586
|
+
isValid = nacl.sign.detached.verify(
|
|
587
|
+
sha256_ofpayload,
|
|
588
|
+
buffer_signature_ofpayload,
|
|
589
|
+
server_public_key
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
logger.debug("Standard signature verification completed", {
|
|
593
|
+
component: "AuthServices",
|
|
594
|
+
method: "authenticate_webhook",
|
|
595
|
+
requestId,
|
|
596
|
+
isValid
|
|
597
|
+
});
|
|
598
|
+
} catch (error) {
|
|
599
|
+
logger.warn("Signature verification failed with error", {
|
|
600
|
+
component: "AuthServices",
|
|
601
|
+
method: "authenticate_webhook",
|
|
602
|
+
requestId,
|
|
603
|
+
error: error.message
|
|
604
|
+
});
|
|
605
|
+
isValid = false;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const verificationDuration = Date.now() - verificationStartTime;
|
|
609
|
+
|
|
610
|
+
// Log the verification result
|
|
611
|
+
logger.info("Webhook signature verification result", {
|
|
612
|
+
component: "AuthServices",
|
|
613
|
+
method: "authenticate_webhook",
|
|
614
|
+
requestId,
|
|
615
|
+
isValid,
|
|
616
|
+
verificationDuration
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
// Log verification metrics
|
|
620
|
+
logger.metric(
|
|
621
|
+
"signature_verification_duration_ms",
|
|
622
|
+
verificationDuration,
|
|
623
|
+
{
|
|
624
|
+
component: "AuthServices",
|
|
625
|
+
success: isValid,
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
if (!isValid) {
|
|
630
|
+
const duration = Date.now() - startTime;
|
|
631
|
+
|
|
632
|
+
logger.warn("Webhook authentication failed - invalid signature", {
|
|
633
|
+
component: "AuthServices",
|
|
634
|
+
method: "authenticate_webhook",
|
|
635
|
+
requestId,
|
|
636
|
+
duration,
|
|
637
|
+
verificationDuration,
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Emit metrics for dashboards
|
|
641
|
+
logger.metric("webhook_authentication_duration_ms", duration, {
|
|
642
|
+
component: "AuthServices",
|
|
643
|
+
success: false,
|
|
644
|
+
reason: "WEBHOOK_SIGNATURE_INVALID",
|
|
645
|
+
});
|
|
646
|
+
logger.metric("webhook_authentication_failures_total", 1, {
|
|
647
|
+
component: "AuthServices",
|
|
648
|
+
reason: "WEBHOOK_SIGNATURE_INVALID",
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
return {
|
|
652
|
+
isValid: false,
|
|
653
|
+
error: {
|
|
654
|
+
code: "WEBHOOK_SIGNATURE_INVALID",
|
|
655
|
+
message: "Webhook signature verification failed: Ed25519 proof over the webhook payload did not verify.",
|
|
656
|
+
requestId,
|
|
657
|
+
},
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const duration = Date.now() - startTime;
|
|
662
|
+
logger.info("Webhook authentication successful", {
|
|
663
|
+
component: "AuthServices",
|
|
664
|
+
method: "authenticate_webhook",
|
|
665
|
+
requestId,
|
|
666
|
+
duration,
|
|
667
|
+
verificationDuration,
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// Emit metrics for dashboards
|
|
671
|
+
logger.metric("webhook_authentication_duration_ms", duration, {
|
|
672
|
+
component: "AuthServices",
|
|
673
|
+
success: true,
|
|
674
|
+
});
|
|
675
|
+
logger.metric("successful_webhook_authentications_total", 1, {
|
|
676
|
+
component: "AuthServices",
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
return {
|
|
680
|
+
isValid: true,
|
|
681
|
+
message: "Webhook authentication successful",
|
|
682
|
+
requestId,
|
|
683
|
+
duration,
|
|
684
|
+
};
|
|
685
|
+
} catch (error) {
|
|
686
|
+
const duration = Date.now() - startTime;
|
|
687
|
+
|
|
688
|
+
logger.error("Webhook authentication error", {
|
|
689
|
+
component: "AuthServices",
|
|
690
|
+
method: "authenticate_webhook",
|
|
691
|
+
requestId,
|
|
692
|
+
duration,
|
|
693
|
+
errorMessage: error.message,
|
|
694
|
+
errorCode: error.code || "UNKNOWN_ERROR",
|
|
695
|
+
stack: error.stack,
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
// Emit metrics for dashboards
|
|
699
|
+
logger.metric("webhook_authentication_duration_ms", duration, {
|
|
700
|
+
component: "AuthServices",
|
|
701
|
+
success: false,
|
|
702
|
+
error: error.code || "UNKNOWN_ERROR",
|
|
703
|
+
});
|
|
704
|
+
logger.metric("webhook_authentication_errors_total", 1, {
|
|
705
|
+
component: "AuthServices",
|
|
706
|
+
error: error.constructor.name,
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
return {
|
|
710
|
+
isValid: false,
|
|
711
|
+
error: {
|
|
712
|
+
code: "AUTHENTICATION_ERROR",
|
|
713
|
+
message: "An unexpected error occurred during webhook authentication",
|
|
714
|
+
details: error.message,
|
|
715
|
+
requestId,
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Validates that a timestamp is not in the future
|
|
723
|
+
*
|
|
724
|
+
* @param {number} timestamp - Unix timestamp in seconds
|
|
725
|
+
* @param {number} maxAgeSeconds - Maximum age in seconds (not used, kept for backward compatibility)
|
|
726
|
+
* @returns {Promise<boolean>} True if timestamp is valid, false otherwise
|
|
727
|
+
*/
|
|
728
|
+
async function validateTimestamp(timestamp, maxAgeSeconds = 300) {
|
|
729
|
+
const requestId = ulid();
|
|
730
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
731
|
+
const timeDifference = currentTime - timestamp;
|
|
732
|
+
|
|
733
|
+
// Check if timestamp is in the future (with small buffer for clock skew)
|
|
734
|
+
if (timestamp > currentTime + 30) {
|
|
735
|
+
logger.warn("Authentication rejected: timestamp is in the future", {
|
|
736
|
+
component: "RoditAuth",
|
|
737
|
+
method: "validateTimestamp",
|
|
738
|
+
requestId,
|
|
739
|
+
timestamp,
|
|
740
|
+
currentTime,
|
|
741
|
+
difference: timestamp - currentTime
|
|
742
|
+
});
|
|
743
|
+
return false;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// We've removed the timestamp age check as it's not needed with our comprehensive validation system
|
|
747
|
+
// The system already validates tokens through other means
|
|
748
|
+
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function roditTokenResolved(pr) {
|
|
753
|
+
return !!(pr && pr.token_id);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Resolve peer RODiT for login_client: exactly one of roditid or accountid must be non-empty (callers enforce).
|
|
758
|
+
* Roditid path: token by id, then 64-hex implicit fallback on that field alone. Account path: tokens by account.
|
|
759
|
+
*
|
|
760
|
+
* @param {string} roditidTrimmed - trimmed roditid or ""
|
|
761
|
+
* @param {string} accountidTrimmed - trimmed accountid or ""
|
|
762
|
+
* @returns {Promise<object|null>} Peer RODiT instance (possibly empty), or null if both identifiers non-empty
|
|
763
|
+
*/
|
|
764
|
+
async function resolve_peer_rodit_for_login(roditidTrimmed, accountidTrimmed) {
|
|
765
|
+
const r = roditidTrimmed ? String(roditidTrimmed).trim() : "";
|
|
766
|
+
const a = accountidTrimmed ? String(accountidTrimmed).trim() : "";
|
|
767
|
+
if (r && a) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
let peer_rodit = null;
|
|
772
|
+
|
|
773
|
+
if (r) {
|
|
774
|
+
peer_rodit = await nearorg_rpc_tokenfromroditid(r);
|
|
775
|
+
if (roditTokenResolved(peer_rodit)) {
|
|
776
|
+
return peer_rodit;
|
|
777
|
+
}
|
|
778
|
+
if (/^[0-9a-f]{64}$/i.test(r)) {
|
|
779
|
+
peer_rodit = await nearorg_rpc_tokensfromaccountid(r.toLowerCase());
|
|
780
|
+
}
|
|
781
|
+
return peer_rodit;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (a) {
|
|
785
|
+
peer_rodit = await nearorg_rpc_tokensfromaccountid(a.toLowerCase());
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return peer_rodit;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Verify an already-resolved peer RODiT (timestamp, signature, match, live, active, trust).
|
|
793
|
+
*
|
|
794
|
+
* @param {object|null} peer_rodit - Resolved peer token from chain (or empty)
|
|
795
|
+
* @param {string} peerroditid - Identifier string used in verify_rodit_ownership (client-signed prefix)
|
|
796
|
+
* @param {number} peertimestamp - Unix seconds
|
|
797
|
+
* @param {string} peerroditid_base64url_signature - base64url Ed25519 signature
|
|
798
|
+
*/
|
|
799
|
+
async function verify_peer_rodit(
|
|
800
|
+
peer_rodit,
|
|
801
|
+
peerroditid,
|
|
802
|
+
peertimestamp,
|
|
803
|
+
peerroditid_base64url_signature
|
|
804
|
+
) {
|
|
805
|
+
const requestId = ulid();
|
|
806
|
+
const startTime = Date.now();
|
|
807
|
+
|
|
808
|
+
const config_own_rodit = await stateManager.getConfigOwnRodit();
|
|
809
|
+
|
|
810
|
+
logger.debug("Starting peer RODiT verification", {
|
|
811
|
+
component: "RoditAuth",
|
|
812
|
+
method: "verify_peer_rodit",
|
|
813
|
+
requestId,
|
|
814
|
+
peerRoditId: peerroditid,
|
|
815
|
+
timestamp: peertimestamp,
|
|
816
|
+
signatureLength: peerroditid_base64url_signature?.length,
|
|
817
|
+
hasOwnRodit: !!config_own_rodit,
|
|
818
|
+
ownRoditId: config_own_rodit?.token_id,
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
const timestampValid = await validateTimestamp(peertimestamp);
|
|
823
|
+
|
|
824
|
+
if (!timestampValid) {
|
|
825
|
+
logger.warn("Invalid timestamp, aborting RODiT verification", {
|
|
826
|
+
component: "RoditAuth",
|
|
827
|
+
method: "verify_peer_rodit",
|
|
828
|
+
requestId,
|
|
829
|
+
roditId: peerroditid,
|
|
830
|
+
timestamp: peertimestamp,
|
|
831
|
+
currentTime: Math.floor(Date.now() / 1000),
|
|
832
|
+
});
|
|
833
|
+
return {
|
|
834
|
+
peer_rodit: null,
|
|
835
|
+
goodrodit: false,
|
|
836
|
+
failureReason: "LOGIN_CHALLENGE_TIMESTAMP_INVALID",
|
|
837
|
+
failureMessage:
|
|
838
|
+
"Login challenge timestamp invalid: the Unix `timestamp` from your POST body is too far in the future relative to server time (use the `timestamp` from the same GET /api/login/timestamp response as your login signing payload; check clock skew)."
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
logger.debug("Timestamp validation ok", {
|
|
843
|
+
component: "RoditAuth",
|
|
844
|
+
method: "verify_peer_rodit",
|
|
845
|
+
requestId,
|
|
846
|
+
timestamp: peertimestamp
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
logger.debug("Peer RODiT resolved for verification", {
|
|
850
|
+
requestId,
|
|
851
|
+
peerRoditId: peer_rodit?.token_id,
|
|
852
|
+
peerRoditOwnerId: peer_rodit?.owner_id,
|
|
853
|
+
hasPeerRoditMetadata: !!(peer_rodit && peer_rodit.metadata),
|
|
854
|
+
metadataKeys:
|
|
855
|
+
peer_rodit && peer_rodit.metadata
|
|
856
|
+
? Object.keys(peer_rodit.metadata)
|
|
857
|
+
: [],
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
if (!roditTokenResolved(peer_rodit)) {
|
|
861
|
+
logger.error("Failed to retrieve peer RODiT data", {
|
|
862
|
+
component: "AuthServices",
|
|
863
|
+
method: "verify_peer_rodit",
|
|
864
|
+
requestId,
|
|
865
|
+
duration: Date.now() - startTime,
|
|
866
|
+
peerRoditId: peerroditid,
|
|
867
|
+
});
|
|
868
|
+
return {
|
|
869
|
+
peer_rodit: null,
|
|
870
|
+
goodrodit: false,
|
|
871
|
+
failureReason: "RODIT_NOT_FOUND",
|
|
872
|
+
failureMessage: "RODiT not found on blockchain"
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (!peer_rodit.metadata) {
|
|
877
|
+
logger.error("Peer RODiT missing metadata", {
|
|
878
|
+
component: "AuthServices",
|
|
879
|
+
method: "verify_peer_rodit",
|
|
880
|
+
requestId,
|
|
881
|
+
duration: Date.now() - startTime,
|
|
882
|
+
peerRoditId: peerroditid,
|
|
883
|
+
peerRoditOwnerId: peer_rodit.owner_id,
|
|
884
|
+
});
|
|
885
|
+
return {
|
|
886
|
+
peer_rodit: null,
|
|
887
|
+
goodrodit: false,
|
|
888
|
+
failureReason: "RODIT_MISSING_METADATA",
|
|
889
|
+
failureMessage: "RODiT is missing required metadata"
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const ownershipStart = Date.now();
|
|
894
|
+
const ownershipVerified = await verify_rodit_ownership(
|
|
895
|
+
peerroditid,
|
|
896
|
+
peertimestamp,
|
|
897
|
+
peerroditid_base64url_signature,
|
|
898
|
+
peer_rodit
|
|
899
|
+
);
|
|
900
|
+
const ownershipDuration = Date.now() - ownershipStart;
|
|
901
|
+
|
|
902
|
+
logger.debug("Ownership verification completed", {
|
|
903
|
+
requestId,
|
|
904
|
+
ownershipDuration,
|
|
905
|
+
ownershipVerified,
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
if (!ownershipVerified) {
|
|
909
|
+
logger.warn("Invalid signature, aborting RODiT verification", {
|
|
910
|
+
requestId,
|
|
911
|
+
roditId: peerroditid,
|
|
912
|
+
});
|
|
913
|
+
return {
|
|
914
|
+
peer_rodit,
|
|
915
|
+
goodrodit: false,
|
|
916
|
+
failureReason: "LOGIN_BASE64URL_SIGNATURE_INVALID",
|
|
917
|
+
failureMessage:
|
|
918
|
+
"Login base64url signature invalid: Ed25519 verification failed for the base64url_signature over UTF-8 (roditid or accountid) + canonical timestamp_iso from the login challenge (GET /api/login/timestamp). Wrong key, wrong payload, or wrong encoding (must be base64url, not standard base64)."
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const matchStart = Date.now();
|
|
923
|
+
|
|
924
|
+
if (!config_own_rodit || !config_own_rodit.own_rodit.metadata) {
|
|
925
|
+
logger.error("Own RODiT configuration is incomplete", {
|
|
926
|
+
component: "AuthServices",
|
|
927
|
+
method: "verify_peer_rodit",
|
|
928
|
+
requestId,
|
|
929
|
+
duration: Date.now() - startTime,
|
|
930
|
+
hasOwnRodit: !!config_own_rodit,
|
|
931
|
+
hasMetadata: config_own_rodit && !!config_own_rodit.own_rodit.metadata
|
|
932
|
+
});
|
|
933
|
+
return {
|
|
934
|
+
peer_rodit,
|
|
935
|
+
goodrodit: false,
|
|
936
|
+
failureReason: "SERVER_CONFIG_INCOMPLETE",
|
|
937
|
+
failureMessage: "Server RODiT configuration is incomplete"
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const matchResult = await verify_rodit_isamatch(
|
|
942
|
+
config_own_rodit.own_rodit.metadata.serviceprovider_id,
|
|
943
|
+
peer_rodit
|
|
944
|
+
);
|
|
945
|
+
const matchDuration = Date.now() - matchStart;
|
|
946
|
+
|
|
947
|
+
logger.debug("Match verification completed", {
|
|
948
|
+
requestId,
|
|
949
|
+
matchDuration,
|
|
950
|
+
isMatch: matchResult.isMatch,
|
|
951
|
+
verificationType: matchResult.verificationType,
|
|
952
|
+
failureReason: matchResult.failureReason,
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
if (!matchResult.isMatch) {
|
|
956
|
+
logger.warn("RODiT match verification failed", {
|
|
957
|
+
requestId,
|
|
958
|
+
roditId: peerroditid,
|
|
959
|
+
failureReason: matchResult.failureReason,
|
|
960
|
+
failureMessage: matchResult.failureMessage,
|
|
961
|
+
});
|
|
962
|
+
return {
|
|
963
|
+
peer_rodit,
|
|
964
|
+
goodrodit: false,
|
|
965
|
+
failureReason: matchResult.failureReason,
|
|
966
|
+
failureMessage: matchResult.failureMessage
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const liveStart = Date.now();
|
|
971
|
+
const isLive = await verify_rodit_islive(
|
|
972
|
+
peer_rodit.metadata.not_after,
|
|
973
|
+
peer_rodit.metadata.not_before
|
|
974
|
+
);
|
|
975
|
+
const liveDuration = Date.now() - liveStart;
|
|
976
|
+
|
|
977
|
+
logger.debug("Live verification completed", {
|
|
978
|
+
requestId,
|
|
979
|
+
liveDuration,
|
|
980
|
+
isLive,
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
if (!isLive) {
|
|
984
|
+
logger.warn("RODiT live verification failed", {
|
|
985
|
+
requestId,
|
|
986
|
+
roditId: peerroditid,
|
|
987
|
+
});
|
|
988
|
+
return {
|
|
989
|
+
peer_rodit,
|
|
990
|
+
goodrodit: false,
|
|
991
|
+
failureReason: "RODIT_NOT_LIVE",
|
|
992
|
+
failureMessage: "RODiT is expired or not yet valid"
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const activeStart = Date.now();
|
|
997
|
+
const isActive = await verify_rodit_isactive(
|
|
998
|
+
peer_rodit.token_id,
|
|
999
|
+
config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url
|
|
1000
|
+
);
|
|
1001
|
+
const activeDuration = Date.now() - activeStart;
|
|
1002
|
+
|
|
1003
|
+
logger.debug("Active verification completed", {
|
|
1004
|
+
requestId,
|
|
1005
|
+
activeDuration,
|
|
1006
|
+
isActive,
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
if (!isActive) {
|
|
1010
|
+
logger.warn("RODiT active verification failed", {
|
|
1011
|
+
requestId,
|
|
1012
|
+
roditId: peerroditid,
|
|
1013
|
+
});
|
|
1014
|
+
return {
|
|
1015
|
+
peer_rodit,
|
|
1016
|
+
goodrodit: false,
|
|
1017
|
+
failureReason: "RODIT_REVOKED",
|
|
1018
|
+
failureMessage: "RODiT has been revoked"
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const trustedStart = Date.now();
|
|
1023
|
+
const isTrusted = await verify_rodit_istrusted_issuingsmartcontract(
|
|
1024
|
+
config_own_rodit.own_rodit.metadata.subjectuniqueidentifier_url
|
|
1025
|
+
);
|
|
1026
|
+
const trustedDuration = Date.now() - trustedStart;
|
|
1027
|
+
|
|
1028
|
+
logger.debug("Trust verification completed", {
|
|
1029
|
+
requestId,
|
|
1030
|
+
trustedDuration,
|
|
1031
|
+
isTrusted,
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
if (!isTrusted) {
|
|
1035
|
+
logger.warn("RODiT trusted verification failed", {
|
|
1036
|
+
requestId,
|
|
1037
|
+
roditId: peerroditid,
|
|
1038
|
+
});
|
|
1039
|
+
return {
|
|
1040
|
+
peer_rodit,
|
|
1041
|
+
goodrodit: false,
|
|
1042
|
+
failureReason: "SMART_CONTRACT_NOT_TRUSTED",
|
|
1043
|
+
failureMessage: "Issuing smart contract is not trusted by this server"
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const totalDuration = Date.now() - startTime;
|
|
1048
|
+
|
|
1049
|
+
logger.info("Peer RODiT verification successful", {
|
|
1050
|
+
component: "AuthServices",
|
|
1051
|
+
method: "verify_peer_rodit",
|
|
1052
|
+
requestId,
|
|
1053
|
+
duration: totalDuration,
|
|
1054
|
+
peerRoditId: peerroditid,
|
|
1055
|
+
peerOwnerId: peer_rodit.owner_id,
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
return {
|
|
1059
|
+
peer_rodit,
|
|
1060
|
+
goodrodit: true,
|
|
1061
|
+
};
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
const duration = Date.now() - startTime;
|
|
1064
|
+
|
|
1065
|
+
logger.error("Error in verify_peer_rodit", {
|
|
1066
|
+
component: "AuthServices",
|
|
1067
|
+
method: "verify_peer_rodit",
|
|
1068
|
+
requestId,
|
|
1069
|
+
duration,
|
|
1070
|
+
error: {
|
|
1071
|
+
message: error.message,
|
|
1072
|
+
stack: error.stack,
|
|
1073
|
+
name: error.name,
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
logger.metric &&
|
|
1078
|
+
logger.metric("rodit_verification_errors", 1, {
|
|
1079
|
+
error_type: error.name || "Unknown",
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
return {
|
|
1083
|
+
peer_rodit: null,
|
|
1084
|
+
goodrodit: false,
|
|
1085
|
+
failureReason: error.failureReason || error.code || error.name || "VERIFICATION_ERROR",
|
|
1086
|
+
failureMessage: error.failureMessage || error.message || "Unknown verification error",
|
|
1087
|
+
error: `Error in verify_peer_rodit: ${error.message}`
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
async function verify_rodit_islive(peer_rodit_notafter, peer_rodit_notbefore) {
|
|
1093
|
+
const requestId = ulid();
|
|
1094
|
+
const startTime = Date.now();
|
|
1095
|
+
|
|
1096
|
+
logger.debug("Checking RODiT time validity", {
|
|
1097
|
+
component: "RoditAuth",
|
|
1098
|
+
method: "verify_rodit_islive",
|
|
1099
|
+
requestId,
|
|
1100
|
+
notAfter: peer_rodit_notafter,
|
|
1101
|
+
notBefore: peer_rodit_notbefore,
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
function parseDate(datestring) {
|
|
1105
|
+
const date = new Date(datestring);
|
|
1106
|
+
return isNaN(date.getTime()) ? new Date(0) : date;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const datetimenul = new Date(0);
|
|
1110
|
+
const datetimenotafter = parseDate(peer_rodit_notafter);
|
|
1111
|
+
const datetimenotbefore = parseDate(peer_rodit_notbefore);
|
|
1112
|
+
|
|
1113
|
+
logger.debug("Parsed validity dates", {
|
|
1114
|
+
requestId,
|
|
1115
|
+
parsedNotAfter: datetimenotafter.toISOString(),
|
|
1116
|
+
parsedNotBefore: datetimenotbefore.toISOString(),
|
|
1117
|
+
isNotAfterNull: datetimenotafter.getTime() === datetimenul.getTime(),
|
|
1118
|
+
isNotBeforeNull: datetimenotbefore.getTime() === datetimenul.getTime(),
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
try {
|
|
1122
|
+
const rpcStart = Date.now();
|
|
1123
|
+
const stringtimenow = await nearorg_rpc_timestamp();
|
|
1124
|
+
const rpcDuration = Date.now() - rpcStart;
|
|
1125
|
+
|
|
1126
|
+
logger.debug("Retrieved blockchain timestamp", {
|
|
1127
|
+
requestId,
|
|
1128
|
+
rpcDuration,
|
|
1129
|
+
blockchainTimestamp: stringtimenow,
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
const timestamp = parseInt(stringtimenow, 10);
|
|
1133
|
+
|
|
1134
|
+
if (isNaN(timestamp)) {
|
|
1135
|
+
logger.error("Failed to parse blockchain timestamp", {
|
|
1136
|
+
component: "AuthServices",
|
|
1137
|
+
requestId,
|
|
1138
|
+
duration: Date.now() - startTime,
|
|
1139
|
+
blockchainTimestamp: stringtimenow,
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// Add metrics for timestamp parsing errors
|
|
1143
|
+
logger.metric &&
|
|
1144
|
+
logger.metric("rodit_islive_errors", 1, {
|
|
1145
|
+
error_type: "timestamp_parse_error",
|
|
1146
|
+
blockchain_timestamp: stringtimenow,
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const datetimetimestamp = new Date(timestamp / 1000000); // Convert nanoseconds to milliseconds
|
|
1153
|
+
|
|
1154
|
+
logger.debug("Converted blockchain time", {
|
|
1155
|
+
requestId,
|
|
1156
|
+
blockchainTime: datetimetimestamp.toISOString(),
|
|
1157
|
+
originalTimestamp: timestamp,
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
const isAfterNotBefore =
|
|
1161
|
+
datetimetimestamp >= datetimenotbefore ||
|
|
1162
|
+
datetimenotbefore.getTime() === datetimenul.getTime();
|
|
1163
|
+
|
|
1164
|
+
const isBeforeNotAfter =
|
|
1165
|
+
datetimetimestamp <= datetimenotafter ||
|
|
1166
|
+
datetimenotafter.getTime() === datetimenul.getTime();
|
|
1167
|
+
|
|
1168
|
+
const isLive = isAfterNotBefore && isBeforeNotAfter;
|
|
1169
|
+
|
|
1170
|
+
const totalDuration = Date.now() - startTime;
|
|
1171
|
+
|
|
1172
|
+
if (isLive) {
|
|
1173
|
+
logger.info("RODiT is live", {
|
|
1174
|
+
component: "AuthServices",
|
|
1175
|
+
method: "verify_rodit_islive",
|
|
1176
|
+
requestId,
|
|
1177
|
+
duration: totalDuration,
|
|
1178
|
+
rpcDuration,
|
|
1179
|
+
currentTime: datetimetimestamp.toISOString(),
|
|
1180
|
+
notBefore: datetimenotbefore.toISOString(),
|
|
1181
|
+
notAfter: datetimenotafter.toISOString(),
|
|
1182
|
+
isLive: true,
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
// Add metrics for live tokens
|
|
1186
|
+
logger.metric &&
|
|
1187
|
+
logger.metric("rodit_time_checks", totalDuration, {
|
|
1188
|
+
result: "live",
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
return true;
|
|
1192
|
+
} else {
|
|
1193
|
+
logger.warn("RODiT is not live - outside valid time period", {
|
|
1194
|
+
component: "AuthServices",
|
|
1195
|
+
method: "verify_rodit_islive",
|
|
1196
|
+
requestId,
|
|
1197
|
+
duration: totalDuration,
|
|
1198
|
+
rpcDuration,
|
|
1199
|
+
currentTime: datetimetimestamp.toISOString(),
|
|
1200
|
+
notBefore: datetimenotbefore.toISOString(),
|
|
1201
|
+
notAfter: datetimenotafter.toISOString(),
|
|
1202
|
+
isBeforeExpiry: isBeforeNotAfter,
|
|
1203
|
+
isAfterStart: isAfterNotBefore,
|
|
1204
|
+
isLive: false,
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// Add metrics for expired or not-yet-valid tokens
|
|
1208
|
+
logger.metric &&
|
|
1209
|
+
logger.metric("rodit_time_checks", totalDuration, {
|
|
1210
|
+
result: "not_live",
|
|
1211
|
+
not_before_valid: isAfterNotBefore,
|
|
1212
|
+
not_after_valid: isBeforeNotAfter,
|
|
1213
|
+
});
|
|
1214
|
+
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
const duration = Date.now() - startTime;
|
|
1219
|
+
|
|
1220
|
+
logger.error("Failed to check RODiT time validity", {
|
|
1221
|
+
component: "AuthServices",
|
|
1222
|
+
method: "verify_rodit_islive",
|
|
1223
|
+
requestId,
|
|
1224
|
+
duration,
|
|
1225
|
+
notAfter: peer_rodit_notafter,
|
|
1226
|
+
notBefore: peer_rodit_notbefore,
|
|
1227
|
+
error: {
|
|
1228
|
+
message: error.message,
|
|
1229
|
+
stack: error.stack,
|
|
1230
|
+
name: error.name,
|
|
1231
|
+
},
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// Add metrics for validation errors
|
|
1235
|
+
logger.metric &&
|
|
1236
|
+
logger.metric("rodit_islive_errors", 1, {
|
|
1237
|
+
error_type: error.name || "Unknown",
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
return false;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
async function verify_rodit_isactive(tokenId, ownsubjectuniqueidentifier_url) {
|
|
1245
|
+
const requestId = ulid();
|
|
1246
|
+
const startTime = Date.now();
|
|
1247
|
+
|
|
1248
|
+
// WHILE DEBUGGING TEMPORARY FIX DO NOT REMOVE THIS LINE EVER WITHOUT PERMISSION
|
|
1249
|
+
return true;
|
|
1250
|
+
|
|
1251
|
+
logger.debug("Checking RODiT activity status", {
|
|
1252
|
+
component: "RoditAuth",
|
|
1253
|
+
method: "verify_rodit_isactive",
|
|
1254
|
+
requestId,
|
|
1255
|
+
tokenId,
|
|
1256
|
+
subjectUrl: ownsubjectuniqueidentifier_url,
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
const domainandextensionRegex =
|
|
1260
|
+
/(?:https?:\/\/)?(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/i;
|
|
1261
|
+
|
|
1262
|
+
const match = ownsubjectuniqueidentifier_url.match(domainandextensionRegex);
|
|
1263
|
+
|
|
1264
|
+
if (match) {
|
|
1265
|
+
const domainandextension = match[1];
|
|
1266
|
+
const revokingDnsEntry = `${tokenId}.revoked.${domainandextension}`;
|
|
1267
|
+
|
|
1268
|
+
logger.debug("Checking DNS revocation entry", {
|
|
1269
|
+
requestId,
|
|
1270
|
+
domain: domainandextension,
|
|
1271
|
+
revokingDnsEntry,
|
|
1272
|
+
});
|
|
1273
|
+
|
|
1274
|
+
try {
|
|
1275
|
+
const dnsStart = Date.now();
|
|
1276
|
+
const resolver = new Resolver();
|
|
1277
|
+
await resolver.resolveTxt(revokingDnsEntry);
|
|
1278
|
+
const dnsDuration = Date.now() - dnsStart;
|
|
1279
|
+
const totalDuration = Date.now() - startTime;
|
|
1280
|
+
|
|
1281
|
+
logger.info("RODiT revocation found", {
|
|
1282
|
+
component: "AuthServices",
|
|
1283
|
+
method: "verify_rodit_isactive",
|
|
1284
|
+
requestId,
|
|
1285
|
+
duration: totalDuration,
|
|
1286
|
+
dnsDuration,
|
|
1287
|
+
tokenId,
|
|
1288
|
+
domain: domainandextension,
|
|
1289
|
+
revokingDnsEntry,
|
|
1290
|
+
isActive: false,
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
// Add metrics for revoked tokens
|
|
1294
|
+
logger.metric &&
|
|
1295
|
+
logger.metric("rodit_revocation_checks", totalDuration, {
|
|
1296
|
+
result: "revoked",
|
|
1297
|
+
token_id: tokenId,
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
return false;
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
// DNS error usually means no revocation entry found, which is good
|
|
1303
|
+
const dnsDuration = Date.now() - dnsStart || 0;
|
|
1304
|
+
const totalDuration = Date.now() - startTime;
|
|
1305
|
+
|
|
1306
|
+
logger.debug("No revocation found for RODiT", {
|
|
1307
|
+
requestId,
|
|
1308
|
+
dnsDuration,
|
|
1309
|
+
tokenId,
|
|
1310
|
+
error: error.code,
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
logger.info("RODiT is active", {
|
|
1314
|
+
component: "AuthServices",
|
|
1315
|
+
method: "verify_rodit_isactive",
|
|
1316
|
+
requestId,
|
|
1317
|
+
duration: totalDuration,
|
|
1318
|
+
dnsDuration,
|
|
1319
|
+
tokenId,
|
|
1320
|
+
domain: domainandextension,
|
|
1321
|
+
isActive: true,
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// Add metrics for active tokens
|
|
1325
|
+
logger.metric &&
|
|
1326
|
+
logger.metric("rodit_revocation_checks", totalDuration, {
|
|
1327
|
+
result: "active",
|
|
1328
|
+
token_id: tokenId,
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
return true;
|
|
1332
|
+
}
|
|
1333
|
+
} else {
|
|
1334
|
+
const duration = Date.now() - startTime;
|
|
1335
|
+
|
|
1336
|
+
logger.warn("Unable to parse domain from URL", {
|
|
1337
|
+
component: "AuthServices",
|
|
1338
|
+
method: "verify_rodit_isactive",
|
|
1339
|
+
requestId,
|
|
1340
|
+
duration,
|
|
1341
|
+
tokenId,
|
|
1342
|
+
subjectUrl: ownsubjectuniqueidentifier_url,
|
|
1343
|
+
});
|
|
1344
|
+
|
|
1345
|
+
// Add metrics for parsing errors
|
|
1346
|
+
logger.metric &&
|
|
1347
|
+
logger.metric("rodit_revocation_checks", duration, {
|
|
1348
|
+
result: "parse_error",
|
|
1349
|
+
token_id: tokenId,
|
|
1350
|
+
});
|
|
1351
|
+
|
|
1352
|
+
// Default to allowing the token if domain parsing fails
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
async function verify_rodit_istrusted_issuingsmartcontract(
|
|
1358
|
+
ownsubjectuniqueidentifier_url
|
|
1359
|
+
) {
|
|
1360
|
+
const requestId = ulid();
|
|
1361
|
+
const startTime = Date.now();
|
|
1362
|
+
|
|
1363
|
+
logger.debug("Verifying smart contract trust", {
|
|
1364
|
+
component: "RoditAuth",
|
|
1365
|
+
method: "verify_rodit_istrusted_issuingsmartcontract",
|
|
1366
|
+
requestId,
|
|
1367
|
+
url: ownsubjectuniqueidentifier_url,
|
|
1368
|
+
smartContract: CONSTANTS.NEAR_CONTRACT_ID,
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
try {
|
|
1372
|
+
const smartcontract = CONSTANTS.NEAR_CONTRACT_ID;
|
|
1373
|
+
// Remove .near suffix
|
|
1374
|
+
const smartontractnonear = smartcontract.replace(/\.near$/, "");
|
|
1375
|
+
|
|
1376
|
+
logger.debug("Prepared smart contract identifiers", {
|
|
1377
|
+
requestId,
|
|
1378
|
+
originalContract: smartcontract,
|
|
1379
|
+
nonearContract: smartontractnonear,
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
const domainRegex =
|
|
1383
|
+
/(?:https?:\/\/)?(?:www\.)?([a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)/i;
|
|
1384
|
+
|
|
1385
|
+
const maindomainmatch = domainRegex.exec(ownsubjectuniqueidentifier_url);
|
|
1386
|
+
|
|
1387
|
+
if (!maindomainmatch) {
|
|
1388
|
+
logger.error("Failed to parse domain from URL", {
|
|
1389
|
+
component: "AuthServices",
|
|
1390
|
+
requestId,
|
|
1391
|
+
duration: Date.now() - startTime,
|
|
1392
|
+
url: ownsubjectuniqueidentifier_url,
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
// Add metrics for domain parsing failures
|
|
1396
|
+
logger.metric &&
|
|
1397
|
+
logger.metric("rodit_trust_errors", 1, {
|
|
1398
|
+
error_type: "domain_parse_error",
|
|
1399
|
+
url: ownsubjectuniqueidentifier_url,
|
|
1400
|
+
});
|
|
1401
|
+
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
`Domain can't be parsed from URL: ${ownsubjectuniqueidentifier_url}`
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const extractedDomain = maindomainmatch[1];
|
|
1408
|
+
const enablingdnsentry = `${smartontractnonear}.smartcontract.${extractedDomain}`;
|
|
1409
|
+
|
|
1410
|
+
logger.debug("Checking DNS trust entry", {
|
|
1411
|
+
requestId,
|
|
1412
|
+
extractedDomain,
|
|
1413
|
+
enablingDnsEntry: enablingdnsentry,
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
try {
|
|
1417
|
+
const dnsStart = Date.now();
|
|
1418
|
+
const resolver = new Resolver();
|
|
1419
|
+
const cfgresponse = await resolver.resolveTxt(enablingdnsentry);
|
|
1420
|
+
const dnsDuration = Date.now() - dnsStart;
|
|
1421
|
+
|
|
1422
|
+
logger.debug("DNS response received", {
|
|
1423
|
+
requestId,
|
|
1424
|
+
dnsDuration,
|
|
1425
|
+
recordCount: cfgresponse?.length || 0,
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
if (cfgresponse.length > 0) {
|
|
1429
|
+
const totalDuration = Date.now() - startTime;
|
|
1430
|
+
|
|
1431
|
+
logger.info("Smart contract is trusted", {
|
|
1432
|
+
component: "AuthServices",
|
|
1433
|
+
method: "verify_rodit_istrusted_issuingsmartcontract",
|
|
1434
|
+
requestId,
|
|
1435
|
+
duration: totalDuration,
|
|
1436
|
+
dnsDuration,
|
|
1437
|
+
smartContract: smartontractnonear,
|
|
1438
|
+
domain: extractedDomain,
|
|
1439
|
+
dnsEntry: enablingdnsentry,
|
|
1440
|
+
recordCount: cfgresponse.length,
|
|
1441
|
+
isTrusted: true,
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
// Add metrics for trusted contracts
|
|
1445
|
+
logger.metric &&
|
|
1446
|
+
logger.metric("rodit_trust_checks", totalDuration, {
|
|
1447
|
+
result: "trusted",
|
|
1448
|
+
domain: extractedDomain,
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
return true;
|
|
1452
|
+
} else {
|
|
1453
|
+
const totalDuration = Date.now() - startTime;
|
|
1454
|
+
|
|
1455
|
+
logger.warn("Smart contract not trusted - empty DNS record", {
|
|
1456
|
+
component: "AuthServices",
|
|
1457
|
+
method: "verify_rodit_istrusted_issuingsmartcontract",
|
|
1458
|
+
requestId,
|
|
1459
|
+
duration: totalDuration,
|
|
1460
|
+
dnsDuration,
|
|
1461
|
+
smartContract: smartontractnonear,
|
|
1462
|
+
domain: extractedDomain,
|
|
1463
|
+
dnsEntry: enablingdnsentry,
|
|
1464
|
+
isTrusted: false,
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
// Add metrics for untrusted contracts
|
|
1468
|
+
logger.metric &&
|
|
1469
|
+
logger.metric("rodit_trust_checks", totalDuration, {
|
|
1470
|
+
result: "empty_dns",
|
|
1471
|
+
domain: extractedDomain,
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
const totalDuration = Date.now() - startTime;
|
|
1478
|
+
|
|
1479
|
+
logger.warn("Smart contract not trusted - DNS lookup failed", {
|
|
1480
|
+
component: "AuthServices",
|
|
1481
|
+
method: "verify_rodit_istrusted_issuingsmartcontract",
|
|
1482
|
+
requestId,
|
|
1483
|
+
duration: totalDuration,
|
|
1484
|
+
smartContract: smartontractnonear,
|
|
1485
|
+
domain: extractedDomain,
|
|
1486
|
+
dnsEntry: enablingdnsentry,
|
|
1487
|
+
dnsError: error.code,
|
|
1488
|
+
isTrusted: false,
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
// Add metrics for DNS errors
|
|
1492
|
+
logger.metric &&
|
|
1493
|
+
logger.metric("rodit_trust_checks", totalDuration, {
|
|
1494
|
+
result: "dns_error",
|
|
1495
|
+
domain: extractedDomain,
|
|
1496
|
+
error_code: error.code,
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
const duration = Date.now() - startTime;
|
|
1503
|
+
|
|
1504
|
+
logger.error("Trust verification failed", {
|
|
1505
|
+
component: "AuthServices",
|
|
1506
|
+
method: "verify_rodit_istrusted_issuingsmartcontract",
|
|
1507
|
+
requestId,
|
|
1508
|
+
duration,
|
|
1509
|
+
url: ownsubjectuniqueidentifier_url,
|
|
1510
|
+
error: {
|
|
1511
|
+
message: error.message,
|
|
1512
|
+
stack: error.stack,
|
|
1513
|
+
name: error.name,
|
|
1514
|
+
},
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
// Add metrics for verification errors
|
|
1518
|
+
logger.metric &&
|
|
1519
|
+
logger.metric("rodit_trust_errors", 1, {
|
|
1520
|
+
error_type: error.name || "Unknown",
|
|
1521
|
+
message: error.message,
|
|
1522
|
+
});
|
|
1523
|
+
|
|
1524
|
+
return false;
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
async function verify_rodit_isamatch(own_service_provider_id, peer_rodit) {
|
|
1529
|
+
const requestId = ulid();
|
|
1530
|
+
const startTime = Date.now();
|
|
1531
|
+
|
|
1532
|
+
// Get login mode configuration
|
|
1533
|
+
const config = require("../../services/configsdk");
|
|
1534
|
+
const loginMode = config.get("SECURITY_OPTIONS.LOGIN_MODE", "partner").toLowerCase();
|
|
1535
|
+
|
|
1536
|
+
// Track the last rejection reason for better error reporting
|
|
1537
|
+
let lastRejectionReason = null;
|
|
1538
|
+
let lastVerificationType = null;
|
|
1539
|
+
|
|
1540
|
+
logger.debug("Starting RODiT match verification", {
|
|
1541
|
+
component: "RoditAuth",
|
|
1542
|
+
method: "verify_rodit_isamatch",
|
|
1543
|
+
requestId,
|
|
1544
|
+
ownServiceProviderId: own_service_provider_id,
|
|
1545
|
+
peerRoditId: peer_rodit?.token_id,
|
|
1546
|
+
loginMode,
|
|
1547
|
+
});
|
|
1548
|
+
|
|
1549
|
+
try {
|
|
1550
|
+
const own_provider_components = own_service_provider_id.split(";");
|
|
1551
|
+
|
|
1552
|
+
logger.debug("Split provider components", {
|
|
1553
|
+
requestId,
|
|
1554
|
+
componentCount: own_provider_components.length,
|
|
1555
|
+
components: own_provider_components,
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// Get blockchain and contract parts
|
|
1559
|
+
const bcPart = own_provider_components.find((part) =>
|
|
1560
|
+
part.startsWith("bc=")
|
|
1561
|
+
);
|
|
1562
|
+
const scPart = own_provider_components.find((part) =>
|
|
1563
|
+
part.startsWith("sc=")
|
|
1564
|
+
);
|
|
1565
|
+
|
|
1566
|
+
// Find all ID components
|
|
1567
|
+
const idComponents = own_provider_components.filter(
|
|
1568
|
+
(part) =>
|
|
1569
|
+
part.startsWith("id=") &&
|
|
1570
|
+
!part.startsWith("bc=") &&
|
|
1571
|
+
!part.startsWith("sc=")
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
if (!bcPart || !scPart || idComponents.length < 1) {
|
|
1575
|
+
logger.error("Invalid provider ID format", {
|
|
1576
|
+
component: "AuthServices",
|
|
1577
|
+
requestId,
|
|
1578
|
+
duration: Date.now() - startTime,
|
|
1579
|
+
providerId: own_service_provider_id,
|
|
1580
|
+
components: own_provider_components,
|
|
1581
|
+
hasBlockchain: !!bcPart,
|
|
1582
|
+
hasSmartContract: !!scPart,
|
|
1583
|
+
idCount: idComponents.length,
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
// Add metrics for format errors
|
|
1587
|
+
logger.metric &&
|
|
1588
|
+
logger.metric("rodit_match_format_errors", 1, {
|
|
1589
|
+
error_type: "invalid_provider_id",
|
|
1590
|
+
bc_part_present: !!bcPart,
|
|
1591
|
+
sc_part_present: !!scPart,
|
|
1592
|
+
id_count: idComponents.length,
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
return false;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// Construct the base prefix
|
|
1599
|
+
const base_prefix = `${bcPart};${scPart}`;
|
|
1600
|
+
logger.debug("Constructed base prefix", {
|
|
1601
|
+
requestId,
|
|
1602
|
+
basePrefix: base_prefix,
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
// Extract the peer's service provider IDs for comparison
|
|
1606
|
+
const peer_service_provider_id = peer_rodit.metadata.serviceprovider_id;
|
|
1607
|
+
const peer_provider_components = peer_service_provider_id.split(";");
|
|
1608
|
+
const peer_idComponents = peer_provider_components.filter(
|
|
1609
|
+
(part) => part.startsWith("id=") && !part.startsWith("bc=") && !part.startsWith("sc=")
|
|
1610
|
+
);
|
|
1611
|
+
|
|
1612
|
+
logger.debug("Peer service provider analysis", {
|
|
1613
|
+
requestId,
|
|
1614
|
+
peerServiceProviderId: peer_service_provider_id,
|
|
1615
|
+
peerIdComponents: peer_idComponents,
|
|
1616
|
+
ownIdComponents: idComponents,
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
// Try verification with each ID component
|
|
1620
|
+
for (let i = 0; i < idComponents.length; i++) {
|
|
1621
|
+
const idPosition = i + 1;
|
|
1622
|
+
const signing_token_id = `${base_prefix};${idComponents[i]}`;
|
|
1623
|
+
const current_own_id = idComponents[i];
|
|
1624
|
+
|
|
1625
|
+
// Determine verification type based on service provider ID comparison
|
|
1626
|
+
// PARTNER: Different service provider IDs (client-server relationship)
|
|
1627
|
+
// PEER: Same service provider ID (peer-to-peer relationship)
|
|
1628
|
+
const isSignedBySameProvider = peer_idComponents.includes(current_own_id);
|
|
1629
|
+
const verificationType = isSignedBySameProvider ? "PARTNER":"PEER";
|
|
1630
|
+
const isPartnerVerification = !isSignedBySameProvider;
|
|
1631
|
+
const isPeerVerification = isSignedBySameProvider;
|
|
1632
|
+
|
|
1633
|
+
logger.debug(
|
|
1634
|
+
`Trying ${verificationType} verification with ID [${idPosition}/${idComponents.length}]`,
|
|
1635
|
+
{
|
|
1636
|
+
requestId,
|
|
1637
|
+
idPosition,
|
|
1638
|
+
verificationType,
|
|
1639
|
+
totalIds: idComponents.length,
|
|
1640
|
+
signingTokenId: signing_token_id,
|
|
1641
|
+
currentOwnId: current_own_id,
|
|
1642
|
+
peerIdComponents: peer_idComponents,
|
|
1643
|
+
isSignedBySameProvider,
|
|
1644
|
+
relationshipType: isSignedBySameProvider ? "peer-to-peer" : "client-to-server"
|
|
1645
|
+
}
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
logger.debug("About to fetch signing RODiT", {
|
|
1649
|
+
requestId,
|
|
1650
|
+
idPosition,
|
|
1651
|
+
verificationType,
|
|
1652
|
+
signingTokenId: signing_token_id,
|
|
1653
|
+
expectedAccount: current_own_id.replace('id=', ''),
|
|
1654
|
+
peerServiceProviderId: peer_service_provider_id
|
|
1655
|
+
});
|
|
1656
|
+
|
|
1657
|
+
const tokenFetchStart = Date.now();
|
|
1658
|
+
const signing_rodit = await nearorg_rpc_tokenfromroditid(
|
|
1659
|
+
signing_token_id
|
|
1660
|
+
);
|
|
1661
|
+
const tokenFetchDuration = Date.now() - tokenFetchStart;
|
|
1662
|
+
|
|
1663
|
+
logger.debug("Retrieved signing RODiT - DETAILED DEBUG", {
|
|
1664
|
+
requestId,
|
|
1665
|
+
idPosition,
|
|
1666
|
+
verificationType,
|
|
1667
|
+
tokenFetchDuration,
|
|
1668
|
+
tokenId: signing_rodit?.token_id,
|
|
1669
|
+
ownerId: signing_rodit?.owner_id,
|
|
1670
|
+
ownerIdExpected: current_own_id.replace('id=', ''),
|
|
1671
|
+
ownerIdMatches: signing_rodit?.owner_id === current_own_id.replace('id=', ''),
|
|
1672
|
+
signingTokenIdRequested: signing_token_id,
|
|
1673
|
+
hasMetadata: !!signing_rodit?.metadata,
|
|
1674
|
+
metadataServiceProviderId: signing_rodit?.metadata?.serviceprovider_id
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
// Process the owner ID
|
|
1678
|
+
try {
|
|
1679
|
+
// Add detailed logging for debugging
|
|
1680
|
+
logger.debug("Processing owner ID for signing verification", {
|
|
1681
|
+
requestId,
|
|
1682
|
+
idPosition,
|
|
1683
|
+
verificationType,
|
|
1684
|
+
ownerIdType: typeof signing_rodit.owner_id,
|
|
1685
|
+
ownerIdValue: signing_rodit.owner_id,
|
|
1686
|
+
ownerIdLength: signing_rodit.owner_id?.length,
|
|
1687
|
+
isValidHex: signing_rodit.owner_id && /^[0-9a-fA-F]+$/.test(signing_rodit.owner_id),
|
|
1688
|
+
peerSignature: peer_rodit.metadata.serviceprovider_signature,
|
|
1689
|
+
});
|
|
1690
|
+
|
|
1691
|
+
const bytes_signing_owner_id = new Uint8Array(
|
|
1692
|
+
Buffer.from(signing_rodit.owner_id, "hex")
|
|
1693
|
+
);
|
|
1694
|
+
|
|
1695
|
+
logger.debug("Hex conversion result", {
|
|
1696
|
+
requestId,
|
|
1697
|
+
idPosition,
|
|
1698
|
+
verificationType,
|
|
1699
|
+
resultLength: bytes_signing_owner_id.length,
|
|
1700
|
+
expectedLength: CONSTANTS.RODIT_ID_PK_SZ,
|
|
1701
|
+
bufferFirst4Bytes: Array.from(bytes_signing_owner_id.slice(0, 4)),
|
|
1702
|
+
});
|
|
1703
|
+
|
|
1704
|
+
if (bytes_signing_owner_id.length !== CONSTANTS.RODIT_ID_PK_SZ) {
|
|
1705
|
+
logger.warn(`Invalid signing key length for ${verificationType} verification (ID position: ${idPosition})`, {
|
|
1706
|
+
requestId,
|
|
1707
|
+
verificationType,
|
|
1708
|
+
actual: bytes_signing_owner_id.length,
|
|
1709
|
+
expected: CONSTANTS.RODIT_ID_PK_SZ,
|
|
1710
|
+
ownerIdValue: signing_rodit.owner_id,
|
|
1711
|
+
ownerIdType: typeof signing_rodit.owner_id,
|
|
1712
|
+
});
|
|
1713
|
+
continue; // Try the next ID
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// Process the signature
|
|
1717
|
+
const base64urlSignature =
|
|
1718
|
+
peer_rodit.metadata.serviceprovider_signature;
|
|
1719
|
+
const base64Signature = base64urlSignature
|
|
1720
|
+
.replace(/-/g, "+")
|
|
1721
|
+
.replace(/_/g, "/")
|
|
1722
|
+
.padEnd(
|
|
1723
|
+
base64urlSignature.length +
|
|
1724
|
+
((4 - (base64urlSignature.length % 4)) % 4),
|
|
1725
|
+
"="
|
|
1726
|
+
);
|
|
1727
|
+
|
|
1728
|
+
const signatureBytes = new Uint8Array(
|
|
1729
|
+
Buffer.from(base64Signature, "base64")
|
|
1730
|
+
);
|
|
1731
|
+
|
|
1732
|
+
if (signatureBytes.length !== CONSTANTS.RODIT_ID_SIGNATURE_SZ) {
|
|
1733
|
+
logger.warn(`Invalid signature length for ${verificationType} verification (ID position: ${idPosition})`, {
|
|
1734
|
+
requestId,
|
|
1735
|
+
verificationType,
|
|
1736
|
+
actual: signatureBytes.length,
|
|
1737
|
+
expected: CONSTANTS.RODIT_ID_SIGNATURE_SZ,
|
|
1738
|
+
});
|
|
1739
|
+
continue; // Try the next ID
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Prepare the hash input - MUST exactly match verifyRoditSignature function format
|
|
1743
|
+
const hashInput = {
|
|
1744
|
+
token_id: peer_rodit.token_id,
|
|
1745
|
+
openapijson_url: peer_rodit.metadata.openapijson_url,
|
|
1746
|
+
not_after: peer_rodit.metadata.not_after,
|
|
1747
|
+
not_before: peer_rodit.metadata.not_before,
|
|
1748
|
+
max_requests: String(peer_rodit.metadata.max_requests),
|
|
1749
|
+
maxrq_window: String(peer_rodit.metadata.maxrq_window),
|
|
1750
|
+
webhook_cidr: peer_rodit.metadata.webhook_cidr,
|
|
1751
|
+
allowed_cidr: peer_rodit.metadata.allowed_cidr,
|
|
1752
|
+
allowed_iso3166list: peer_rodit.metadata.allowed_iso3166list,
|
|
1753
|
+
jwt_duration: peer_rodit.metadata.jwt_duration,
|
|
1754
|
+
permissioned_routes: peer_rodit.metadata.permissioned_routes,
|
|
1755
|
+
serviceprovider_id: peer_rodit.metadata.serviceprovider_id,
|
|
1756
|
+
subjectuniqueidentifier_url: peer_rodit.metadata.subjectuniqueidentifier_url,
|
|
1757
|
+
};
|
|
1758
|
+
|
|
1759
|
+
// Debug logging to compare with working frontend verification
|
|
1760
|
+
logger.debug("Backend hash input structure for verification", {
|
|
1761
|
+
requestId,
|
|
1762
|
+
idPosition,
|
|
1763
|
+
verificationType,
|
|
1764
|
+
hashInput: JSON.stringify(hashInput, null, 2),
|
|
1765
|
+
peerSignature: peer_rodit.metadata.serviceprovider_signature,
|
|
1766
|
+
signingOwnerId: bytes_signing_owner_id ? Buffer.from(bytes_signing_owner_id).toString('hex') : 'null',
|
|
1767
|
+
});
|
|
1768
|
+
|
|
1769
|
+
const hashStart = Date.now();
|
|
1770
|
+
const hashHex = calculateCanonicalHash(hashInput);
|
|
1771
|
+
const hashBytes = new Uint8Array(Buffer.from(hashHex, "hex"));
|
|
1772
|
+
const hashDuration = Date.now() - hashStart;
|
|
1773
|
+
|
|
1774
|
+
logger.debug("Hash calculation completed", {
|
|
1775
|
+
requestId,
|
|
1776
|
+
idPosition,
|
|
1777
|
+
verificationType,
|
|
1778
|
+
hashHex: hashHex.substring(0, 32) + '...',
|
|
1779
|
+
hashLength: hashHex.length,
|
|
1780
|
+
hashDuration,
|
|
1781
|
+
});
|
|
1782
|
+
|
|
1783
|
+
logger.debug("Calculated hash for verification", {
|
|
1784
|
+
requestId,
|
|
1785
|
+
idPosition,
|
|
1786
|
+
verificationType,
|
|
1787
|
+
hashDuration,
|
|
1788
|
+
hashLength: hashBytes.length,
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
// Verify the signature
|
|
1792
|
+
const verifyStart = Date.now();
|
|
1793
|
+
const is_valid = nacl.sign.detached.verify(
|
|
1794
|
+
hashBytes,
|
|
1795
|
+
signatureBytes,
|
|
1796
|
+
bytes_signing_owner_id
|
|
1797
|
+
);
|
|
1798
|
+
const verifyDuration = Date.now() - verifyStart;
|
|
1799
|
+
|
|
1800
|
+
logger.debug("Signature verification result", {
|
|
1801
|
+
requestId,
|
|
1802
|
+
idPosition,
|
|
1803
|
+
verificationType,
|
|
1804
|
+
verifyDuration,
|
|
1805
|
+
isValid: is_valid,
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
if (is_valid) {
|
|
1809
|
+
const totalDuration = Date.now() - startTime;
|
|
1810
|
+
|
|
1811
|
+
// Enforce login mode policy
|
|
1812
|
+
const shouldAccept = (
|
|
1813
|
+
loginMode === "promiscuous" || // Accept all
|
|
1814
|
+
(loginMode === "partner" && verificationType === "PARTNER") || // Accept only Partner
|
|
1815
|
+
(loginMode === "p2p" && verificationType === "PEER") // Accept only Peer
|
|
1816
|
+
);
|
|
1817
|
+
|
|
1818
|
+
if (!shouldAccept) {
|
|
1819
|
+
logger.warn(`${verificationType} login rejected by LOGIN_MODE policy`, {
|
|
1820
|
+
component: "AuthServices",
|
|
1821
|
+
method: "verify_rodit_isamatch",
|
|
1822
|
+
requestId,
|
|
1823
|
+
duration: totalDuration,
|
|
1824
|
+
verificationType,
|
|
1825
|
+
loginMode,
|
|
1826
|
+
idPosition,
|
|
1827
|
+
policyReason: `LOGIN_MODE=${loginMode} does not accept ${verificationType} logins`
|
|
1828
|
+
});
|
|
1829
|
+
|
|
1830
|
+
// Add metrics for policy rejection
|
|
1831
|
+
logger.metric &&
|
|
1832
|
+
logger.metric("rodit_match_verification", totalDuration, {
|
|
1833
|
+
result: "policy_rejected",
|
|
1834
|
+
verification_type: verificationType.toLowerCase(),
|
|
1835
|
+
login_mode: loginMode,
|
|
1836
|
+
});
|
|
1837
|
+
|
|
1838
|
+
// Track this rejection for error reporting
|
|
1839
|
+
lastRejectionReason = "LOGIN_MODE_POLICY_REJECTED";
|
|
1840
|
+
lastVerificationType = verificationType;
|
|
1841
|
+
|
|
1842
|
+
// Continue to try next ID component
|
|
1843
|
+
continue;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
// Log based on verification type
|
|
1847
|
+
logger.info(`${verificationType} login verified successfully`, {
|
|
1848
|
+
component: "AuthServices",
|
|
1849
|
+
method: "verify_rodit_isamatch",
|
|
1850
|
+
requestId,
|
|
1851
|
+
duration: totalDuration,
|
|
1852
|
+
verificationType,
|
|
1853
|
+
loginMode,
|
|
1854
|
+
idPosition,
|
|
1855
|
+
partnerVerification: isPartnerVerification,
|
|
1856
|
+
peerVerification: isPeerVerification
|
|
1857
|
+
});
|
|
1858
|
+
|
|
1859
|
+
// Add metrics for successful matching
|
|
1860
|
+
logger.metric &&
|
|
1861
|
+
logger.metric("rodit_match_verification", totalDuration, {
|
|
1862
|
+
result: "success",
|
|
1863
|
+
verification_type: verificationType.toLowerCase(),
|
|
1864
|
+
login_mode: loginMode,
|
|
1865
|
+
});
|
|
1866
|
+
|
|
1867
|
+
return {
|
|
1868
|
+
isMatch: true,
|
|
1869
|
+
verificationType,
|
|
1870
|
+
loginMode
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
logger.debug(`${verificationType} verification failed (ID position: ${idPosition})`, {
|
|
1875
|
+
requestId,
|
|
1876
|
+
verificationType
|
|
1877
|
+
});
|
|
1878
|
+
} catch (verifyError) {
|
|
1879
|
+
logger.warn(`Error during ${verificationType} verification (ID position: ${idPosition})`, {
|
|
1880
|
+
requestId,
|
|
1881
|
+
verificationType,
|
|
1882
|
+
error: verifyError.message,
|
|
1883
|
+
stack: verifyError.stack,
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// If we get here, all verification attempts failed
|
|
1889
|
+
const totalDuration = Date.now() - startTime;
|
|
1890
|
+
|
|
1891
|
+
// Determine the failure reason and message
|
|
1892
|
+
let failureReason, failureMessage;
|
|
1893
|
+
|
|
1894
|
+
if (lastRejectionReason === "LOGIN_MODE_POLICY_REJECTED") {
|
|
1895
|
+
// Include both the policy and what was rejected in the error code
|
|
1896
|
+
failureReason = `LOGIN_MODE_POLICY_REJECTED_${lastVerificationType}_BY_${loginMode.toUpperCase()}`;
|
|
1897
|
+
failureMessage = `${lastVerificationType} login rejected: LOGIN_MODE=${loginMode} does not accept ${lastVerificationType} logins`;
|
|
1898
|
+
} else {
|
|
1899
|
+
failureReason = "RODIT_FAMILY_MISMATCH";
|
|
1900
|
+
failureMessage = "RODiT does not belong to the same family as the server";
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
logger.error("All verification attempts failed", {
|
|
1904
|
+
component: "AuthServices",
|
|
1905
|
+
method: "verify_rodit_isamatch",
|
|
1906
|
+
requestId,
|
|
1907
|
+
duration: totalDuration,
|
|
1908
|
+
ownServiceProviderId: own_service_provider_id,
|
|
1909
|
+
peerRoditId: peer_rodit?.token_id,
|
|
1910
|
+
attemptCount: idComponents.length,
|
|
1911
|
+
failureReason,
|
|
1912
|
+
lastRejectionReason,
|
|
1913
|
+
lastVerificationType,
|
|
1914
|
+
});
|
|
1915
|
+
|
|
1916
|
+
// Add metrics for failed matching
|
|
1917
|
+
logger.metric &&
|
|
1918
|
+
logger.metric("rodit_match_verification", totalDuration, {
|
|
1919
|
+
result: "failure",
|
|
1920
|
+
attempts: idComponents.length,
|
|
1921
|
+
failure_reason: failureReason,
|
|
1922
|
+
});
|
|
1923
|
+
|
|
1924
|
+
return {
|
|
1925
|
+
isMatch: false,
|
|
1926
|
+
failureReason,
|
|
1927
|
+
failureMessage
|
|
1928
|
+
};
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
const duration = Date.now() - startTime;
|
|
1931
|
+
|
|
1932
|
+
logger.error("RODiT match verification error", {
|
|
1933
|
+
component: "AuthServices",
|
|
1934
|
+
method: "verify_rodit_isamatch",
|
|
1935
|
+
requestId,
|
|
1936
|
+
duration,
|
|
1937
|
+
ownServiceProviderId: own_service_provider_id,
|
|
1938
|
+
peerRoditId: peer_rodit?.token_id,
|
|
1939
|
+
error: {
|
|
1940
|
+
message: error.message,
|
|
1941
|
+
stack: error.stack,
|
|
1942
|
+
name: error.name,
|
|
1943
|
+
},
|
|
1944
|
+
});
|
|
1945
|
+
|
|
1946
|
+
// Add metrics for verification errors
|
|
1947
|
+
logger.metric &&
|
|
1948
|
+
logger.metric("rodit_match_errors", 1, {
|
|
1949
|
+
error_type: error.name || "Unknown",
|
|
1950
|
+
});
|
|
1951
|
+
|
|
1952
|
+
return {
|
|
1953
|
+
isMatch: false,
|
|
1954
|
+
failureReason: "VERIFICATION_ERROR",
|
|
1955
|
+
failureMessage: `RODiT match verification error: ${error.message}`
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
module.exports = {
|
|
1961
|
+
verify_rodit_ownership,
|
|
1962
|
+
verify_rodit_ownership_withnep413,
|
|
1963
|
+
resolve_peer_rodit_for_login,
|
|
1964
|
+
verify_peer_rodit,
|
|
1965
|
+
validateTimestamp,
|
|
1966
|
+
verify_rodit_isactive,
|
|
1967
|
+
verify_rodit_isamatch,
|
|
1968
|
+
verify_rodit_islive,
|
|
1969
|
+
verify_rodit_istrusted_issuingsmartcontract,
|
|
1970
|
+
authenticate_webhook
|
|
1971
|
+
};
|