@kya-os/mcp-i-cloudflare 1.7.13 → 1.7.16
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/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +16 -4
- package/dist/agent.js.map +1 -1
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +110 -4
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/services/consent-audit.service.d.ts +55 -0
- package/dist/services/consent-audit.service.d.ts.map +1 -1
- package/dist/services/consent-audit.service.js +116 -0
- package/dist/services/consent-audit.service.js.map +1 -1
- package/dist/services/consent-templates/base/base-template.d.ts.map +1 -1
- package/dist/services/consent-templates/base/base-template.js +10 -1
- package/dist/services/consent-templates/base/base-template.js.map +1 -1
- package/dist/services/consent-templates/modes/credentials.template.d.ts.map +1 -1
- package/dist/services/consent-templates/modes/credentials.template.js +4 -1
- package/dist/services/consent-templates/modes/credentials.template.js.map +1 -1
- package/dist/services/consent-templates/registry.d.ts +6 -0
- package/dist/services/consent-templates/registry.d.ts.map +1 -1
- package/dist/services/consent-templates/registry.js +11 -17
- package/dist/services/consent-templates/registry.js.map +1 -1
- package/dist/services/consent-templates/template-renderer.d.ts +3 -0
- package/dist/services/consent-templates/template-renderer.d.ts.map +1 -1
- package/dist/services/consent-templates/template-renderer.js +9 -2
- package/dist/services/consent-templates/template-renderer.js.map +1 -1
- package/dist/services/consent-templates/types.d.ts +8 -0
- package/dist/services/consent-templates/types.d.ts.map +1 -1
- package/dist/services/consent.service.d.ts +29 -0
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +477 -256
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/credential-auth.handler.d.ts.map +1 -1
- package/dist/services/credential-auth.handler.js +55 -2
- package/dist/services/credential-auth.handler.js.map +1 -1
- package/package.json +6 -5
|
@@ -13,7 +13,7 @@ import { STORAGE_KEYS } from "../constants/storage-keys";
|
|
|
13
13
|
import { loadDay0Config, getDelegationFieldName } from "../utils/day0-config";
|
|
14
14
|
import { validateConsentApprovalRequest, } from "@kya-os/contracts/consent";
|
|
15
15
|
import { AGENTSHIELD_ENDPOINTS, createDelegationAPIResponseSchema, createDelegationResponseSchema, } from "@kya-os/contracts/agentshield-api";
|
|
16
|
-
import { fetchRemoteConfig, createUnsignedVCJWT, completeVCJWT, parseVCJWT, base64urlEncodeFromBytes, base64urlDecodeToBytes, bytesToBase64, generateDidKeyFromBase64, createDelegationVerifier, createDidKeyResolver, } from "@kya-os/mcp-i-core";
|
|
16
|
+
import { fetchRemoteConfig, createUnsignedVCJWT, completeVCJWT, parseVCJWT, base64urlEncodeFromBytes, base64urlDecodeToBytes, bytesToBase64, generateDidKeyFromBase64, createDelegationVerifier, createDidKeyResolver, logger, } from "@kya-os/mcp-i-core";
|
|
17
17
|
import { wrapDelegationAsVC } from "@kya-os/contracts";
|
|
18
18
|
import { WebCryptoProvider } from "../providers/crypto";
|
|
19
19
|
import { ConsentAuditService } from "./consent-audit.service";
|
|
@@ -76,11 +76,31 @@ export class ConsentService {
|
|
|
76
76
|
await this.auditInitPromise;
|
|
77
77
|
}
|
|
78
78
|
catch (error) {
|
|
79
|
-
|
|
79
|
+
logger.warn("[ConsentService] Audit service initialization failed:", error);
|
|
80
80
|
// Don't throw - audit failures shouldn't break consent flow
|
|
81
81
|
}
|
|
82
82
|
return this.auditService;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Public audit helpers for OAuth callback handler
|
|
86
|
+
*
|
|
87
|
+
* The OAuth callback handler lives in `src/runtime/oauth-handler.ts` and only
|
|
88
|
+
* has access to a `ConsentService` instance (not the full runtime wiring needed
|
|
89
|
+
* to construct `ConsentAuditService`). These helpers bridge that gap and ensure
|
|
90
|
+
* OAuth auth events generate proofs when proofing is enabled.
|
|
91
|
+
*/
|
|
92
|
+
async logOAuthAuthSuccess(event) {
|
|
93
|
+
const auditService = await this.getAuditService(event.projectId);
|
|
94
|
+
if (!auditService)
|
|
95
|
+
return;
|
|
96
|
+
await auditService.logOAuthSuccess(event);
|
|
97
|
+
}
|
|
98
|
+
async logOAuthAuthFailed(event) {
|
|
99
|
+
const auditService = await this.getAuditService(event.projectId);
|
|
100
|
+
if (!auditService)
|
|
101
|
+
return;
|
|
102
|
+
await auditService.logOAuthFailed(event);
|
|
103
|
+
}
|
|
84
104
|
/**
|
|
85
105
|
* Initialize audit service - fetches config from remote API
|
|
86
106
|
*
|
|
@@ -95,7 +115,7 @@ export class ConsentService {
|
|
|
95
115
|
// ✅ CRITICAL: Fetch config from remote API
|
|
96
116
|
const config = await this.getConfigFromRemoteAPI(projectId);
|
|
97
117
|
if (!config?.proofing?.enabled) {
|
|
98
|
-
|
|
118
|
+
logger.debug("[ConsentService] Proofing not enabled in remote config");
|
|
99
119
|
return; // Proofing not enabled
|
|
100
120
|
}
|
|
101
121
|
// Get identity (async - requires runtime to be initialized)
|
|
@@ -105,16 +125,16 @@ export class ConsentService {
|
|
|
105
125
|
// Get audit logger
|
|
106
126
|
const auditLogger = this.runtime.getAuditLogger();
|
|
107
127
|
if (!auditLogger) {
|
|
108
|
-
|
|
128
|
+
logger.warn("[ConsentService] AuditLogger not available");
|
|
109
129
|
return;
|
|
110
130
|
}
|
|
111
131
|
// Create audit service with fetched config
|
|
112
132
|
this.auditService = new ConsentAuditService(new ProofService(config, this.runtime), auditLogger, proofGenerator, config, // ✅ Config fetched from remote API
|
|
113
133
|
this.runtime);
|
|
114
|
-
|
|
134
|
+
logger.debug("[ConsentService] Audit service initialized successfully");
|
|
115
135
|
}
|
|
116
136
|
catch (error) {
|
|
117
|
-
|
|
137
|
+
logger.debug("[ConsentService] Failed to initialize audit service:", error);
|
|
118
138
|
// Don't throw - audit failures shouldn't break consent flow
|
|
119
139
|
}
|
|
120
140
|
}
|
|
@@ -131,7 +151,7 @@ export class ConsentService {
|
|
|
131
151
|
*/
|
|
132
152
|
async getConfigFromRemoteAPI(projectId) {
|
|
133
153
|
if (!this.env.AGENTSHIELD_API_KEY) {
|
|
134
|
-
|
|
154
|
+
logger.warn("[ConsentService] No API key for runtime config fetch");
|
|
135
155
|
return undefined;
|
|
136
156
|
}
|
|
137
157
|
try {
|
|
@@ -166,11 +186,11 @@ export class ConsentService {
|
|
|
166
186
|
const runtimeIdentity = await this.runtime?.getIdentity();
|
|
167
187
|
if (runtimeIdentity?.did) {
|
|
168
188
|
identityConfig.serverDid = runtimeIdentity.did;
|
|
169
|
-
|
|
189
|
+
logger.debug("[ConsentService] Populated serverDid from runtime identity");
|
|
170
190
|
}
|
|
171
191
|
}
|
|
172
192
|
catch (error) {
|
|
173
|
-
|
|
193
|
+
logger.warn("[ConsentService] Failed to get runtime identity for serverDid:", error);
|
|
174
194
|
}
|
|
175
195
|
}
|
|
176
196
|
}
|
|
@@ -179,7 +199,7 @@ export class ConsentService {
|
|
|
179
199
|
return config;
|
|
180
200
|
}
|
|
181
201
|
catch (error) {
|
|
182
|
-
|
|
202
|
+
logger.warn("[ConsentService] Error fetching runtime config:", error);
|
|
183
203
|
return undefined;
|
|
184
204
|
}
|
|
185
205
|
}
|
|
@@ -210,7 +230,7 @@ export class ConsentService {
|
|
|
210
230
|
const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
|
|
211
231
|
const mappedUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
|
|
212
232
|
if (mappedUserDid) {
|
|
213
|
-
|
|
233
|
+
logger.debug("[ConsentService] Found persistent User DID from OAuth mapping:", {
|
|
214
234
|
provider: oauthIdentity.provider,
|
|
215
235
|
userDid: mappedUserDid.substring(0, 20) + "...",
|
|
216
236
|
});
|
|
@@ -218,7 +238,7 @@ export class ConsentService {
|
|
|
218
238
|
}
|
|
219
239
|
}
|
|
220
240
|
catch (error) {
|
|
221
|
-
|
|
241
|
+
logger.warn("[ConsentService] Failed to check OAuth mapping:", error);
|
|
222
242
|
}
|
|
223
243
|
}
|
|
224
244
|
// Priority 2: Check session storage for existing DID
|
|
@@ -231,7 +251,7 @@ export class ConsentService {
|
|
|
231
251
|
}
|
|
232
252
|
}
|
|
233
253
|
catch (error) {
|
|
234
|
-
|
|
254
|
+
logger.warn("[ConsentService] Failed to read session cache:", error);
|
|
235
255
|
}
|
|
236
256
|
}
|
|
237
257
|
// Phase 5: No ephemeral DID generation - session stays anonymous
|
|
@@ -250,7 +270,7 @@ export class ConsentService {
|
|
|
250
270
|
*/
|
|
251
271
|
async updateSessionWithIdentity(sessionId, userDid, oauthIdentity) {
|
|
252
272
|
if (!this.env.DELEGATION_STORAGE) {
|
|
253
|
-
|
|
273
|
+
logger.warn("[ConsentService] No DELEGATION_STORAGE - cannot persist session identity");
|
|
254
274
|
return;
|
|
255
275
|
}
|
|
256
276
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
@@ -270,7 +290,7 @@ export class ConsentService {
|
|
|
270
290
|
await this.env.DELEGATION_STORAGE.put(userAgentKey, delegationToken, {
|
|
271
291
|
expirationTtl: delegationTtl,
|
|
272
292
|
});
|
|
273
|
-
|
|
293
|
+
logger.debug("[ConsentService] 🔄 DELEGATION UPGRADE: Migrated consent-only delegation to user+agent scoped:", {
|
|
274
294
|
sessionId: sessionId.substring(0, 8) + "...",
|
|
275
295
|
userDid: userDid.substring(0, 20) + "...",
|
|
276
296
|
agentDid: agentDid.substring(0, 20) + "...",
|
|
@@ -294,7 +314,7 @@ export class ConsentService {
|
|
|
294
314
|
}),
|
|
295
315
|
};
|
|
296
316
|
await this.env.DELEGATION_STORAGE.put(sessionKey, JSON.stringify(sessionData), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
|
|
297
|
-
|
|
317
|
+
logger.debug("[ConsentService] Session identity updated:", {
|
|
298
318
|
sessionId: sessionId.substring(0, 8) + "...",
|
|
299
319
|
userDid: userDid.substring(0, 20) + "...",
|
|
300
320
|
provider: oauthIdentity?.provider,
|
|
@@ -302,7 +322,7 @@ export class ConsentService {
|
|
|
302
322
|
});
|
|
303
323
|
}
|
|
304
324
|
catch (error) {
|
|
305
|
-
|
|
325
|
+
logger.warn("[ConsentService] Failed to update session with identity:", error);
|
|
306
326
|
throw error;
|
|
307
327
|
}
|
|
308
328
|
// Create OAuth → DID mapping for future lookups
|
|
@@ -312,13 +332,13 @@ export class ConsentService {
|
|
|
312
332
|
await this.env.DELEGATION_STORAGE.put(oauthKey, userDid, {
|
|
313
333
|
expirationTtl: 90 * 24 * 60 * 60, // 90 days for persistent mapping
|
|
314
334
|
});
|
|
315
|
-
|
|
335
|
+
logger.debug("[ConsentService] Created OAuth → DID mapping:", {
|
|
316
336
|
provider: oauthIdentity.provider,
|
|
317
337
|
userDid: userDid.substring(0, 20) + "...",
|
|
318
338
|
});
|
|
319
339
|
}
|
|
320
340
|
catch (error) {
|
|
321
|
-
|
|
341
|
+
logger.warn("[ConsentService] Failed to create OAuth mapping:", error);
|
|
322
342
|
// Non-fatal - session was updated successfully
|
|
323
343
|
}
|
|
324
344
|
}
|
|
@@ -343,12 +363,12 @@ export class ConsentService {
|
|
|
343
363
|
const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
|
|
344
364
|
const stored = (await this.env.DELEGATION_STORAGE.get(key, "json"));
|
|
345
365
|
if (stored) {
|
|
346
|
-
|
|
366
|
+
logger.debug("[ConsentService] Found key pair in OAuth KV storage");
|
|
347
367
|
return stored;
|
|
348
368
|
}
|
|
349
369
|
}
|
|
350
370
|
catch (error) {
|
|
351
|
-
|
|
371
|
+
logger.warn("[ConsentService] KV key pair lookup failed:", error);
|
|
352
372
|
}
|
|
353
373
|
}
|
|
354
374
|
// Priority 2: Check session storage for ephemeral key pair (consent-only flow)
|
|
@@ -358,12 +378,12 @@ export class ConsentService {
|
|
|
358
378
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
359
379
|
const sessionData = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
|
|
360
380
|
if (sessionData?.keyPair) {
|
|
361
|
-
|
|
381
|
+
logger.debug("[ConsentService] Found ephemeral key pair in session storage (consent-only VC signing)");
|
|
362
382
|
return sessionData.keyPair;
|
|
363
383
|
}
|
|
364
384
|
}
|
|
365
385
|
catch (error) {
|
|
366
|
-
|
|
386
|
+
logger.warn("[ConsentService] Session key pair lookup failed:", error);
|
|
367
387
|
}
|
|
368
388
|
}
|
|
369
389
|
// Priority 3: Try UserDidManager (legacy path, may not be initialized)
|
|
@@ -378,7 +398,7 @@ export class ConsentService {
|
|
|
378
398
|
}
|
|
379
399
|
}
|
|
380
400
|
// No key pair found
|
|
381
|
-
|
|
401
|
+
logger.warn("[ConsentService] No key pair found for session - user cannot sign VCs");
|
|
382
402
|
return null;
|
|
383
403
|
}
|
|
384
404
|
/**
|
|
@@ -400,7 +420,7 @@ export class ConsentService {
|
|
|
400
420
|
// 1. Try existing lookup first
|
|
401
421
|
const existingKeyPair = await this.getKeyPairForSession(sessionId, oauthIdentity);
|
|
402
422
|
if (existingKeyPair) {
|
|
403
|
-
|
|
423
|
+
logger.debug("[ConsentService] Found existing key pair for OAuth identity");
|
|
404
424
|
return existingKeyPair;
|
|
405
425
|
}
|
|
406
426
|
// 2. Check KV storage directly by OAuth identity
|
|
@@ -411,16 +431,16 @@ export class ConsentService {
|
|
|
411
431
|
const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
|
|
412
432
|
const stored = (await this.env.DELEGATION_STORAGE.get(key, "json"));
|
|
413
433
|
if (stored) {
|
|
414
|
-
|
|
434
|
+
logger.debug("[ConsentService] Found key pair in KV storage");
|
|
415
435
|
return stored;
|
|
416
436
|
}
|
|
417
437
|
}
|
|
418
438
|
catch (error) {
|
|
419
|
-
|
|
439
|
+
logger.warn("[ConsentService] KV key pair lookup failed:", error);
|
|
420
440
|
}
|
|
421
441
|
}
|
|
422
442
|
// 3. Generate new key pair
|
|
423
|
-
|
|
443
|
+
logger.debug("[ConsentService] Generating new key pair for OAuth identity");
|
|
424
444
|
try {
|
|
425
445
|
const crypto = new WebCryptoProvider();
|
|
426
446
|
const rawKeyPair = await crypto.generateKeyPair();
|
|
@@ -443,20 +463,20 @@ export class ConsentService {
|
|
|
443
463
|
await this.env.DELEGATION_STORAGE.put(key, JSON.stringify(keyPair), {
|
|
444
464
|
expirationTtl: KEY_PAIR_TTL_SECONDS,
|
|
445
465
|
});
|
|
446
|
-
|
|
466
|
+
logger.debug("[ConsentService] Key pair persisted to KV storage");
|
|
447
467
|
}
|
|
448
468
|
catch (persistError) {
|
|
449
|
-
|
|
469
|
+
logger.warn("[ConsentService] KV key pair persistence failed (non-fatal):", persistError);
|
|
450
470
|
}
|
|
451
471
|
}
|
|
452
|
-
|
|
472
|
+
logger.debug("[ConsentService] Key pair generated:", {
|
|
453
473
|
did: did.substring(0, 30) + "...",
|
|
454
474
|
keyId,
|
|
455
475
|
});
|
|
456
476
|
return keyPair;
|
|
457
477
|
}
|
|
458
478
|
catch (error) {
|
|
459
|
-
|
|
479
|
+
logger.error("[ConsentService] Key pair generation failed:", error);
|
|
460
480
|
return null;
|
|
461
481
|
}
|
|
462
482
|
}
|
|
@@ -489,7 +509,7 @@ export class ConsentService {
|
|
|
489
509
|
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
490
510
|
const existingSession = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
|
|
491
511
|
if (existingSession?.userDid) {
|
|
492
|
-
|
|
512
|
+
logger.debug("[ConsentService] Session already has userDid, reusing:", {
|
|
493
513
|
sessionId: sessionId.substring(0, 20) + "...",
|
|
494
514
|
userDid: existingSession.userDid.substring(0, 20) + "...",
|
|
495
515
|
hasKeyPair: !!existingSession.keyPair,
|
|
@@ -498,11 +518,11 @@ export class ConsentService {
|
|
|
498
518
|
}
|
|
499
519
|
}
|
|
500
520
|
catch (error) {
|
|
501
|
-
|
|
521
|
+
logger.warn("[ConsentService] Failed to check existing session userDid:", error);
|
|
502
522
|
}
|
|
503
523
|
}
|
|
504
524
|
// Generate new ephemeral keypair - THIS TIME WE STORE IT!
|
|
505
|
-
|
|
525
|
+
logger.debug("[ConsentService] Generating ephemeral key pair for consent-only VC signing");
|
|
506
526
|
try {
|
|
507
527
|
const crypto = new WebCryptoProvider();
|
|
508
528
|
const rawKeyPair = await crypto.generateKeyPair();
|
|
@@ -528,13 +548,13 @@ export class ConsentService {
|
|
|
528
548
|
keyPair, // Store the full key pair for VC signing!
|
|
529
549
|
identityState: "ephemeral",
|
|
530
550
|
}), { expirationTtl: DEFAULT_SESSION_CACHE_TTL });
|
|
531
|
-
|
|
551
|
+
logger.debug("[ConsentService] ✅ Ephemeral key pair stored in session for VC signing");
|
|
532
552
|
}
|
|
533
553
|
catch (storeError) {
|
|
534
|
-
|
|
554
|
+
logger.warn("[ConsentService] Failed to store ephemeral key pair (VC signing may fail):", storeError);
|
|
535
555
|
}
|
|
536
556
|
}
|
|
537
|
-
|
|
557
|
+
logger.info("[ConsentService] ✅ Ephemeral key pair generated:", {
|
|
538
558
|
sessionId: sessionId.substring(0, 20) + "...",
|
|
539
559
|
userDid: ephemeralUserDid.substring(0, 30) + "...",
|
|
540
560
|
hasPrivateKey: true,
|
|
@@ -543,7 +563,7 @@ export class ConsentService {
|
|
|
543
563
|
return ephemeralUserDid;
|
|
544
564
|
}
|
|
545
565
|
catch (error) {
|
|
546
|
-
|
|
566
|
+
logger.debug("[ConsentService] Failed to generate ephemeral key pair:", error);
|
|
547
567
|
return null;
|
|
548
568
|
}
|
|
549
569
|
}
|
|
@@ -567,7 +587,7 @@ export class ConsentService {
|
|
|
567
587
|
const keyPair = providedKeyPair ??
|
|
568
588
|
(await this.getKeyPairForSession(sessionId, oauthIdentity));
|
|
569
589
|
if (!keyPair) {
|
|
570
|
-
|
|
590
|
+
logger.warn("[ConsentService] Cannot issue VC: no key pair available for session");
|
|
571
591
|
return null;
|
|
572
592
|
}
|
|
573
593
|
try {
|
|
@@ -592,7 +612,7 @@ export class ConsentService {
|
|
|
592
612
|
const signature = base64urlEncodeFromBytes(signatureBytes);
|
|
593
613
|
// Step 4: Complete the JWT
|
|
594
614
|
const jwt = completeVCJWT(signingInput, signature);
|
|
595
|
-
|
|
615
|
+
logger.debug("[ConsentService] VC issued successfully:", {
|
|
596
616
|
issuerDid: keyPair.did.substring(0, 20) + "...",
|
|
597
617
|
subjectDid: delegation.subjectDid.substring(0, 20) + "...",
|
|
598
618
|
delegationId: delegation.id,
|
|
@@ -601,7 +621,7 @@ export class ConsentService {
|
|
|
601
621
|
return jwt;
|
|
602
622
|
}
|
|
603
623
|
catch (error) {
|
|
604
|
-
|
|
624
|
+
logger.error("[ConsentService] Failed to issue VC:", error);
|
|
605
625
|
return null;
|
|
606
626
|
}
|
|
607
627
|
}
|
|
@@ -627,7 +647,7 @@ export class ConsentService {
|
|
|
627
647
|
// Step 1: Parse JWT
|
|
628
648
|
const parsed = parseVCJWT(vcJwt);
|
|
629
649
|
if (!parsed) {
|
|
630
|
-
|
|
650
|
+
logger.warn("[ConsentService] verifyDelegationVC: Invalid JWT format");
|
|
631
651
|
return {
|
|
632
652
|
valid: false,
|
|
633
653
|
reason: "Invalid JWT format",
|
|
@@ -638,7 +658,7 @@ export class ConsentService {
|
|
|
638
658
|
// Step 2: Extract VC from payload
|
|
639
659
|
const vc = parsed.payload.vc;
|
|
640
660
|
if (!vc) {
|
|
641
|
-
|
|
661
|
+
logger.warn("[ConsentService] verifyDelegationVC: No VC in JWT payload");
|
|
642
662
|
return {
|
|
643
663
|
valid: false,
|
|
644
664
|
reason: "No VC in JWT payload",
|
|
@@ -678,7 +698,7 @@ export class ConsentService {
|
|
|
678
698
|
const crypto = new WebCryptoProvider();
|
|
679
699
|
const isValid = await crypto.verify(signingInputBytes, signatureBytes, publicKeyBase64);
|
|
680
700
|
if (!isValid) {
|
|
681
|
-
|
|
701
|
+
logger.warn("[ConsentService] verifyDelegationVC: JWT signature verification failed");
|
|
682
702
|
return {
|
|
683
703
|
valid: false,
|
|
684
704
|
reason: "JWT signature verification failed",
|
|
@@ -686,7 +706,7 @@ export class ConsentService {
|
|
|
686
706
|
metrics: { totalMs: Date.now() - startTime },
|
|
687
707
|
};
|
|
688
708
|
}
|
|
689
|
-
|
|
709
|
+
logger.debug("[ConsentService] verifyDelegationVC: JWT signature verified successfully");
|
|
690
710
|
}
|
|
691
711
|
// Step 4: Run basic VC validation via DelegationCredentialVerifier
|
|
692
712
|
// Skip signature verification since we already verified the JWT signature above
|
|
@@ -702,13 +722,13 @@ export class ConsentService {
|
|
|
702
722
|
result.metrics.totalMs = Date.now() - startTime;
|
|
703
723
|
}
|
|
704
724
|
if (result.valid) {
|
|
705
|
-
|
|
725
|
+
logger.debug("[ConsentService] verifyDelegationVC: VC verified successfully", {
|
|
706
726
|
issuer: vc.issuer,
|
|
707
727
|
subject: vc.credentialSubject?.id,
|
|
708
728
|
});
|
|
709
729
|
}
|
|
710
730
|
else {
|
|
711
|
-
|
|
731
|
+
logger.warn("[ConsentService] verifyDelegationVC: VC validation failed", {
|
|
712
732
|
reason: result.reason,
|
|
713
733
|
stage: result.stage,
|
|
714
734
|
});
|
|
@@ -732,7 +752,7 @@ export class ConsentService {
|
|
|
732
752
|
// Priority 0: Check for consent-only mode (authorization.type === 'none')
|
|
733
753
|
// Consent-only means delegation is required but NO OAuth provider is needed
|
|
734
754
|
if (toolProtection?.authorization?.type === "none") {
|
|
735
|
-
|
|
755
|
+
logger.debug("[ConsentService] Tool is consent-only mode (authorization.type=none), OAuth NOT required", {
|
|
736
756
|
projectId,
|
|
737
757
|
requiresDelegation: toolProtection.requiresDelegation,
|
|
738
758
|
});
|
|
@@ -755,7 +775,7 @@ export class ConsentService {
|
|
|
755
775
|
try {
|
|
756
776
|
const resolvedProvider = await this.providerResolver.resolveProvider(toolProtection, projectId);
|
|
757
777
|
if (resolvedProvider) {
|
|
758
|
-
|
|
778
|
+
logger.debug("[ConsentService] OAuth required: ProviderResolver resolved provider", {
|
|
759
779
|
projectId,
|
|
760
780
|
provider: resolvedProvider,
|
|
761
781
|
toolRequiresDelegation: toolProtection.requiresDelegation,
|
|
@@ -767,11 +787,22 @@ export class ConsentService {
|
|
|
767
787
|
catch (error) {
|
|
768
788
|
// Check if this is ConsentOnlyModeError - not a real error
|
|
769
789
|
if (error instanceof Error && error.name === "ConsentOnlyModeError") {
|
|
770
|
-
|
|
790
|
+
logger.debug("[ConsentService] Tool is consent-only mode (ConsentOnlyModeError), OAuth NOT required", { projectId });
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
// Check for CredentialAuthModeError - credential auth does NOT require OAuth
|
|
794
|
+
// Use multiple detection methods to handle bundled code where class names may be mangled
|
|
795
|
+
const isCredentialAuthError = error instanceof Error &&
|
|
796
|
+
(error.name === "CredentialAuthModeError" ||
|
|
797
|
+
error.constructor?.name === "CredentialAuthModeError" ||
|
|
798
|
+
("provider" in error &&
|
|
799
|
+
typeof error.provider === "string"));
|
|
800
|
+
if (isCredentialAuthError) {
|
|
801
|
+
logger.debug("[ConsentService] Tool uses credential auth (CredentialAuthModeError), OAuth NOT required", { projectId });
|
|
771
802
|
return false;
|
|
772
803
|
}
|
|
773
804
|
// ProviderResolver failed - log but continue to fallback check
|
|
774
|
-
|
|
805
|
+
logger.warn("[ConsentService] ProviderResolver failed to resolve provider, falling back to project config check:", {
|
|
775
806
|
projectId,
|
|
776
807
|
error: error instanceof Error ? error.message : String(error),
|
|
777
808
|
});
|
|
@@ -805,7 +836,7 @@ export class ConsentService {
|
|
|
805
836
|
return false;
|
|
806
837
|
}
|
|
807
838
|
catch (error) {
|
|
808
|
-
|
|
839
|
+
logger.warn("[ConsentService] Failed to check OAuth requirement:", error);
|
|
809
840
|
// On error, default to not requiring OAuth (backward compatibility)
|
|
810
841
|
return false;
|
|
811
842
|
}
|
|
@@ -846,7 +877,7 @@ export class ConsentService {
|
|
|
846
877
|
codeChallenge = pkceChallenge.challenge;
|
|
847
878
|
}
|
|
848
879
|
catch (error) {
|
|
849
|
-
|
|
880
|
+
logger.warn("[ConsentService] Failed to generate PKCE challenge:", error);
|
|
850
881
|
}
|
|
851
882
|
}
|
|
852
883
|
// Build state data with required fields
|
|
@@ -881,7 +912,7 @@ export class ConsentService {
|
|
|
881
912
|
// Store state data securely in KV (10 minute TTL for OAuth flow)
|
|
882
913
|
await oauthSecurityService.storeOAuthState(stateValue, stateData, 600);
|
|
883
914
|
stateParam = stateValue;
|
|
884
|
-
|
|
915
|
+
logger.debug("[ConsentService] 🔒 SECURITY EVENT: OAuth state stored securely:", {
|
|
885
916
|
projectId,
|
|
886
917
|
agentDid: agentDid.substring(0, 20) + "...",
|
|
887
918
|
sessionId: sessionId.substring(0, 20) + "...",
|
|
@@ -894,7 +925,7 @@ export class ConsentService {
|
|
|
894
925
|
else {
|
|
895
926
|
// Fallback: Encode state as base64 (less secure, but backward compatible)
|
|
896
927
|
if (!oauthSecurityService) {
|
|
897
|
-
|
|
928
|
+
logger.warn("[ConsentService] ⚠️ SECURITY WARNING: OAuthSecurityService not provided, using insecure state encoding");
|
|
898
929
|
}
|
|
899
930
|
stateParam = btoa(JSON.stringify(stateData));
|
|
900
931
|
}
|
|
@@ -904,20 +935,20 @@ export class ConsentService {
|
|
|
904
935
|
if (provider && this.providerRegistry) {
|
|
905
936
|
// Lazy load providers if registry is empty (first request for this project)
|
|
906
937
|
if (this.providerRegistry.getProviderNames().length === 0) {
|
|
907
|
-
|
|
938
|
+
logger.debug("[ConsentService] Loading providers from AgentShield", {
|
|
908
939
|
projectId,
|
|
909
940
|
provider,
|
|
910
941
|
});
|
|
911
942
|
try {
|
|
912
943
|
await this.providerRegistry.loadFromAgentShield(projectId);
|
|
913
|
-
|
|
944
|
+
logger.debug("[ConsentService] Providers loaded successfully", {
|
|
914
945
|
projectId,
|
|
915
946
|
providers: this.providerRegistry.getProviderNames(),
|
|
916
947
|
configuredProvider: this.providerRegistry.getConfiguredProvider(),
|
|
917
948
|
});
|
|
918
949
|
}
|
|
919
950
|
catch (loadError) {
|
|
920
|
-
|
|
951
|
+
logger.warn("[ConsentService] Failed to load providers from AgentShield", {
|
|
921
952
|
projectId,
|
|
922
953
|
error: loadError instanceof Error
|
|
923
954
|
? loadError.message
|
|
@@ -929,7 +960,7 @@ export class ConsentService {
|
|
|
929
960
|
providerConfig = this.providerRegistry.getProvider(provider);
|
|
930
961
|
}
|
|
931
962
|
// Diagnostic logging to track race condition and OAuth mode selection
|
|
932
|
-
|
|
963
|
+
logger.debug("[ConsentService] OAuth mode decision:", {
|
|
933
964
|
provider: provider || "none",
|
|
934
965
|
hasProviderRegistry: !!this.providerRegistry,
|
|
935
966
|
hasProviderConfig: !!providerConfig,
|
|
@@ -968,7 +999,7 @@ export class ConsentService {
|
|
|
968
999
|
!providerConfig.proxyMode) {
|
|
969
1000
|
// Use providerConfig.clientId from AgentShield dashboard config
|
|
970
1001
|
const oauthClientId = providerConfig.clientId || projectId;
|
|
971
|
-
|
|
1002
|
+
logger.debug("[ConsentService] Using direct OAuth mode (PKCE)", {
|
|
972
1003
|
provider: provider || "unknown",
|
|
973
1004
|
authorizationUrl: providerConfig.authorizationUrl,
|
|
974
1005
|
supportsPKCE: providerConfig.supportsPKCE,
|
|
@@ -1119,7 +1150,7 @@ export class ConsentService {
|
|
|
1119
1150
|
try {
|
|
1120
1151
|
const existingUserDid = await this.env.DELEGATION_STORAGE.get(oauthKey, "text");
|
|
1121
1152
|
if (existingUserDid) {
|
|
1122
|
-
|
|
1153
|
+
logger.debug("[ConsentService] OAuth identity already mapped:", {
|
|
1123
1154
|
provider: oauthIdentity.provider,
|
|
1124
1155
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1125
1156
|
userDid: existingUserDid.substring(0, 20) + "...",
|
|
@@ -1128,7 +1159,7 @@ export class ConsentService {
|
|
|
1128
1159
|
}
|
|
1129
1160
|
}
|
|
1130
1161
|
catch (error) {
|
|
1131
|
-
|
|
1162
|
+
logger.warn("[ConsentService] Failed to check OAuth mapping:", error);
|
|
1132
1163
|
// Continue to check session
|
|
1133
1164
|
}
|
|
1134
1165
|
// Phase 5: Check for existing user DID in session
|
|
@@ -1147,14 +1178,14 @@ export class ConsentService {
|
|
|
1147
1178
|
await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
|
|
1148
1179
|
expirationTtl: 90 * 24 * 60 * 60, // 90 days
|
|
1149
1180
|
});
|
|
1150
|
-
|
|
1181
|
+
logger.debug("[ConsentService] OAuth identity linked to User DID:", {
|
|
1151
1182
|
provider: oauthIdentity.provider,
|
|
1152
1183
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1153
1184
|
userDid: userDid.substring(0, 20) + "...",
|
|
1154
1185
|
});
|
|
1155
1186
|
}
|
|
1156
1187
|
catch (error) {
|
|
1157
|
-
|
|
1188
|
+
logger.error("[ConsentService] Failed to store OAuth mapping:", error);
|
|
1158
1189
|
// Non-fatal - continue with User DID
|
|
1159
1190
|
}
|
|
1160
1191
|
return userDid;
|
|
@@ -1172,7 +1203,7 @@ export class ConsentService {
|
|
|
1172
1203
|
*/
|
|
1173
1204
|
async cacheExternalUserDid(oauthIdentity, userDid) {
|
|
1174
1205
|
if (!this.env.DELEGATION_STORAGE) {
|
|
1175
|
-
|
|
1206
|
+
logger.warn("[ConsentService] No storage available for caching external User DID");
|
|
1176
1207
|
return;
|
|
1177
1208
|
}
|
|
1178
1209
|
const oauthKey = STORAGE_KEYS.oauthIdentity(oauthIdentity.provider, oauthIdentity.subject);
|
|
@@ -1182,7 +1213,7 @@ export class ConsentService {
|
|
|
1182
1213
|
if (existingUserDid) {
|
|
1183
1214
|
// Mapping already exists - verify it matches
|
|
1184
1215
|
if (existingUserDid !== userDid) {
|
|
1185
|
-
|
|
1216
|
+
logger.warn("[ConsentService] OAuth identity already mapped to different User DID:", {
|
|
1186
1217
|
provider: oauthIdentity.provider,
|
|
1187
1218
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1188
1219
|
existingUserDid: existingUserDid.substring(0, 30) + "...",
|
|
@@ -1194,7 +1225,7 @@ export class ConsentService {
|
|
|
1194
1225
|
}
|
|
1195
1226
|
}
|
|
1196
1227
|
catch (error) {
|
|
1197
|
-
|
|
1228
|
+
logger.warn("[ConsentService] Failed to check existing OAuth mapping:", error);
|
|
1198
1229
|
// Continue to store new mapping
|
|
1199
1230
|
}
|
|
1200
1231
|
// Store OAuth identity → userDid mapping (persistent - 90 days)
|
|
@@ -1207,7 +1238,7 @@ export class ConsentService {
|
|
|
1207
1238
|
await this.env.DELEGATION_STORAGE.put(oauthIdentityKey, JSON.stringify(oauthIdentity), {
|
|
1208
1239
|
expirationTtl: 90 * 24 * 60 * 60, // 90 days
|
|
1209
1240
|
});
|
|
1210
|
-
|
|
1241
|
+
logger.debug("[ConsentService] Cached external User DID from AgentShield:", {
|
|
1211
1242
|
provider: oauthIdentity.provider,
|
|
1212
1243
|
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
1213
1244
|
userDid: userDid.substring(0, 30) + "...",
|
|
@@ -1215,7 +1246,7 @@ export class ConsentService {
|
|
|
1215
1246
|
});
|
|
1216
1247
|
}
|
|
1217
1248
|
catch (error) {
|
|
1218
|
-
|
|
1249
|
+
logger.debug("[ConsentService] Failed to cache external User DID:", error);
|
|
1219
1250
|
// Non-fatal - the persistent user_did will still be used
|
|
1220
1251
|
}
|
|
1221
1252
|
}
|
|
@@ -1301,6 +1332,21 @@ export class ConsentService {
|
|
|
1301
1332
|
// ✅ Extract agent_name from URL query param (passed from buildConsentUrl)
|
|
1302
1333
|
// Human-readable name for AgentShield dashboard display
|
|
1303
1334
|
const agentName = params.get("agent_name");
|
|
1335
|
+
// ✅ Extract credential auth params for 3-screen flow (Auth → Clickwrap → Success)
|
|
1336
|
+
// These are set when credential auth completes and redirects to clickwrap page
|
|
1337
|
+
// They ensure the delegation is created with 'password' type instead of 'none'
|
|
1338
|
+
const credentialProviderType = params.get("credential_provider_type");
|
|
1339
|
+
const credentialProvider = params.get("credential_provider");
|
|
1340
|
+
// ✅ Extract mode param for post-credential clickwrap detection
|
|
1341
|
+
// When credential auth completes, user is redirected to /consent?mode=consent-only&credential_provider_type=password
|
|
1342
|
+
const mode = params.get("mode");
|
|
1343
|
+
// ✅ LOOP FIX: Detect post-credential clickwrap state to prevent re-rendering credential form
|
|
1344
|
+
// Both conditions must be true to identify post-credential clickwrap:
|
|
1345
|
+
// 1. mode === "consent-only" (indicates we're past auth phase)
|
|
1346
|
+
// 2. credential_provider_type === "password" (confirms this was a credential auth flow)
|
|
1347
|
+
// IMPORTANT: credential_provider_type is a clickwrap-only signal. It is only set by the
|
|
1348
|
+
// credential-auth redirect to clickwrap; it is NOT used for pure consent-only or oauth flows.
|
|
1349
|
+
const isPostCredentialClickwrap = mode === "consent-only" && credentialProviderType === "password";
|
|
1304
1350
|
// Validate required parameters
|
|
1305
1351
|
if (!tool || !agentDid || !sessionId || !projectId) {
|
|
1306
1352
|
return new Response(JSON.stringify({
|
|
@@ -1372,13 +1418,13 @@ export class ConsentService {
|
|
|
1372
1418
|
}
|
|
1373
1419
|
}
|
|
1374
1420
|
catch (parseError) {
|
|
1375
|
-
|
|
1421
|
+
logger.warn("[ConsentService] Invalid OAuth cookie format:", parseError);
|
|
1376
1422
|
}
|
|
1377
1423
|
}
|
|
1378
1424
|
}
|
|
1379
1425
|
}
|
|
1380
1426
|
catch (error) {
|
|
1381
|
-
|
|
1427
|
+
logger.warn("[ConsentService] Failed to extract OAuth cookie:", error);
|
|
1382
1428
|
// Non-fatal - continue without OAuth identity
|
|
1383
1429
|
}
|
|
1384
1430
|
// Check if OAuth is required (after extracting OAuth identity)
|
|
@@ -1400,7 +1446,7 @@ export class ConsentService {
|
|
|
1400
1446
|
// This is the most explicit - passed from buildConsentUrl when tool has oauthProvider configured
|
|
1401
1447
|
if (urlProvider) {
|
|
1402
1448
|
provider = urlProvider;
|
|
1403
|
-
|
|
1449
|
+
logger.debug("[ConsentService] Using provider from URL query param:", {
|
|
1404
1450
|
provider,
|
|
1405
1451
|
projectId,
|
|
1406
1452
|
tool,
|
|
@@ -1417,7 +1463,7 @@ export class ConsentService {
|
|
|
1417
1463
|
if (protection) {
|
|
1418
1464
|
provider = await this.providerResolver.resolveProvider(protection, projectId);
|
|
1419
1465
|
if (provider) {
|
|
1420
|
-
|
|
1466
|
+
logger.debug("[ConsentService] Using provider from resolver:", {
|
|
1421
1467
|
provider,
|
|
1422
1468
|
projectId,
|
|
1423
1469
|
tool,
|
|
@@ -1428,13 +1474,32 @@ export class ConsentService {
|
|
|
1428
1474
|
}
|
|
1429
1475
|
catch (error) {
|
|
1430
1476
|
// Check if this is ConsentOnlyModeError - expected behavior, not an error
|
|
1431
|
-
|
|
1432
|
-
|
|
1477
|
+
// Use multiple detection methods to handle bundled code where class names may be mangled
|
|
1478
|
+
const isConsentOnlyError = error instanceof Error &&
|
|
1479
|
+
(error.name === "ConsentOnlyModeError" ||
|
|
1480
|
+
error.constructor?.name === "ConsentOnlyModeError");
|
|
1481
|
+
// Check for CredentialAuthModeError - expected behavior for password-based auth
|
|
1482
|
+
// Also check for 'provider' property as fallback detection
|
|
1483
|
+
const isCredentialAuthError = error instanceof Error &&
|
|
1484
|
+
(error.name === "CredentialAuthModeError" ||
|
|
1485
|
+
error.constructor?.name === "CredentialAuthModeError" ||
|
|
1486
|
+
// Fallback: check for provider property (unique to CredentialAuthModeError)
|
|
1487
|
+
("provider" in error &&
|
|
1488
|
+
typeof error.provider === "string"));
|
|
1489
|
+
if (isConsentOnlyError) {
|
|
1490
|
+
logger.debug("[ConsentService] Tool is consent-only mode, no provider needed for consent page", { projectId, tool });
|
|
1433
1491
|
// Continue without provider - consent-only mode just needs clickwrap
|
|
1434
1492
|
}
|
|
1493
|
+
else if (isCredentialAuthError) {
|
|
1494
|
+
// CredentialAuthModeError is expected behavior for password-based auth
|
|
1495
|
+
// Extract provider from the error for credential form rendering
|
|
1496
|
+
const credError = error;
|
|
1497
|
+
provider = credError.provider;
|
|
1498
|
+
logger.debug("[ConsentService] Tool uses credential auth, setting provider for credential form", { projectId, tool, provider });
|
|
1499
|
+
}
|
|
1435
1500
|
else {
|
|
1436
1501
|
// Non-fatal - continue without provider info
|
|
1437
|
-
|
|
1502
|
+
logger.warn("[ConsentService] Failed to resolve provider for consent page:", error);
|
|
1438
1503
|
}
|
|
1439
1504
|
}
|
|
1440
1505
|
}
|
|
@@ -1442,7 +1507,7 @@ export class ConsentService {
|
|
|
1442
1507
|
// This is a SEPARATE check (not else-if) to handle when providerResolver exists but returns undefined
|
|
1443
1508
|
if (!provider && toolOAuthProvider) {
|
|
1444
1509
|
provider = toolOAuthProvider;
|
|
1445
|
-
|
|
1510
|
+
logger.debug("[ConsentService] Using provider from tool protection (fallback):", {
|
|
1446
1511
|
provider,
|
|
1447
1512
|
projectId,
|
|
1448
1513
|
tool,
|
|
@@ -1450,10 +1515,29 @@ export class ConsentService {
|
|
|
1450
1515
|
}
|
|
1451
1516
|
let resolvedOAuthUrl;
|
|
1452
1517
|
let isOAuthRequired = false;
|
|
1453
|
-
//
|
|
1454
|
-
|
|
1518
|
+
// ✅ CONFIG-BASED CREDENTIAL DETECTION (replaces string matching)
|
|
1519
|
+
// Fetch credential provider config ONCE and reuse throughout the method
|
|
1520
|
+
// Only fetch when: provider exists AND we're not in post-credential clickwrap state
|
|
1521
|
+
// This avoids unnecessary network calls for pure consent-only tools and post-auth clickwrap
|
|
1522
|
+
let credentialProviderConfig = null;
|
|
1523
|
+
if (provider && projectId && !isPostCredentialClickwrap) {
|
|
1524
|
+
try {
|
|
1525
|
+
credentialProviderConfig = await this.getCredentialProviderConfig(projectId, provider);
|
|
1526
|
+
}
|
|
1527
|
+
catch (err) {
|
|
1528
|
+
// Non-fatal: log and continue - will fall back to OAuth flow
|
|
1529
|
+
logger.warn("[ConsentService] Failed to fetch credential provider config:", err);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
// Determine if this is a credentials provider based on config existence (not string matching)
|
|
1533
|
+
const isCredentialsProvider = !!credentialProviderConfig;
|
|
1455
1534
|
if (isCredentialsProvider && oauthRequired) {
|
|
1456
|
-
|
|
1535
|
+
logger.debug("[ConsentService] Credential provider detected via config - skipping OAuth flow, will render credential form", {
|
|
1536
|
+
provider,
|
|
1537
|
+
projectId,
|
|
1538
|
+
tool,
|
|
1539
|
+
providerType: credentialProviderConfig?.type,
|
|
1540
|
+
});
|
|
1457
1541
|
}
|
|
1458
1542
|
if (oauthRequired && !isCredentialsProvider) {
|
|
1459
1543
|
// OAuth is required - redirect to OAuth provider instead of showing consent page
|
|
@@ -1468,7 +1552,7 @@ export class ConsentService {
|
|
|
1468
1552
|
oauthSecurityService, agentName || undefined // Human-readable agent name for AgentShield dashboard
|
|
1469
1553
|
);
|
|
1470
1554
|
isOAuthRequired = true;
|
|
1471
|
-
|
|
1555
|
+
logger.debug("[ConsentService] 🔒 SECURITY EVENT: OAuth required, preparing consent page with redirect:", {
|
|
1472
1556
|
projectId,
|
|
1473
1557
|
agentDid: agentDid.substring(0, 20) + "...",
|
|
1474
1558
|
oauthUrl: resolvedOAuthUrl.substring(0, 100) + "...",
|
|
@@ -1492,7 +1576,7 @@ export class ConsentService {
|
|
|
1492
1576
|
})
|
|
1493
1577
|
.catch((err) => {
|
|
1494
1578
|
// Structured error logging
|
|
1495
|
-
|
|
1579
|
+
logger.debug("[ConsentService] Audit logging failed", {
|
|
1496
1580
|
eventType: "consent:page_viewed",
|
|
1497
1581
|
sessionId,
|
|
1498
1582
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -1515,10 +1599,12 @@ export class ConsentService {
|
|
|
1515
1599
|
provider, // Phase 2: Include provider if resolved
|
|
1516
1600
|
branding: consentConfig.branding,
|
|
1517
1601
|
// ✅ Ensure terms.required has a default value (contracts requires boolean, not boolean | undefined)
|
|
1518
|
-
terms: consentConfig.terms
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1602
|
+
terms: consentConfig.terms
|
|
1603
|
+
? {
|
|
1604
|
+
...consentConfig.terms,
|
|
1605
|
+
required: consentConfig.terms.required ?? true,
|
|
1606
|
+
}
|
|
1607
|
+
: undefined,
|
|
1522
1608
|
customFields: consentConfig.customFields,
|
|
1523
1609
|
autoClose: consentConfig.ui?.autoClose ?? false,
|
|
1524
1610
|
// ✅ FIX: Pass full UI config (title, description, button text, etc.)
|
|
@@ -1530,35 +1616,34 @@ export class ConsentService {
|
|
|
1530
1616
|
// Pass OAuth details for client-side handling
|
|
1531
1617
|
oauthRequired: isOAuthRequired,
|
|
1532
1618
|
oauthUrl: resolvedOAuthUrl,
|
|
1619
|
+
// ✅ Pass credential auth params for 3-screen flow (Auth → Clickwrap → Success)
|
|
1620
|
+
// These are set when user is redirected from credential auth page to clickwrap
|
|
1621
|
+
// They ensure the delegation is created with 'password' type instead of 'none'
|
|
1622
|
+
credentialProviderType: credentialProviderType || undefined,
|
|
1623
|
+
credentialProvider: credentialProvider || undefined,
|
|
1533
1624
|
};
|
|
1534
|
-
// CRED-003:
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
catch (credError) {
|
|
1559
|
-
// Non-fatal: If credential provider check fails, fall through to OAuth
|
|
1560
|
-
console.warn("[ConsentService] Credential provider check failed, falling back to OAuth:", credError);
|
|
1561
|
-
}
|
|
1625
|
+
// CRED-003: Render credential form if this is a credential provider
|
|
1626
|
+
// Guard conditions:
|
|
1627
|
+
// 1. credentialProviderConfig must exist (already fetched above)
|
|
1628
|
+
// 2. Must NOT be in post-credential clickwrap mode (would cause infinite loop)
|
|
1629
|
+
if (credentialProviderConfig && !isPostCredentialClickwrap) {
|
|
1630
|
+
logger.debug("[ConsentService] Credential provider detected, rendering credential page", {
|
|
1631
|
+
projectId,
|
|
1632
|
+
provider,
|
|
1633
|
+
providerType: credentialProviderConfig.type,
|
|
1634
|
+
displayName: credentialProviderConfig.displayName,
|
|
1635
|
+
});
|
|
1636
|
+
// Generate and store CSRF token for form security
|
|
1637
|
+
const csrfToken = await this.storeCredentialCsrfToken(sessionId);
|
|
1638
|
+
// Render credential login page instead of OAuth consent
|
|
1639
|
+
const html = this.renderer.renderCredentialPage(pageConfig, credentialProviderConfig, provider, csrfToken);
|
|
1640
|
+
return new Response(html, {
|
|
1641
|
+
status: 200,
|
|
1642
|
+
headers: {
|
|
1643
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
1644
|
+
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
1645
|
+
},
|
|
1646
|
+
});
|
|
1562
1647
|
}
|
|
1563
1648
|
// Standard OAuth flow: Render page with OAuth identity (if available)
|
|
1564
1649
|
const html = this.renderer.render(pageConfig, oauthIdentity);
|
|
@@ -1571,7 +1656,7 @@ export class ConsentService {
|
|
|
1571
1656
|
});
|
|
1572
1657
|
}
|
|
1573
1658
|
catch (error) {
|
|
1574
|
-
|
|
1659
|
+
logger.error("[ConsentService] Error rendering consent page:", error);
|
|
1575
1660
|
return new Response(JSON.stringify({
|
|
1576
1661
|
success: false,
|
|
1577
1662
|
error: "Failed to render consent page",
|
|
@@ -1809,7 +1894,7 @@ export class ConsentService {
|
|
|
1809
1894
|
}
|
|
1810
1895
|
catch (formDataError) {
|
|
1811
1896
|
// FormData parsing failed, try URL-encoded text parsing
|
|
1812
|
-
|
|
1897
|
+
logger.warn("[ConsentService] FormData parsing failed, trying URL-encoded text:", formDataError);
|
|
1813
1898
|
}
|
|
1814
1899
|
// Try URL-encoded text parsing as fallback
|
|
1815
1900
|
try {
|
|
@@ -1820,7 +1905,7 @@ export class ConsentService {
|
|
|
1820
1905
|
}
|
|
1821
1906
|
catch (urlEncodedError) {
|
|
1822
1907
|
// Both failed, fall through to JSON parsing
|
|
1823
|
-
|
|
1908
|
+
logger.warn("[ConsentService] URL-encoded parsing also failed:", urlEncodedError);
|
|
1824
1909
|
}
|
|
1825
1910
|
}
|
|
1826
1911
|
// Parse multipart FormData (multipart/form-data or when FormData object is passed with other Content-Type)
|
|
@@ -2148,7 +2233,7 @@ export class ConsentService {
|
|
|
2148
2233
|
}
|
|
2149
2234
|
}
|
|
2150
2235
|
catch (formDataError) {
|
|
2151
|
-
|
|
2236
|
+
logger.warn("[ConsentService] FormData parsing failed:", formDataError);
|
|
2152
2237
|
// Fall through to JSON parsing
|
|
2153
2238
|
}
|
|
2154
2239
|
}
|
|
@@ -2665,58 +2750,115 @@ export class ConsentService {
|
|
|
2665
2750
|
* @returns JSON response
|
|
2666
2751
|
*/
|
|
2667
2752
|
async handleApproval(request) {
|
|
2668
|
-
|
|
2753
|
+
logger.debug("[ConsentService] Approval request received");
|
|
2669
2754
|
try {
|
|
2670
2755
|
// Parse and validate request body (supports both JSON and FormData)
|
|
2671
2756
|
const body = await this.parseRequestBody(request);
|
|
2672
|
-
|
|
2757
|
+
logger.debug("[ConsentService] Request body parsed:", {
|
|
2673
2758
|
hasBody: !!body,
|
|
2674
2759
|
bodyKeys: Object.keys(body || {}),
|
|
2675
2760
|
hasOAuthIdentity: !!body?.oauth_identity,
|
|
2676
2761
|
});
|
|
2677
2762
|
// Handle provider_type routing:
|
|
2678
|
-
// - 'credential': User authenticated via credential form (email/password)
|
|
2763
|
+
// - 'credential' or 'password': User authenticated via credential form (email/password)
|
|
2679
2764
|
// - 'oauth2': User authenticated via OAuth provider (handled below)
|
|
2680
2765
|
// - 'none': Consent-only mode (clickwrap) - user agrees without authentication
|
|
2681
2766
|
const bodyObj = body;
|
|
2682
2767
|
const providerType = bodyObj?.provider_type;
|
|
2683
2768
|
// CRED-003: Check for credential provider submission
|
|
2684
|
-
// Credential submissions include `provider_type: 'credential'` and are handled separately
|
|
2685
|
-
|
|
2686
|
-
|
|
2769
|
+
// Credential submissions include `provider_type: 'credential'` or 'password' and are handled separately
|
|
2770
|
+
// IMPORTANT: AgentShield tool protection uses 'password' for credential-based auth validation,
|
|
2771
|
+
// but we also accept 'credential' for backward compatibility
|
|
2772
|
+
if (providerType === CONSENT_PROVIDER_TYPES.CREDENTIAL ||
|
|
2773
|
+
providerType === CONSENT_PROVIDER_TYPES.PASSWORD) {
|
|
2774
|
+
logger.debug("[ConsentService] Credential submission detected", {
|
|
2775
|
+
provider_type: providerType,
|
|
2776
|
+
});
|
|
2687
2777
|
return this.handleCredentialApproval(bodyObj);
|
|
2688
2778
|
}
|
|
2689
2779
|
// ✅ Consent-only mode (provider_type: 'none')
|
|
2690
|
-
//
|
|
2691
|
-
//
|
|
2780
|
+
// This can be:
|
|
2781
|
+
// 1. Pure consent-only: User consents without authentication (clickwrap only)
|
|
2782
|
+
// 2. Post-credential clickwrap: User already authenticated via credentials, this is the clickwrap step
|
|
2783
|
+
// 3. Post-OAuth clickwrap: User already authenticated via OAuth, this is the clickwrap step
|
|
2692
2784
|
if (providerType === CONSENT_PROVIDER_TYPES.NONE) {
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
else {
|
|
2715
|
-
console.warn("[ConsentService] ⚠️ Failed to generate ephemeral userDid - delegation will use session-scoped storage only");
|
|
2785
|
+
const bodyObj = body;
|
|
2786
|
+
const sessionId = bodyObj.session_id;
|
|
2787
|
+
// Check for credential auth parameters passed from handleCredentialApproval
|
|
2788
|
+
// These indicate this is the clickwrap step AFTER credential authentication
|
|
2789
|
+
const credentialProviderType = bodyObj.credential_provider_type;
|
|
2790
|
+
const credentialProvider = bodyObj.credential_provider;
|
|
2791
|
+
if (credentialProviderType &&
|
|
2792
|
+
credentialProviderType === CONSENT_PROVIDER_TYPES.PASSWORD) {
|
|
2793
|
+
// This is the clickwrap step after credential auth
|
|
2794
|
+
// The session already has userDid from credential auth
|
|
2795
|
+
logger.debug("[ConsentService] Consent-only mode (post-credential clickwrap)");
|
|
2796
|
+
// Override provider_type to 'password' so delegation is created with correct authorization
|
|
2797
|
+
// This ensures authorization.type matches tool protection expectation
|
|
2798
|
+
bodyObj.provider_type = CONSENT_PROVIDER_TYPES.PASSWORD;
|
|
2799
|
+
if (credentialProvider) {
|
|
2800
|
+
bodyObj.customFields = {
|
|
2801
|
+
...(bodyObj.customFields || {}),
|
|
2802
|
+
provider: credentialProvider,
|
|
2803
|
+
provider_type: CONSENT_PROVIDER_TYPES.PASSWORD,
|
|
2804
|
+
};
|
|
2716
2805
|
}
|
|
2806
|
+
logger.debug("[ConsentService] ✅ Using credential auth provider_type for delegation:", {
|
|
2807
|
+
sessionId: sessionId?.substring(0, 20) + "...",
|
|
2808
|
+
providerType: CONSENT_PROVIDER_TYPES.PASSWORD,
|
|
2809
|
+
provider: credentialProvider,
|
|
2810
|
+
note: "Delegation will have authorization.type='password' to match tool protection",
|
|
2811
|
+
});
|
|
2812
|
+
// The userDid should already be in the session from credential auth
|
|
2813
|
+
// We don't need to generate ephemeral one
|
|
2717
2814
|
}
|
|
2718
2815
|
else {
|
|
2719
|
-
|
|
2816
|
+
// Pure consent-only mode - no prior authentication
|
|
2817
|
+
logger.debug("[ConsentService] Consent-only mode detected (pure clickwrap)");
|
|
2818
|
+
if (sessionId && this.env.DELEGATION_STORAGE) {
|
|
2819
|
+
// Check if session already has userDid (from credential auth or OAuth)
|
|
2820
|
+
let existingUserDid;
|
|
2821
|
+
try {
|
|
2822
|
+
const sessionKey = STORAGE_KEYS.session(sessionId);
|
|
2823
|
+
const existingSession = (await this.env.DELEGATION_STORAGE.get(sessionKey, "json"));
|
|
2824
|
+
existingUserDid = existingSession?.userDid;
|
|
2825
|
+
}
|
|
2826
|
+
catch (error) {
|
|
2827
|
+
logger.warn("[ConsentService] Failed to read session for userDid:", error);
|
|
2828
|
+
}
|
|
2829
|
+
if (existingUserDid) {
|
|
2830
|
+
// Session has userDid from prior auth step - use it
|
|
2831
|
+
logger.debug("[ConsentService] Session already has userDid, reusing:", {
|
|
2832
|
+
sessionId: sessionId.substring(0, 20) + "...",
|
|
2833
|
+
userDid: existingUserDid.substring(0, 30) + "...",
|
|
2834
|
+
});
|
|
2835
|
+
bodyObj.user_did = existingUserDid;
|
|
2836
|
+
}
|
|
2837
|
+
else {
|
|
2838
|
+
// Generate ephemeral userDid for pure consent-only
|
|
2839
|
+
// This enables PRIORITY 1 delegation key (user+agent scoped) for reliable lookup
|
|
2840
|
+
const ephemeralUserDid = await this.generateEphemeralUserDidForSession(sessionId);
|
|
2841
|
+
if (ephemeralUserDid) {
|
|
2842
|
+
// Store ephemeral userDid in session cache
|
|
2843
|
+
// This ensures storeDelegationToken can find it and use PRIORITY 1 key
|
|
2844
|
+
await this.updateSessionWithIdentity(sessionId, ephemeralUserDid, null // No OAuth identity for consent-only
|
|
2845
|
+
);
|
|
2846
|
+
// Also inject the user_did into the request body so createDelegation receives it
|
|
2847
|
+
bodyObj.user_did = ephemeralUserDid;
|
|
2848
|
+
logger.debug("[ConsentService] ✅ Ephemeral userDid stored in session for consent-only flow:", {
|
|
2849
|
+
sessionId: sessionId.substring(0, 20) + "...",
|
|
2850
|
+
userDid: ephemeralUserDid.substring(0, 30) + "...",
|
|
2851
|
+
note: "Enables reliable delegation lookup via PRIORITY 1 key",
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
else {
|
|
2855
|
+
logger.warn("[ConsentService] ⚠️ Failed to generate ephemeral userDid - delegation will use session-scoped storage only");
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
else {
|
|
2860
|
+
logger.warn("[ConsentService] ⚠️ No session_id for consent-only flow - delegation will use session-scoped storage only");
|
|
2861
|
+
}
|
|
2720
2862
|
}
|
|
2721
2863
|
}
|
|
2722
2864
|
// Convert null oauth_identity to undefined for proper schema validation
|
|
@@ -2733,7 +2875,7 @@ export class ConsentService {
|
|
|
2733
2875
|
}
|
|
2734
2876
|
const validation = validateConsentApprovalRequest(body);
|
|
2735
2877
|
if (!validation.success) {
|
|
2736
|
-
|
|
2878
|
+
logger.error("[ConsentService] Approval request validation failed:", {
|
|
2737
2879
|
errors: validation.error.errors,
|
|
2738
2880
|
receivedBody: body,
|
|
2739
2881
|
});
|
|
@@ -2748,7 +2890,7 @@ export class ConsentService {
|
|
|
2748
2890
|
});
|
|
2749
2891
|
}
|
|
2750
2892
|
const approvalRequest = validation.data;
|
|
2751
|
-
|
|
2893
|
+
logger.debug("[ConsentService] Approval request validated:", {
|
|
2752
2894
|
agentDid: approvalRequest.agent_did?.substring(0, 20) + "...",
|
|
2753
2895
|
sessionId: approvalRequest.session_id?.substring(0, 20) + "...",
|
|
2754
2896
|
scopes: approvalRequest.scopes,
|
|
@@ -2788,7 +2930,7 @@ export class ConsentService {
|
|
|
2788
2930
|
oauthProvider: approvalRequest.oauth_identity?.provider,
|
|
2789
2931
|
})
|
|
2790
2932
|
.catch((err) => {
|
|
2791
|
-
|
|
2933
|
+
logger.debug("[ConsentService] Failed to log credential required", {
|
|
2792
2934
|
eventType: "consent:credential_required",
|
|
2793
2935
|
sessionId: approvalRequest.session_id,
|
|
2794
2936
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -2798,10 +2940,10 @@ export class ConsentService {
|
|
|
2798
2940
|
// The credential_required event is just for audit tracking
|
|
2799
2941
|
}
|
|
2800
2942
|
// Create delegation via AgentShield API
|
|
2801
|
-
|
|
2943
|
+
logger.debug("[ConsentService] Creating delegation...");
|
|
2802
2944
|
const delegationResult = await this.createDelegation(approvalRequest);
|
|
2803
2945
|
if (!delegationResult.success) {
|
|
2804
|
-
|
|
2946
|
+
logger.error("[ConsentService] Delegation creation failed:", {
|
|
2805
2947
|
error: delegationResult.error,
|
|
2806
2948
|
error_code: delegationResult.error_code,
|
|
2807
2949
|
});
|
|
@@ -2814,7 +2956,7 @@ export class ConsentService {
|
|
|
2814
2956
|
headers: { "Content-Type": "application/json" },
|
|
2815
2957
|
});
|
|
2816
2958
|
}
|
|
2817
|
-
|
|
2959
|
+
logger.info("[ConsentService] ✅ Delegation created successfully:", {
|
|
2818
2960
|
delegationId: delegationResult.delegation_id?.substring(0, 20) + "...",
|
|
2819
2961
|
});
|
|
2820
2962
|
// Store delegation token in KV
|
|
@@ -2831,7 +2973,7 @@ export class ConsentService {
|
|
|
2831
2973
|
(await this.getUserDidForSession(approvalRequest.session_id, approvalRequest.oauth_identity || undefined)) ?? undefined; // Phase 5: Convert null to undefined
|
|
2832
2974
|
}
|
|
2833
2975
|
catch (error) {
|
|
2834
|
-
|
|
2976
|
+
logger.warn("[ConsentService] Failed to get userDid for audit logging:", error);
|
|
2835
2977
|
// Continue without userDid - audit events can still be logged
|
|
2836
2978
|
}
|
|
2837
2979
|
}
|
|
@@ -2862,7 +3004,7 @@ export class ConsentService {
|
|
|
2862
3004
|
});
|
|
2863
3005
|
}
|
|
2864
3006
|
catch (error) {
|
|
2865
|
-
|
|
3007
|
+
logger.debug("[ConsentService] Audit failed but continuing", {
|
|
2866
3008
|
sessionId: approvalRequest.session_id,
|
|
2867
3009
|
error: error instanceof Error ? error.message : String(error),
|
|
2868
3010
|
eventTypes: ["consent:approved", "consent:delegation_created"],
|
|
@@ -2881,7 +3023,7 @@ export class ConsentService {
|
|
|
2881
3023
|
});
|
|
2882
3024
|
}
|
|
2883
3025
|
catch (error) {
|
|
2884
|
-
|
|
3026
|
+
logger.error("[ConsentService] Error handling approval:", error);
|
|
2885
3027
|
return new Response(JSON.stringify({
|
|
2886
3028
|
success: false,
|
|
2887
3029
|
error: "Internal server error",
|
|
@@ -2902,7 +3044,7 @@ export class ConsentService {
|
|
|
2902
3044
|
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
2903
3045
|
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
2904
3046
|
if (!apiKey) {
|
|
2905
|
-
|
|
3047
|
+
logger.warn("[ConsentService] No API key configured, cannot create delegation");
|
|
2906
3048
|
return {
|
|
2907
3049
|
success: false,
|
|
2908
3050
|
error: "API key not configured",
|
|
@@ -2920,7 +3062,7 @@ export class ConsentService {
|
|
|
2920
3062
|
// Only fetch from storage if not already provided in request
|
|
2921
3063
|
if (!userDid && request.session_id) {
|
|
2922
3064
|
try {
|
|
2923
|
-
|
|
3065
|
+
logger.debug("[ConsentService] Getting User DID for session:", {
|
|
2924
3066
|
sessionId: request.session_id.substring(0, 20) + "...",
|
|
2925
3067
|
hasOAuthIdentity: !!request.oauth_identity,
|
|
2926
3068
|
oauthProvider: request.oauth_identity?.provider,
|
|
@@ -2932,27 +3074,27 @@ export class ConsentService {
|
|
|
2932
3074
|
userDid =
|
|
2933
3075
|
(await this.getUserDidForSession(request.session_id, request.oauth_identity || undefined // Explicitly handle null as undefined
|
|
2934
3076
|
)) ?? undefined;
|
|
2935
|
-
|
|
3077
|
+
logger.debug("[ConsentService] User DID retrieved from storage:", {
|
|
2936
3078
|
userDid: userDid?.substring(0, 20) + "...",
|
|
2937
3079
|
hasUserDid: !!userDid,
|
|
2938
3080
|
});
|
|
2939
3081
|
}
|
|
2940
3082
|
catch (error) {
|
|
2941
|
-
|
|
3083
|
+
logger.debug("[ConsentService] Failed to get/generate userDid:", error);
|
|
2942
3084
|
// Continue without userDid - delegation will work without user_identifier
|
|
2943
3085
|
// This is valid for non-OAuth scenarios, but we should log this as a warning
|
|
2944
|
-
|
|
3086
|
+
logger.warn("[ConsentService] Delegation will be created without user_identifier - this may affect user tracking");
|
|
2945
3087
|
}
|
|
2946
3088
|
}
|
|
2947
3089
|
else if (userDid) {
|
|
2948
3090
|
// userDid was provided in request (e.g., from credential auth flow)
|
|
2949
|
-
|
|
3091
|
+
logger.debug("[ConsentService] Using provided user_did from request:", {
|
|
2950
3092
|
userDid: userDid.substring(0, 20) + "...",
|
|
2951
3093
|
source: "request.user_did",
|
|
2952
3094
|
});
|
|
2953
3095
|
}
|
|
2954
3096
|
else {
|
|
2955
|
-
|
|
3097
|
+
logger.debug("[ConsentService] No session_id provided - skipping User DID generation");
|
|
2956
3098
|
}
|
|
2957
3099
|
const expiresInDays = 7; // Default to 7 days
|
|
2958
3100
|
// Phase 2 VC-Only: Issue Delegation VC if we have a session and userDid
|
|
@@ -2983,21 +3125,21 @@ export class ConsentService {
|
|
|
2983
3125
|
const vcResult = await this.issueDelegationVC(localDelegation, request.session_id, request.oauth_identity || undefined);
|
|
2984
3126
|
credentialJwt = vcResult ?? undefined;
|
|
2985
3127
|
if (credentialJwt) {
|
|
2986
|
-
|
|
3128
|
+
logger.debug("[ConsentService] VC issued for delegation:", {
|
|
2987
3129
|
delegationId,
|
|
2988
3130
|
jwtLength: credentialJwt.length,
|
|
2989
3131
|
});
|
|
2990
3132
|
}
|
|
2991
3133
|
}
|
|
2992
3134
|
catch (error) {
|
|
2993
|
-
|
|
3135
|
+
logger.warn("[ConsentService] Failed to issue VC, continuing without it:", error);
|
|
2994
3136
|
// Non-fatal - delegation will work without VC
|
|
2995
3137
|
}
|
|
2996
3138
|
}
|
|
2997
3139
|
// Build delegation request with error-based format detection
|
|
2998
3140
|
// Try full format first, fallback to simplified format on error
|
|
2999
3141
|
const delegationRequest = await this.buildDelegationRequest(request, userDid, expiresInDays, fieldName, credentialJwt);
|
|
3000
|
-
|
|
3142
|
+
logger.debug("[ConsentService] Creating delegation:", {
|
|
3001
3143
|
url: `${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`,
|
|
3002
3144
|
agentDid: request.agent_did.substring(0, 20) + "...",
|
|
3003
3145
|
scopes: request.scopes,
|
|
@@ -3028,7 +3170,7 @@ export class ConsentService {
|
|
|
3028
3170
|
// AgentShield now returns delegation_token for stateless verification
|
|
3029
3171
|
const responseDataObj = wrappedResponse.data;
|
|
3030
3172
|
const delegationToken = responseDataObj.delegation_token;
|
|
3031
|
-
|
|
3173
|
+
logger.info("[ConsentService] ✅ Delegation created successfully:", {
|
|
3032
3174
|
delegationId,
|
|
3033
3175
|
agentDid: wrappedResponse.data.agent_did.substring(0, 20) + "...",
|
|
3034
3176
|
scopes: wrappedResponse.data.scopes,
|
|
@@ -3051,7 +3193,7 @@ export class ConsentService {
|
|
|
3051
3193
|
// Extract delegation_token (JWT) if present in response
|
|
3052
3194
|
const unwrappedDataObj = unwrappedResponse;
|
|
3053
3195
|
const delegationToken = unwrappedDataObj.delegation_token;
|
|
3054
|
-
|
|
3196
|
+
logger.info("[ConsentService] ✅ Delegation created successfully:", {
|
|
3055
3197
|
delegationId,
|
|
3056
3198
|
agentDid: unwrappedResponse.agent_did.substring(0, 20) + "...",
|
|
3057
3199
|
scopes: unwrappedResponse.scopes,
|
|
@@ -3074,7 +3216,7 @@ export class ConsentService {
|
|
|
3074
3216
|
data?.id ||
|
|
3075
3217
|
delegationObj?.id;
|
|
3076
3218
|
if (!delegationId) {
|
|
3077
|
-
|
|
3219
|
+
logger.debug("[ConsentService] Invalid response format - missing delegation_id:", responseData);
|
|
3078
3220
|
return {
|
|
3079
3221
|
success: false,
|
|
3080
3222
|
error: "Invalid API response format - missing delegation_id",
|
|
@@ -3084,7 +3226,7 @@ export class ConsentService {
|
|
|
3084
3226
|
// Extract delegation_token from fallback parsing
|
|
3085
3227
|
const delegationToken = data?.delegation_token ||
|
|
3086
3228
|
delegationObj?.delegation_token;
|
|
3087
|
-
|
|
3229
|
+
logger.warn("[ConsentService] ⚠️ Response format not fully validated, using fallback parsing:", {
|
|
3088
3230
|
delegationId,
|
|
3089
3231
|
hasDelegationToken: !!delegationToken,
|
|
3090
3232
|
tokenLength: delegationToken?.length,
|
|
@@ -3097,7 +3239,7 @@ export class ConsentService {
|
|
|
3097
3239
|
};
|
|
3098
3240
|
}
|
|
3099
3241
|
catch (error) {
|
|
3100
|
-
|
|
3242
|
+
logger.error("[ConsentService] Error creating delegation:", error);
|
|
3101
3243
|
return {
|
|
3102
3244
|
success: false,
|
|
3103
3245
|
error: error instanceof Error ? error.message : "Unknown error",
|
|
@@ -3118,7 +3260,7 @@ export class ConsentService {
|
|
|
3118
3260
|
async storeDelegationToken(sessionId, agentDid, token, delegationId) {
|
|
3119
3261
|
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3120
3262
|
if (!delegationStorage) {
|
|
3121
|
-
|
|
3263
|
+
logger.warn("[ConsentService] No delegation storage configured, token not stored");
|
|
3122
3264
|
return;
|
|
3123
3265
|
}
|
|
3124
3266
|
try {
|
|
@@ -3137,7 +3279,7 @@ export class ConsentService {
|
|
|
3137
3279
|
await delegationStorage.put(userAgentKey, token, {
|
|
3138
3280
|
expirationTtl: ttl,
|
|
3139
3281
|
});
|
|
3140
|
-
|
|
3282
|
+
logger.info("[ConsentService] ✅ Token stored with user+agent DID:", {
|
|
3141
3283
|
key: userAgentKey,
|
|
3142
3284
|
ttl,
|
|
3143
3285
|
delegationId,
|
|
@@ -3162,7 +3304,7 @@ export class ConsentService {
|
|
|
3162
3304
|
await delegationStorage.put(sessionKey, JSON.stringify(sessionDataToStore), {
|
|
3163
3305
|
expirationTtl: ttl, // Use full TTL - this is the primary storage for session
|
|
3164
3306
|
});
|
|
3165
|
-
|
|
3307
|
+
logger.info("[ConsentService] ✅ Token stored for session:", {
|
|
3166
3308
|
key: sessionKey,
|
|
3167
3309
|
ttl,
|
|
3168
3310
|
sessionId: sessionId.substring(0, 20) + "...",
|
|
@@ -3170,7 +3312,7 @@ export class ConsentService {
|
|
|
3170
3312
|
preservedIdentityState: sessionData?.identityState || "none",
|
|
3171
3313
|
});
|
|
3172
3314
|
// Metrics: Log delegation key type for monitoring
|
|
3173
|
-
|
|
3315
|
+
logger.debug("[ConsentService] 📊 Delegation storage metrics:", {
|
|
3174
3316
|
delegationKeyType: userDid ? "user_agent_scoped" : "session_scoped",
|
|
3175
3317
|
hasUserDid: !!userDid,
|
|
3176
3318
|
userDidPrefix: userDid
|
|
@@ -3186,7 +3328,7 @@ export class ConsentService {
|
|
|
3186
3328
|
}
|
|
3187
3329
|
catch (error) {
|
|
3188
3330
|
// Storage errors are non-fatal - log but don't fail the request
|
|
3189
|
-
|
|
3331
|
+
logger.debug("[ConsentService] Storage error (non-fatal):", error);
|
|
3190
3332
|
}
|
|
3191
3333
|
}
|
|
3192
3334
|
/**
|
|
@@ -3208,7 +3350,7 @@ export class ConsentService {
|
|
|
3208
3350
|
}
|
|
3209
3351
|
catch (error) {
|
|
3210
3352
|
// Ignore errors, use default (autoClose = false)
|
|
3211
|
-
|
|
3353
|
+
logger.warn("[ConsentService] Failed to fetch config for autoClose:", error);
|
|
3212
3354
|
}
|
|
3213
3355
|
}
|
|
3214
3356
|
const html = this.renderer.renderSuccess({
|
|
@@ -3285,9 +3427,24 @@ export class ConsentService {
|
|
|
3285
3427
|
* @returns JSON response
|
|
3286
3428
|
*/
|
|
3287
3429
|
async handleCredentialApproval(body) {
|
|
3288
|
-
|
|
3430
|
+
logger.debug("[ConsentService] Processing credential approval");
|
|
3289
3431
|
// Extract standard fields
|
|
3290
3432
|
const { tool, scopes: rawScopes, agent_did, session_id, project_id, provider, provider_type, csrf_token, ...credentials } = body;
|
|
3433
|
+
// DEBUG: Log credentials to diagnose password encoding issues
|
|
3434
|
+
// SECURITY: Mask password, but show length and first/last char for debugging
|
|
3435
|
+
const pwd = credentials.password || "";
|
|
3436
|
+
logger.debug("[ConsentService] DEBUG: Credential values received", {
|
|
3437
|
+
hasUsername: !!credentials.username,
|
|
3438
|
+
usernameLength: (credentials.username || "").length,
|
|
3439
|
+
usernameValue: credentials.username, // Safe to log email
|
|
3440
|
+
hasPassword: !!credentials.password,
|
|
3441
|
+
passwordLength: pwd.length,
|
|
3442
|
+
passwordFirstChar: pwd.length > 0 ? pwd[0] : "",
|
|
3443
|
+
passwordLastChar: pwd.length > 0 ? pwd[pwd.length - 1] : "",
|
|
3444
|
+
// Check for special chars that might be escaped
|
|
3445
|
+
passwordContainsDollar: pwd.includes("$"),
|
|
3446
|
+
passwordContainsDoubleD: pwd.includes("$$"),
|
|
3447
|
+
});
|
|
3291
3448
|
// Parse scopes - handles double JSON encoding from form submission
|
|
3292
3449
|
// The form stores scopes as JSON string, then JS submits it as JSON again
|
|
3293
3450
|
const scopes = this.parseScopes(rawScopes);
|
|
@@ -3308,7 +3465,7 @@ export class ConsentService {
|
|
|
3308
3465
|
}
|
|
3309
3466
|
// Validate CSRF token
|
|
3310
3467
|
if (!csrf_token || typeof csrf_token !== "string") {
|
|
3311
|
-
|
|
3468
|
+
logger.warn("[ConsentService] Missing or invalid CSRF token");
|
|
3312
3469
|
return new Response(JSON.stringify({
|
|
3313
3470
|
success: false,
|
|
3314
3471
|
error: "Invalid or missing CSRF token",
|
|
@@ -3318,7 +3475,7 @@ export class ConsentService {
|
|
|
3318
3475
|
// Validate CSRF token against stored value
|
|
3319
3476
|
const csrfValid = await this.validateCredentialCsrfToken(csrf_token, session_id);
|
|
3320
3477
|
if (!csrfValid) {
|
|
3321
|
-
|
|
3478
|
+
logger.warn("[ConsentService] CSRF token validation failed", {
|
|
3322
3479
|
sessionId: session_id.substring(0, 20) + "...",
|
|
3323
3480
|
});
|
|
3324
3481
|
return new Response(JSON.stringify({
|
|
@@ -3331,7 +3488,7 @@ export class ConsentService {
|
|
|
3331
3488
|
// 1. Fetch credential provider config from AgentShield
|
|
3332
3489
|
const providerConfig = await this.getCredentialProviderConfig(project_id, provider);
|
|
3333
3490
|
if (!providerConfig) {
|
|
3334
|
-
|
|
3491
|
+
logger.debug("[ConsentService] Credential provider config not found", {
|
|
3335
3492
|
projectId: project_id,
|
|
3336
3493
|
provider,
|
|
3337
3494
|
});
|
|
@@ -3347,20 +3504,37 @@ export class ConsentService {
|
|
|
3347
3504
|
// See: https://developers.cloudflare.com/workers/observability/errors/#illegal-invocation-errors
|
|
3348
3505
|
const credentialHandler = createCredentialAuthHandler({
|
|
3349
3506
|
fetch: (...args) => globalThis.fetch(...args),
|
|
3350
|
-
logger: (msg, data) =>
|
|
3507
|
+
logger: (msg, data) => logger.debug(msg, data),
|
|
3351
3508
|
});
|
|
3352
3509
|
const authResult = await credentialHandler.authenticate(providerConfig, credentials);
|
|
3353
3510
|
if (!authResult.success) {
|
|
3354
|
-
|
|
3511
|
+
logger.error("[ConsentService] Credential authentication failed", {
|
|
3355
3512
|
error: authResult.error,
|
|
3356
3513
|
});
|
|
3514
|
+
// Log credential auth failure for audit trail
|
|
3515
|
+
const auditService = await this.getAuditService(project_id);
|
|
3516
|
+
if (auditService) {
|
|
3517
|
+
await auditService
|
|
3518
|
+
.logCredentialAuthFailed({
|
|
3519
|
+
sessionId: session_id,
|
|
3520
|
+
agentDid: agent_did,
|
|
3521
|
+
targetTools: [tool],
|
|
3522
|
+
scopes: scopes,
|
|
3523
|
+
projectId: project_id,
|
|
3524
|
+
provider: provider,
|
|
3525
|
+
errorMessage: authResult.error,
|
|
3526
|
+
})
|
|
3527
|
+
.catch((err) => {
|
|
3528
|
+
logger.warn("[ConsentService] Failed to log credential auth failure:", err);
|
|
3529
|
+
});
|
|
3530
|
+
}
|
|
3357
3531
|
return new Response(JSON.stringify({
|
|
3358
3532
|
success: false,
|
|
3359
3533
|
error: authResult.error || "Authentication failed",
|
|
3360
3534
|
error_code: "auth_failed",
|
|
3361
3535
|
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
3362
3536
|
}
|
|
3363
|
-
|
|
3537
|
+
logger.info("[ConsentService] ✅ Credential authentication successful");
|
|
3364
3538
|
// 3. Resolve identity via AgentShield
|
|
3365
3539
|
const identityResult = await this.resolveCredentialIdentity({
|
|
3366
3540
|
projectId: project_id,
|
|
@@ -3376,15 +3550,32 @@ export class ConsentService {
|
|
|
3376
3550
|
error_code: "identity_resolution_failed",
|
|
3377
3551
|
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
3378
3552
|
}
|
|
3379
|
-
|
|
3553
|
+
logger.info("[ConsentService] ✅ Identity resolved", {
|
|
3380
3554
|
userDid: identityResult.userDid.substring(0, 30) + "...",
|
|
3381
3555
|
});
|
|
3556
|
+
// Log credential auth success for audit trail (now that we have userDid)
|
|
3557
|
+
const auditService = await this.getAuditService(project_id);
|
|
3558
|
+
if (auditService) {
|
|
3559
|
+
await auditService
|
|
3560
|
+
.logCredentialAuthSuccess({
|
|
3561
|
+
sessionId: session_id,
|
|
3562
|
+
userDid: identityResult.userDid,
|
|
3563
|
+
agentDid: agent_did,
|
|
3564
|
+
targetTools: [tool],
|
|
3565
|
+
scopes: scopes,
|
|
3566
|
+
projectId: project_id,
|
|
3567
|
+
provider: provider,
|
|
3568
|
+
})
|
|
3569
|
+
.catch((err) => {
|
|
3570
|
+
logger.warn("[ConsentService] Failed to log credential auth success:", err);
|
|
3571
|
+
});
|
|
3572
|
+
}
|
|
3382
3573
|
// NEW: Update session cache with userDid for proper delegation scoping
|
|
3383
3574
|
// This ensures storeDelegationToken() can create user+agent scoped keys
|
|
3384
3575
|
// instead of falling back to legacy agent-only keys
|
|
3385
3576
|
await this.updateSessionWithIdentity(session_id, identityResult.userDid, null // No OAuth identity for credential auth
|
|
3386
3577
|
);
|
|
3387
|
-
|
|
3578
|
+
logger.info("[ConsentService] ✅ Session updated with identity");
|
|
3388
3579
|
// 4. Store token in IdpTokenStorage with usage metadata
|
|
3389
3580
|
// Include userId from authResult for ToolExecutionContext.userId
|
|
3390
3581
|
await this.storeCredentialToken({
|
|
@@ -3396,48 +3587,59 @@ export class ConsentService {
|
|
|
3396
3587
|
scopes,
|
|
3397
3588
|
userId: authResult.userId, // Pass extracted userId from credential provider response
|
|
3398
3589
|
});
|
|
3399
|
-
|
|
3400
|
-
//
|
|
3401
|
-
//
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3590
|
+
logger.info("[ConsentService] ✅ Token stored");
|
|
3591
|
+
// ================================================================================
|
|
3592
|
+
// 5. REDIRECT TO CLICKWRAP PAGE (3-screen flow)
|
|
3593
|
+
// ================================================================================
|
|
3594
|
+
// Flow: Credential Auth → Clickwrap (consent-only UI) → Success
|
|
3595
|
+
//
|
|
3596
|
+
// Instead of creating the delegation here, redirect to the clickwrap page
|
|
3597
|
+
// where the user can see their identity and approve the permission request.
|
|
3598
|
+
// The clickwrap approval will then create the delegation with the stored
|
|
3599
|
+
// credential auth info.
|
|
3600
|
+
//
|
|
3601
|
+
// This matches the OAuth flow pattern for consistency.
|
|
3602
|
+
// ================================================================================
|
|
3603
|
+
const serverUrl = this.env.MCP_SERVER_URL || "";
|
|
3604
|
+
// Build clickwrap URL with all necessary parameters
|
|
3605
|
+
const clickwrapUrl = new URL(`${serverUrl}/consent`);
|
|
3606
|
+
// Mode is consent-only since credential auth is complete
|
|
3607
|
+
clickwrapUrl.searchParams.set("mode", "consent-only");
|
|
3608
|
+
// CRITICAL: Pass provider_type so delegation is created with 'password' not 'none'
|
|
3609
|
+
// This ensures authorization.type matches tool protection expectation
|
|
3610
|
+
clickwrapUrl.searchParams.set("credential_provider_type", CONSENT_PROVIDER_TYPES.PASSWORD);
|
|
3611
|
+
clickwrapUrl.searchParams.set("credential_provider", provider);
|
|
3612
|
+
// Preserve important state from credential flow
|
|
3613
|
+
clickwrapUrl.searchParams.set("project_id", project_id);
|
|
3614
|
+
clickwrapUrl.searchParams.set("agent_did", agent_did);
|
|
3615
|
+
clickwrapUrl.searchParams.set("session_id", session_id);
|
|
3616
|
+
// Include tool and scopes for display
|
|
3617
|
+
clickwrapUrl.searchParams.set("tool", tool);
|
|
3618
|
+
if (Array.isArray(scopes) && scopes.length > 0) {
|
|
3619
|
+
clickwrapUrl.searchParams.set("scopes", scopes.join(","));
|
|
3423
3620
|
}
|
|
3424
|
-
|
|
3425
|
-
|
|
3621
|
+
// Include user info for display on clickwrap page
|
|
3622
|
+
if (authResult.userEmail) {
|
|
3623
|
+
clickwrapUrl.searchParams.set("credential_user_email", authResult.userEmail);
|
|
3624
|
+
}
|
|
3625
|
+
if (authResult.userId) {
|
|
3626
|
+
clickwrapUrl.searchParams.set("credential_user_id", authResult.userId);
|
|
3627
|
+
}
|
|
3628
|
+
logger.debug("[ConsentService] ✅ Credential auth complete, redirecting to clickwrap", {
|
|
3629
|
+
sessionId: session_id.substring(0, 20) + "...",
|
|
3630
|
+
userDid: identityResult.userDid.substring(0, 30) + "...",
|
|
3631
|
+
provider: provider,
|
|
3632
|
+
clickwrapUrl: clickwrapUrl.toString().substring(0, 100) + "...",
|
|
3426
3633
|
});
|
|
3427
|
-
//
|
|
3428
|
-
await this.storeDelegationToken(session_id, agent_did, delegationResult.delegation_token, delegationResult.delegation_id);
|
|
3429
|
-
// Return success with redirect URL
|
|
3430
|
-
const serverUrl = this.env.MCP_SERVER_URL || "";
|
|
3431
|
-
const redirectUrl = `${serverUrl}/consent/success?delegation_id=${encodeURIComponent(delegationResult.delegation_id)}&project_id=${encodeURIComponent(project_id)}`;
|
|
3634
|
+
// Return success with redirect to clickwrap page
|
|
3432
3635
|
return new Response(JSON.stringify({
|
|
3433
3636
|
success: true,
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
redirectUrl,
|
|
3637
|
+
redirectUrl: clickwrapUrl.toString(),
|
|
3638
|
+
// Note: delegation_id is NOT returned here - it will be created on clickwrap approval
|
|
3437
3639
|
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
3438
3640
|
}
|
|
3439
3641
|
catch (error) {
|
|
3440
|
-
|
|
3642
|
+
logger.error("[ConsentService] Credential approval error:", error);
|
|
3441
3643
|
return new Response(JSON.stringify({
|
|
3442
3644
|
success: false,
|
|
3443
3645
|
error: error instanceof Error ? error.message : "Internal error",
|
|
@@ -3469,7 +3671,7 @@ export class ConsentService {
|
|
|
3469
3671
|
}
|
|
3470
3672
|
}
|
|
3471
3673
|
catch {
|
|
3472
|
-
|
|
3674
|
+
logger.warn("[ConsentService] Failed to parse scopes JSON:", rawScopes);
|
|
3473
3675
|
}
|
|
3474
3676
|
}
|
|
3475
3677
|
// Single string scope - wrap in array
|
|
@@ -3491,7 +3693,7 @@ export class ConsentService {
|
|
|
3491
3693
|
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
3492
3694
|
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
3493
3695
|
if (!apiKey) {
|
|
3494
|
-
|
|
3696
|
+
logger.warn("[ConsentService] No AgentShield API key configured");
|
|
3495
3697
|
return null;
|
|
3496
3698
|
}
|
|
3497
3699
|
try {
|
|
@@ -3505,7 +3707,7 @@ export class ConsentService {
|
|
|
3505
3707
|
},
|
|
3506
3708
|
});
|
|
3507
3709
|
if (!response.ok) {
|
|
3508
|
-
|
|
3710
|
+
logger.warn("[ConsentService] Providers API returned error:", {
|
|
3509
3711
|
status: response.status,
|
|
3510
3712
|
statusText: response.statusText,
|
|
3511
3713
|
});
|
|
@@ -3515,7 +3717,7 @@ export class ConsentService {
|
|
|
3515
3717
|
// { success: true, data: { providers: { ... }, configuredProvider: "..." } }
|
|
3516
3718
|
const result = (await response.json());
|
|
3517
3719
|
if (!result.success || !result.data?.providers) {
|
|
3518
|
-
|
|
3720
|
+
logger.warn("[ConsentService] Invalid providers response:", {
|
|
3519
3721
|
hasSuccess: result.success,
|
|
3520
3722
|
hasData: !!result.data,
|
|
3521
3723
|
hasProviders: !!result.data?.providers,
|
|
@@ -3530,13 +3732,13 @@ export class ConsentService {
|
|
|
3530
3732
|
// AgentShield API may return snake_case fields from database
|
|
3531
3733
|
const provider = this.mapCredentialProviderFields(rawProvider, providerName);
|
|
3532
3734
|
if (!provider) {
|
|
3533
|
-
|
|
3735
|
+
logger.debug("[ConsentService] Credential provider validation failed:", {
|
|
3534
3736
|
projectId,
|
|
3535
3737
|
providerName,
|
|
3536
3738
|
});
|
|
3537
3739
|
return null;
|
|
3538
3740
|
}
|
|
3539
|
-
|
|
3741
|
+
logger.debug("[ConsentService] Found credential provider:", {
|
|
3540
3742
|
providerName,
|
|
3541
3743
|
hasAuthEndpoint: !!provider.authEndpoint,
|
|
3542
3744
|
hasResponseFields: !!provider.responseFields,
|
|
@@ -3547,7 +3749,7 @@ export class ConsentService {
|
|
|
3547
3749
|
return null;
|
|
3548
3750
|
}
|
|
3549
3751
|
catch (error) {
|
|
3550
|
-
|
|
3752
|
+
logger.error("[ConsentService] Failed to fetch provider config:", error);
|
|
3551
3753
|
return null;
|
|
3552
3754
|
}
|
|
3553
3755
|
}
|
|
@@ -3619,7 +3821,7 @@ export class ConsentService {
|
|
|
3619
3821
|
raw.auth_endpoint ??
|
|
3620
3822
|
"");
|
|
3621
3823
|
if (!authEndpoint) {
|
|
3622
|
-
|
|
3824
|
+
logger.debug("[ConsentService] Credential provider missing required authEndpoint", { providerName: providerName ?? "unknown" });
|
|
3623
3825
|
return null;
|
|
3624
3826
|
}
|
|
3625
3827
|
return {
|
|
@@ -3646,7 +3848,7 @@ export class ConsentService {
|
|
|
3646
3848
|
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
3647
3849
|
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
3648
3850
|
if (!apiKey) {
|
|
3649
|
-
|
|
3851
|
+
logger.warn("[ConsentService] No AgentShield API key for identity resolution");
|
|
3650
3852
|
return { success: false };
|
|
3651
3853
|
}
|
|
3652
3854
|
try {
|
|
@@ -3676,7 +3878,7 @@ export class ConsentService {
|
|
|
3676
3878
|
};
|
|
3677
3879
|
}
|
|
3678
3880
|
catch (error) {
|
|
3679
|
-
|
|
3881
|
+
logger.error("[ConsentService] Identity resolution failed:", error);
|
|
3680
3882
|
return { success: false };
|
|
3681
3883
|
}
|
|
3682
3884
|
}
|
|
@@ -3686,7 +3888,7 @@ export class ConsentService {
|
|
|
3686
3888
|
async storeCredentialToken(params) {
|
|
3687
3889
|
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3688
3890
|
if (!delegationStorage) {
|
|
3689
|
-
|
|
3891
|
+
logger.warn("[ConsentService] No DELEGATION_STORAGE for credential tokens");
|
|
3690
3892
|
return;
|
|
3691
3893
|
}
|
|
3692
3894
|
const oauthSecurityService = new OAuthSecurityService(delegationStorage, this.env.OAUTH_ENCRYPTION_SECRET);
|
|
@@ -3724,7 +3926,7 @@ export class ConsentService {
|
|
|
3724
3926
|
async validateCredentialCsrfToken(token, sessionId) {
|
|
3725
3927
|
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3726
3928
|
if (!delegationStorage) {
|
|
3727
|
-
|
|
3929
|
+
logger.warn("[ConsentService] No DELEGATION_STORAGE for CSRF validation, skipping");
|
|
3728
3930
|
// Return true to allow flow to continue (graceful degradation)
|
|
3729
3931
|
// This matches OAuth behavior when storage is unavailable
|
|
3730
3932
|
return true;
|
|
@@ -3733,7 +3935,7 @@ export class ConsentService {
|
|
|
3733
3935
|
const csrfKey = STORAGE_KEYS.credentialCsrf(sessionId);
|
|
3734
3936
|
const storedToken = await delegationStorage.get(csrfKey);
|
|
3735
3937
|
if (!storedToken) {
|
|
3736
|
-
|
|
3938
|
+
logger.warn("[ConsentService] No stored CSRF token found");
|
|
3737
3939
|
return false;
|
|
3738
3940
|
}
|
|
3739
3941
|
// Constant-time comparison to prevent timing attacks
|
|
@@ -3745,7 +3947,7 @@ export class ConsentService {
|
|
|
3745
3947
|
return isValid;
|
|
3746
3948
|
}
|
|
3747
3949
|
catch (error) {
|
|
3748
|
-
|
|
3950
|
+
logger.error("[ConsentService] CSRF validation error:", error);
|
|
3749
3951
|
return false;
|
|
3750
3952
|
}
|
|
3751
3953
|
}
|
|
@@ -3766,7 +3968,7 @@ export class ConsentService {
|
|
|
3766
3968
|
.replace(/\//g, "_")
|
|
3767
3969
|
.replace(/=/g, "");
|
|
3768
3970
|
if (!delegationStorage) {
|
|
3769
|
-
|
|
3971
|
+
logger.warn("[ConsentService] No DELEGATION_STORAGE for CSRF storage");
|
|
3770
3972
|
// Return token anyway - form will submit but validation will skip
|
|
3771
3973
|
return token;
|
|
3772
3974
|
}
|
|
@@ -3774,10 +3976,10 @@ export class ConsentService {
|
|
|
3774
3976
|
const csrfKey = STORAGE_KEYS.credentialCsrf(sessionId);
|
|
3775
3977
|
// 10 minute TTL matches OAuth state TTL
|
|
3776
3978
|
await delegationStorage.put(csrfKey, token, { expirationTtl: 600 });
|
|
3777
|
-
|
|
3979
|
+
logger.debug("[ConsentService] CSRF token stored for credential form");
|
|
3778
3980
|
}
|
|
3779
3981
|
catch (error) {
|
|
3780
|
-
|
|
3982
|
+
logger.error("[ConsentService] CSRF token storage error:", error);
|
|
3781
3983
|
}
|
|
3782
3984
|
return token;
|
|
3783
3985
|
}
|
|
@@ -3864,26 +4066,40 @@ export class ConsentService {
|
|
|
3864
4066
|
// user_identifier can be the same as user_did or a human-readable version
|
|
3865
4067
|
// If we have a human-readable identifier from OAuth, use it; otherwise use DID
|
|
3866
4068
|
simplifiedRequest.user_identifier = userDid;
|
|
3867
|
-
|
|
4069
|
+
logger.debug("[ConsentService] Including user_did in delegation request:", {
|
|
3868
4070
|
user_did: userDid.substring(0, 20) + "...",
|
|
3869
4071
|
});
|
|
3870
4072
|
}
|
|
3871
4073
|
else {
|
|
3872
|
-
|
|
4074
|
+
logger.debug("[ConsentService] No user_did (session is anonymous) - delegation will proceed without it");
|
|
3873
4075
|
}
|
|
3874
4076
|
// Phase 2 VC-Only: Include credential_jwt if available
|
|
3875
4077
|
if (credentialJwt) {
|
|
3876
4078
|
simplifiedRequest.credential_jwt = credentialJwt;
|
|
3877
|
-
|
|
4079
|
+
logger.debug("[ConsentService] Including credential_jwt in delegation request");
|
|
3878
4080
|
}
|
|
3879
4081
|
// AgentShield API only accepts "custom_fields", not "metadata"
|
|
3880
4082
|
// Always use "custom_fields" regardless of Day0 config
|
|
3881
4083
|
// Include provider_type and provider for dashboard authorization display
|
|
4084
|
+
//
|
|
4085
|
+
// CRITICAL FIX: request.provider_type is at the TOP LEVEL of ConsentApprovalRequest,
|
|
4086
|
+
// NOT inside customFields. We need to read from both locations:
|
|
4087
|
+
// - request.provider_type (set by handleCredentialApproval, handleApproval, etc.)
|
|
4088
|
+
// - customFieldsFromRequest.provider (set in customFields for backward compat)
|
|
3882
4089
|
const customFieldsFromRequest = request.customFields || {};
|
|
3883
|
-
|
|
4090
|
+
// FIX: Read provider_type from request root (where handleCredentialApproval sets it)
|
|
4091
|
+
// Fall back to customFields for backward compatibility
|
|
4092
|
+
const providerType = request.provider_type || customFieldsFromRequest.provider_type;
|
|
4093
|
+
const provider = customFieldsFromRequest.provider;
|
|
4094
|
+
const hasProviderInfo = providerType || provider;
|
|
3884
4095
|
if (userDid || hasProviderInfo) {
|
|
3885
4096
|
// Include issuer_did and subject_did in custom_fields when we have userDid
|
|
3886
4097
|
// Also include provider_type and provider for AgentShield dashboard
|
|
4098
|
+
// CRITICAL: AgentShield derives authorization from metadata.provider_type:
|
|
4099
|
+
// - provider_type === 'password' → authorization.type = 'password' (credential auth)
|
|
4100
|
+
// - provider_type === 'none' → authorization.type = 'none' (consent-only)
|
|
4101
|
+
// - oauth_provider present → authorization.type = 'oauth'
|
|
4102
|
+
// NOTE: Tool protection expects 'password' for credential flows, NOT 'credential'
|
|
3887
4103
|
simplifiedRequest.custom_fields = {
|
|
3888
4104
|
...(userDid
|
|
3889
4105
|
? {
|
|
@@ -3892,13 +4108,18 @@ export class ConsentService {
|
|
|
3892
4108
|
}
|
|
3893
4109
|
: {}),
|
|
3894
4110
|
format_version: "simplified_v1",
|
|
3895
|
-
//
|
|
4111
|
+
// Include provider_type at the custom_fields level (where AgentShield reads it)
|
|
4112
|
+
...(providerType ? { provider_type: providerType } : {}),
|
|
4113
|
+
// Merge with any existing custom_fields from request
|
|
3896
4114
|
...customFieldsFromRequest,
|
|
3897
4115
|
};
|
|
3898
4116
|
if (hasProviderInfo) {
|
|
3899
|
-
|
|
3900
|
-
provider_type:
|
|
3901
|
-
provider:
|
|
4117
|
+
logger.debug("[ConsentService] Including provider info in custom_fields:", {
|
|
4118
|
+
provider_type: providerType,
|
|
4119
|
+
provider: provider,
|
|
4120
|
+
source: request.provider_type
|
|
4121
|
+
? "request.provider_type"
|
|
4122
|
+
: "customFields",
|
|
3902
4123
|
});
|
|
3903
4124
|
}
|
|
3904
4125
|
}
|
|
@@ -3929,7 +4150,7 @@ export class ConsentService {
|
|
|
3929
4150
|
return fullResponse;
|
|
3930
4151
|
}
|
|
3931
4152
|
// Full format failed with validation error, try simplified
|
|
3932
|
-
|
|
4153
|
+
logger.debug("[ConsentService] Full format failed, trying simplified format...");
|
|
3933
4154
|
const simplifiedResponse = await this.makeAPICall(agentShieldUrl, apiKey, request.simplifiedFormat);
|
|
3934
4155
|
if (simplifiedResponse.success) {
|
|
3935
4156
|
await this.cacheFormatPreference("simplified");
|
|
@@ -3951,11 +4172,11 @@ export class ConsentService {
|
|
|
3951
4172
|
// This provides defense-in-depth protection
|
|
3952
4173
|
const sanitizedBody = { ...requestBody };
|
|
3953
4174
|
if ("session_id" in sanitizedBody) {
|
|
3954
|
-
|
|
4175
|
+
logger.warn("[ConsentService] ⚠️ session_id detected in request body - removing (not in schema)");
|
|
3955
4176
|
delete sanitizedBody.session_id;
|
|
3956
4177
|
}
|
|
3957
4178
|
if ("project_id" in sanitizedBody) {
|
|
3958
|
-
|
|
4179
|
+
logger.warn("[ConsentService] ⚠️ project_id detected in request body - removing (not in schema)");
|
|
3959
4180
|
delete sanitizedBody.project_id;
|
|
3960
4181
|
}
|
|
3961
4182
|
const response = await fetch(`${agentShieldUrl}${AGENTSHIELD_ENDPOINTS.DELEGATIONS_CREATE}`, {
|
|
@@ -4009,7 +4230,7 @@ export class ConsentService {
|
|
|
4009
4230
|
};
|
|
4010
4231
|
}
|
|
4011
4232
|
catch (error) {
|
|
4012
|
-
|
|
4233
|
+
logger.error("[ConsentService] API call failed:", error);
|
|
4013
4234
|
return {
|
|
4014
4235
|
success: false,
|
|
4015
4236
|
error: error instanceof Error ? error.message : "Network request failed",
|
|
@@ -4031,10 +4252,10 @@ export class ConsentService {
|
|
|
4031
4252
|
}), {
|
|
4032
4253
|
expirationTtl: 3600, // 1 hour
|
|
4033
4254
|
});
|
|
4034
|
-
|
|
4255
|
+
logger.debug(`[ConsentService] Cached format preference: ${format}`);
|
|
4035
4256
|
}
|
|
4036
4257
|
catch (error) {
|
|
4037
|
-
|
|
4258
|
+
logger.warn("[ConsentService] Failed to cache format preference:", error);
|
|
4038
4259
|
}
|
|
4039
4260
|
}
|
|
4040
4261
|
}
|