@kya-os/mcp-i-cloudflare 1.6.24 → 1.6.26
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/constants/storage-keys.d.ts +12 -0
- package/dist/constants/storage-keys.d.ts.map +1 -1
- package/dist/constants/storage-keys.js +12 -0
- package/dist/constants/storage-keys.js.map +1 -1
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -1
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +64 -0
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/services/admin.service.d.ts.map +1 -1
- package/dist/services/admin.service.js +31 -0
- package/dist/services/admin.service.js.map +1 -1
- package/dist/services/consent-page-renderer.d.ts +55 -0
- package/dist/services/consent-page-renderer.d.ts.map +1 -1
- package/dist/services/consent-page-renderer.js +284 -0
- package/dist/services/consent-page-renderer.js.map +1 -1
- package/dist/services/consent.service.d.ts +85 -15
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +530 -37
- package/dist/services/consent.service.js.map +1 -1
- package/dist/services/credential-auth.handler.d.ts +72 -0
- package/dist/services/credential-auth.handler.d.ts.map +1 -0
- package/dist/services/credential-auth.handler.js +203 -0
- package/dist/services/credential-auth.handler.js.map +1 -0
- package/dist/services/idp-token-storage.d.ts +54 -6
- package/dist/services/idp-token-storage.d.ts.map +1 -1
- package/dist/services/idp-token-storage.js +21 -6
- package/dist/services/idp-token-storage.js.map +1 -1
- package/package.json +3 -3
- package/dist/__tests__/e2e/test-config.d.ts +0 -37
- package/dist/__tests__/e2e/test-config.d.ts.map +0 -1
- package/dist/__tests__/e2e/test-config.js +0 -62
- package/dist/__tests__/e2e/test-config.js.map +0 -1
- package/dist/utils/client-info.d.ts +0 -69
- package/dist/utils/client-info.d.ts.map +0 -1
- package/dist/utils/client-info.js +0 -178
- package/dist/utils/client-info.js.map +0 -1
- package/dist/utils/error-formatter.d.ts +0 -103
- package/dist/utils/error-formatter.d.ts.map +0 -1
- package/dist/utils/error-formatter.js +0 -245
- package/dist/utils/error-formatter.js.map +0 -1
- package/dist/utils/initialize-context.d.ts +0 -91
- package/dist/utils/initialize-context.d.ts.map +0 -1
- package/dist/utils/initialize-context.js +0 -169
- package/dist/utils/initialize-context.js.map +0 -1
- package/dist/utils/oauth-identity.d.ts +0 -58
- package/dist/utils/oauth-identity.d.ts.map +0 -1
- package/dist/utils/oauth-identity.js +0 -215
- package/dist/utils/oauth-identity.js.map +0 -1
|
@@ -8,17 +8,20 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { ConsentConfigService } from "./consent-config.service";
|
|
10
10
|
import { ConsentPageRenderer } from "./consent-page-renderer";
|
|
11
|
-
import { DEFAULT_AGENTSHIELD_URL, DEFAULT_SESSION_CACHE_TTL, } from "../constants";
|
|
11
|
+
import { DEFAULT_AGENTSHIELD_URL, DEFAULT_SESSION_CACHE_TTL, KEY_PAIR_TTL_SECONDS, } from "../constants";
|
|
12
12
|
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, createDelegationVerifier, createDidKeyResolver, } from "@kya-os/mcp-i-core";
|
|
16
|
+
import { fetchRemoteConfig, createUnsignedVCJWT, completeVCJWT, parseVCJWT, base64urlEncodeFromBytes, base64urlDecodeToBytes, bytesToBase64, generateDidKeyFromBase64, createDelegationVerifier, createDidKeyResolver, } 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";
|
|
20
20
|
import { CloudflareProofGenerator } from "../proof-generator";
|
|
21
21
|
import { ProofService } from "./proof.service";
|
|
22
|
+
import { createCredentialAuthHandler } from "./credential-auth.handler";
|
|
23
|
+
import { IdpTokenStorage } from "./idp-token-storage";
|
|
24
|
+
import { OAuthSecurityService } from "./oauth-security.service";
|
|
22
25
|
export class ConsentService {
|
|
23
26
|
configService;
|
|
24
27
|
renderer;
|
|
@@ -179,16 +182,6 @@ export class ConsentService {
|
|
|
179
182
|
return undefined;
|
|
180
183
|
}
|
|
181
184
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Get or generate User DID for a session
|
|
184
|
-
*
|
|
185
|
-
* Phase 4 PR #1: Generates ephemeral DIDs for sessions
|
|
186
|
-
* Phase 4 PR #3: Checks OAuth mappings for persistent DIDs
|
|
187
|
-
*
|
|
188
|
-
* @param sessionId - Session ID
|
|
189
|
-
* @param oauthIdentity - Optional OAuth provider identity
|
|
190
|
-
* @returns User DID (did:key format)
|
|
191
|
-
*/
|
|
192
185
|
/**
|
|
193
186
|
* Get user DID for a session
|
|
194
187
|
*
|
|
@@ -316,16 +309,123 @@ export class ConsentService {
|
|
|
316
309
|
* @returns UserKeyPair or null if not available
|
|
317
310
|
*/
|
|
318
311
|
async getKeyPairForSession(sessionId, oauthIdentity) {
|
|
319
|
-
//
|
|
312
|
+
// Priority 1: Check direct KV storage by OAuth identity
|
|
313
|
+
// This is the primary storage location used by getOrCreateKeyPairForUser()
|
|
314
|
+
if (this.env.DELEGATION_STORAGE &&
|
|
315
|
+
oauthIdentity?.provider &&
|
|
316
|
+
oauthIdentity?.subject) {
|
|
317
|
+
try {
|
|
318
|
+
const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
|
|
319
|
+
const stored = (await this.env.DELEGATION_STORAGE.get(key, "json"));
|
|
320
|
+
if (stored) {
|
|
321
|
+
console.log("[ConsentService] Found key pair in KV storage");
|
|
322
|
+
return stored;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.warn("[ConsentService] KV key pair lookup failed:", error);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Priority 2: Try UserDidManager (legacy path, may not be initialized)
|
|
320
330
|
if (!this.userDidManager) {
|
|
321
331
|
// Initialize with storage if available
|
|
322
332
|
await this.getUserDidForSession(sessionId, oauthIdentity);
|
|
323
333
|
}
|
|
324
|
-
if (
|
|
325
|
-
|
|
334
|
+
if (this.userDidManager) {
|
|
335
|
+
const keyPair = await this.userDidManager.getKeyPairForSession(sessionId, oauthIdentity);
|
|
336
|
+
if (keyPair) {
|
|
337
|
+
return keyPair;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// No key pair found
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Get or create key pair for a user (OAuth flow VC signing)
|
|
345
|
+
*
|
|
346
|
+
* Unlike getKeyPairForSession which only retrieves, this method
|
|
347
|
+
* GENERATES a new key pair if one doesn't exist.
|
|
348
|
+
*
|
|
349
|
+
* NOTE: The generated key pair has its own did:key which may differ
|
|
350
|
+
* from AgentShield's persistentUserDid. Both are tied to the same
|
|
351
|
+
* OAuth identity. See docs/proposals/VC/AGENTSHIELD_SIGNING_ENDPOINT.md
|
|
352
|
+
* for the future cleaner architecture.
|
|
353
|
+
*
|
|
354
|
+
* @param sessionId - Session ID for caching
|
|
355
|
+
* @param oauthIdentity - OAuth identity for persistent storage
|
|
356
|
+
* @returns UserKeyPair or null if generation fails
|
|
357
|
+
*/
|
|
358
|
+
async getOrCreateKeyPairForUser(sessionId, oauthIdentity) {
|
|
359
|
+
// 1. Try existing lookup first
|
|
360
|
+
const existingKeyPair = await this.getKeyPairForSession(sessionId, oauthIdentity);
|
|
361
|
+
if (existingKeyPair) {
|
|
362
|
+
console.log("[ConsentService] Found existing key pair for OAuth identity");
|
|
363
|
+
return existingKeyPair;
|
|
364
|
+
}
|
|
365
|
+
// 2. Check KV storage directly by OAuth identity
|
|
366
|
+
if (this.env.DELEGATION_STORAGE &&
|
|
367
|
+
oauthIdentity.provider &&
|
|
368
|
+
oauthIdentity.subject) {
|
|
369
|
+
try {
|
|
370
|
+
const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
|
|
371
|
+
const stored = (await this.env.DELEGATION_STORAGE.get(key, "json"));
|
|
372
|
+
if (stored) {
|
|
373
|
+
console.log("[ConsentService] Found key pair in KV storage");
|
|
374
|
+
return stored;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
console.warn("[ConsentService] KV key pair lookup failed:", error);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// 3. Generate new key pair
|
|
382
|
+
console.log("[ConsentService] Generating new key pair for OAuth identity");
|
|
383
|
+
try {
|
|
384
|
+
const crypto = new WebCryptoProvider();
|
|
385
|
+
const rawKeyPair = await crypto.generateKeyPair();
|
|
386
|
+
// Generate did:key from public key
|
|
387
|
+
const did = this.generateDidKeyFromPublicKey(rawKeyPair.publicKey);
|
|
388
|
+
const keyId = `${did}#keys-1`;
|
|
389
|
+
const keyPair = {
|
|
390
|
+
did,
|
|
391
|
+
publicKey: rawKeyPair.publicKey,
|
|
392
|
+
privateKey: rawKeyPair.privateKey,
|
|
393
|
+
keyId,
|
|
394
|
+
};
|
|
395
|
+
// 4. Persist to KV by OAuth identity (1 year TTL)
|
|
396
|
+
// Note: Persistence failure is non-fatal - key pair can still be used
|
|
397
|
+
if (this.env.DELEGATION_STORAGE &&
|
|
398
|
+
oauthIdentity.provider &&
|
|
399
|
+
oauthIdentity.subject) {
|
|
400
|
+
try {
|
|
401
|
+
const key = STORAGE_KEYS.userKeyPair(oauthIdentity.provider, oauthIdentity.subject);
|
|
402
|
+
await this.env.DELEGATION_STORAGE.put(key, JSON.stringify(keyPair), {
|
|
403
|
+
expirationTtl: KEY_PAIR_TTL_SECONDS,
|
|
404
|
+
});
|
|
405
|
+
console.log("[ConsentService] Key pair persisted to KV storage");
|
|
406
|
+
}
|
|
407
|
+
catch (persistError) {
|
|
408
|
+
console.warn("[ConsentService] KV key pair persistence failed (non-fatal):", persistError);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
console.log("[ConsentService] Key pair generated:", {
|
|
412
|
+
did: did.substring(0, 30) + "...",
|
|
413
|
+
keyId,
|
|
414
|
+
});
|
|
415
|
+
return keyPair;
|
|
416
|
+
}
|
|
417
|
+
catch (error) {
|
|
418
|
+
console.error("[ConsentService] Key pair generation failed:", error);
|
|
326
419
|
return null;
|
|
327
420
|
}
|
|
328
|
-
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Generate did:key from Ed25519 public key (base64)
|
|
424
|
+
* Delegates to shared utility in @kya-os/mcp-i-core
|
|
425
|
+
* Following spec: https://w3c-ccg.github.io/did-method-key/
|
|
426
|
+
*/
|
|
427
|
+
generateDidKeyFromPublicKey(publicKeyBase64) {
|
|
428
|
+
return generateDidKeyFromBase64(publicKeyBase64);
|
|
329
429
|
}
|
|
330
430
|
/**
|
|
331
431
|
* Issue a Delegation Verifiable Credential as JWT
|
|
@@ -338,23 +438,25 @@ export class ConsentService {
|
|
|
338
438
|
* @param delegation - The delegation record to wrap as a VC
|
|
339
439
|
* @param sessionId - Session ID for key pair retrieval
|
|
340
440
|
* @param oauthIdentity - Optional OAuth identity for persistent key lookup
|
|
441
|
+
* @param providedKeyPair - Optional key pair to use directly (avoids re-lookup, handles KV persistence failures)
|
|
341
442
|
* @returns JWT string (header.payload.signature) or null if key pair unavailable
|
|
342
443
|
*/
|
|
343
|
-
async issueDelegationVC(delegation, sessionId, oauthIdentity) {
|
|
344
|
-
//
|
|
345
|
-
|
|
444
|
+
async issueDelegationVC(delegation, sessionId, oauthIdentity, providedKeyPair) {
|
|
445
|
+
// Use provided key pair or look it up
|
|
446
|
+
// This allows callers to pass a key pair directly when KV persistence may have failed
|
|
447
|
+
const keyPair = providedKeyPair ??
|
|
448
|
+
(await this.getKeyPairForSession(sessionId, oauthIdentity));
|
|
346
449
|
if (!keyPair) {
|
|
347
450
|
console.warn("[ConsentService] Cannot issue VC: no key pair available for session");
|
|
348
451
|
return null;
|
|
349
452
|
}
|
|
350
453
|
try {
|
|
351
454
|
// Step 1: Create the unsigned VC
|
|
455
|
+
// Note: wrapDelegationAsVC handles notAfter→expirationDate conversion correctly
|
|
456
|
+
// (multiplies seconds by 1000 for Date constructor)
|
|
352
457
|
const unsignedVC = wrapDelegationAsVC(delegation, {
|
|
353
458
|
id: `urn:uuid:${delegation.id}`,
|
|
354
459
|
issuanceDate: new Date().toISOString(),
|
|
355
|
-
expirationDate: delegation.constraints.notAfter
|
|
356
|
-
? new Date(delegation.constraints.notAfter).toISOString()
|
|
357
|
-
: undefined,
|
|
358
460
|
});
|
|
359
461
|
// Override issuer with the user's DID (not the agent's DID)
|
|
360
462
|
const vcWithCorrectIssuer = {
|
|
@@ -679,7 +781,9 @@ export class ConsentService {
|
|
|
679
781
|
//
|
|
680
782
|
// Examples: GitHub Apps (PKCE, no proxy), Google (PKCE, no proxy)
|
|
681
783
|
// Counter-example: GitHub OAuth Apps (PKCE supported, but proxyMode=true for client_secret)
|
|
682
|
-
if (providerConfig &&
|
|
784
|
+
if (providerConfig &&
|
|
785
|
+
providerConfig.supportsPKCE &&
|
|
786
|
+
!providerConfig.proxyMode) {
|
|
683
787
|
// Use providerConfig.clientId from AgentShield dashboard config
|
|
684
788
|
const oauthClientId = providerConfig.clientId || projectId;
|
|
685
789
|
console.log("[ConsentService] Using direct OAuth mode (PKCE)", {
|
|
@@ -800,13 +904,13 @@ export class ConsentService {
|
|
|
800
904
|
* Link OAuth identity to User DID
|
|
801
905
|
*
|
|
802
906
|
* Maps OAuth provider identity (provider + subject) to a persistent User DID.
|
|
803
|
-
*
|
|
804
|
-
*
|
|
805
|
-
* Phase 4 PR #3: OAuth Identity Linking
|
|
907
|
+
* Phase 5: Requires existing userDid from AgentShield /identity/resolve endpoint.
|
|
908
|
+
* Throws error if session has no user DID (enforces OAuth → identity flow).
|
|
806
909
|
*
|
|
807
910
|
* @param oauthIdentity - OAuth provider identity
|
|
808
|
-
* @param sessionId - Current session ID
|
|
911
|
+
* @param sessionId - Current session ID
|
|
809
912
|
* @returns Persistent User DID
|
|
913
|
+
* @throws Error if no userDid exists for the session
|
|
810
914
|
*/
|
|
811
915
|
async linkOAuthToUserDid(oauthIdentity, sessionId) {
|
|
812
916
|
if (!this.env.DELEGATION_STORAGE) {
|
|
@@ -2258,6 +2362,13 @@ export class ConsentService {
|
|
|
2258
2362
|
bodyKeys: Object.keys(body || {}),
|
|
2259
2363
|
hasOAuthIdentity: !!body?.oauth_identity,
|
|
2260
2364
|
});
|
|
2365
|
+
// CRED-003: Check for credential provider submission
|
|
2366
|
+
// Credential submissions include `provider_type: 'credential'` and are handled separately
|
|
2367
|
+
const bodyObj = body;
|
|
2368
|
+
if (bodyObj?.provider_type === "credential") {
|
|
2369
|
+
console.log("[ConsentService] Credential submission detected");
|
|
2370
|
+
return this.handleCredentialApproval(bodyObj);
|
|
2371
|
+
}
|
|
2261
2372
|
// Convert null oauth_identity to undefined for proper schema validation
|
|
2262
2373
|
// Zod's .nullish() should handle null, but converting to undefined is more explicit
|
|
2263
2374
|
// and avoids potential edge cases with FormData parsing
|
|
@@ -2448,10 +2559,9 @@ export class ConsentService {
|
|
|
2448
2559
|
// Load Day0 configuration to determine field name and API capabilities
|
|
2449
2560
|
await loadDay0Config(this.env.DELEGATION_STORAGE);
|
|
2450
2561
|
const fieldName = await getDelegationFieldName(this.env.DELEGATION_STORAGE);
|
|
2451
|
-
// Get userDID from session
|
|
2452
|
-
//
|
|
2453
|
-
// CRITICAL: Must work with or without OAuth identity
|
|
2454
|
-
// CRITICAL: Always generate ephemeral DID if session_id is available, even without DELEGATION_STORAGE
|
|
2562
|
+
// Get userDID from session (Phase 5: returns null for anonymous sessions)
|
|
2563
|
+
// OAuth identity is resolved via AgentShield /identity/resolve endpoint
|
|
2564
|
+
// CRITICAL: Must work with or without OAuth identity - delegation is valid even without user_identifier
|
|
2455
2565
|
let userDid;
|
|
2456
2566
|
if (request.session_id) {
|
|
2457
2567
|
try {
|
|
@@ -2488,8 +2598,11 @@ export class ConsentService {
|
|
|
2488
2598
|
if (request.session_id && userDid) {
|
|
2489
2599
|
try {
|
|
2490
2600
|
// Create a local delegation record for VC issuance
|
|
2601
|
+
// NOTE: notAfter/notBefore use SECONDS (per contracts schema)
|
|
2602
|
+
// createdAt uses MILLISECONDS (per contracts schema)
|
|
2491
2603
|
const delegationId = crypto.randomUUID();
|
|
2492
|
-
const
|
|
2604
|
+
const nowSec = Math.floor(Date.now() / 1000);
|
|
2605
|
+
const notAfter = nowSec + expiresInDays * 24 * 60 * 60; // seconds
|
|
2493
2606
|
const scopes = request.scopes ?? [];
|
|
2494
2607
|
const localDelegation = {
|
|
2495
2608
|
id: delegationId,
|
|
@@ -2499,11 +2612,11 @@ export class ConsentService {
|
|
|
2499
2612
|
constraints: {
|
|
2500
2613
|
scopes,
|
|
2501
2614
|
notAfter,
|
|
2502
|
-
notBefore:
|
|
2615
|
+
notBefore: nowSec,
|
|
2503
2616
|
},
|
|
2504
|
-
signature: "", //
|
|
2617
|
+
signature: "vc-jwt-signed", // Placeholder - actual signature is in the VC-JWT
|
|
2505
2618
|
status: "active",
|
|
2506
|
-
createdAt: Date.now(),
|
|
2619
|
+
createdAt: Date.now(), // milliseconds per schema
|
|
2507
2620
|
};
|
|
2508
2621
|
const vcResult = await this.issueDelegationVC(localDelegation, request.session_id, request.oauth_identity || undefined);
|
|
2509
2622
|
credentialJwt = vcResult ?? undefined;
|
|
@@ -2764,6 +2877,386 @@ export class ConsentService {
|
|
|
2764
2877
|
simplifiedFormat: this.buildSimplifiedFormatRequest(request, userDid, expiresInDays, fieldName, credentialJwt),
|
|
2765
2878
|
};
|
|
2766
2879
|
}
|
|
2880
|
+
// ============================================================================
|
|
2881
|
+
// Credential Provider Handling (CRED-003)
|
|
2882
|
+
// ============================================================================
|
|
2883
|
+
/**
|
|
2884
|
+
* Handle credential-based authentication submission
|
|
2885
|
+
*
|
|
2886
|
+
* Authenticates user credentials against customer's endpoint, resolves identity
|
|
2887
|
+
* via AgentShield, stores token with usage metadata, and creates delegation.
|
|
2888
|
+
*
|
|
2889
|
+
* @param body - Raw request body with credential fields
|
|
2890
|
+
* @returns JSON response
|
|
2891
|
+
*/
|
|
2892
|
+
async handleCredentialApproval(body) {
|
|
2893
|
+
console.log("[ConsentService] Processing credential approval");
|
|
2894
|
+
// Extract standard fields
|
|
2895
|
+
const { tool, scopes: rawScopes, agent_did, session_id, project_id, provider, provider_type, csrf_token, ...credentials } = body;
|
|
2896
|
+
// Parse scopes - handles double JSON encoding from form submission
|
|
2897
|
+
// The form stores scopes as JSON string, then JS submits it as JSON again
|
|
2898
|
+
const scopes = this.parseScopes(rawScopes);
|
|
2899
|
+
// Basic validation - includes provider to avoid misleading errors
|
|
2900
|
+
if (!tool || !session_id || !project_id || !agent_did || !provider) {
|
|
2901
|
+
const missingFields = [
|
|
2902
|
+
!tool && "tool",
|
|
2903
|
+
!session_id && "session_id",
|
|
2904
|
+
!project_id && "project_id",
|
|
2905
|
+
!agent_did && "agent_did",
|
|
2906
|
+
!provider && "provider",
|
|
2907
|
+
].filter(Boolean);
|
|
2908
|
+
return new Response(JSON.stringify({
|
|
2909
|
+
success: false,
|
|
2910
|
+
error: `Missing required fields: ${missingFields.join(", ")}`,
|
|
2911
|
+
error_code: "validation_error",
|
|
2912
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
2913
|
+
}
|
|
2914
|
+
// Validate CSRF token
|
|
2915
|
+
if (!csrf_token || typeof csrf_token !== "string") {
|
|
2916
|
+
console.warn("[ConsentService] Missing or invalid CSRF token");
|
|
2917
|
+
return new Response(JSON.stringify({
|
|
2918
|
+
success: false,
|
|
2919
|
+
error: "Invalid or missing CSRF token",
|
|
2920
|
+
error_code: "csrf_error",
|
|
2921
|
+
}), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
2922
|
+
}
|
|
2923
|
+
// Validate CSRF token against stored value
|
|
2924
|
+
const csrfValid = await this.validateCredentialCsrfToken(csrf_token, session_id);
|
|
2925
|
+
if (!csrfValid) {
|
|
2926
|
+
console.warn("[ConsentService] CSRF token validation failed", {
|
|
2927
|
+
sessionId: session_id.substring(0, 20) + "...",
|
|
2928
|
+
});
|
|
2929
|
+
return new Response(JSON.stringify({
|
|
2930
|
+
success: false,
|
|
2931
|
+
error: "CSRF token validation failed",
|
|
2932
|
+
error_code: "csrf_error",
|
|
2933
|
+
}), { status: 403, headers: { "Content-Type": "application/json" } });
|
|
2934
|
+
}
|
|
2935
|
+
try {
|
|
2936
|
+
// 1. Fetch credential provider config from AgentShield
|
|
2937
|
+
const providerConfig = await this.getCredentialProviderConfig(project_id, provider);
|
|
2938
|
+
if (!providerConfig) {
|
|
2939
|
+
console.error("[ConsentService] Credential provider config not found", {
|
|
2940
|
+
projectId: project_id,
|
|
2941
|
+
provider,
|
|
2942
|
+
});
|
|
2943
|
+
return new Response(JSON.stringify({
|
|
2944
|
+
success: false,
|
|
2945
|
+
error: "Provider configuration not found",
|
|
2946
|
+
error_code: "provider_not_found",
|
|
2947
|
+
}), { status: 400, headers: { "Content-Type": "application/json" } });
|
|
2948
|
+
}
|
|
2949
|
+
// 2. Authenticate with customer's endpoint
|
|
2950
|
+
const credentialHandler = createCredentialAuthHandler({
|
|
2951
|
+
fetch: globalThis.fetch,
|
|
2952
|
+
logger: (msg, data) => console.log(msg, data),
|
|
2953
|
+
});
|
|
2954
|
+
const authResult = await credentialHandler.authenticate(providerConfig, credentials);
|
|
2955
|
+
if (!authResult.success) {
|
|
2956
|
+
console.error("[ConsentService] Credential authentication failed", {
|
|
2957
|
+
error: authResult.error,
|
|
2958
|
+
});
|
|
2959
|
+
return new Response(JSON.stringify({
|
|
2960
|
+
success: false,
|
|
2961
|
+
error: authResult.error || "Authentication failed",
|
|
2962
|
+
error_code: "auth_failed",
|
|
2963
|
+
}), { status: 401, headers: { "Content-Type": "application/json" } });
|
|
2964
|
+
}
|
|
2965
|
+
console.log("[ConsentService] ✅ Credential authentication successful");
|
|
2966
|
+
// 3. Resolve identity via AgentShield
|
|
2967
|
+
const identityResult = await this.resolveCredentialIdentity({
|
|
2968
|
+
projectId: project_id,
|
|
2969
|
+
provider: provider,
|
|
2970
|
+
userId: authResult.userId || "unknown",
|
|
2971
|
+
userEmail: authResult.userEmail,
|
|
2972
|
+
userDisplayName: authResult.userDisplayName,
|
|
2973
|
+
});
|
|
2974
|
+
if (!identityResult.success || !identityResult.userDid) {
|
|
2975
|
+
return new Response(JSON.stringify({
|
|
2976
|
+
success: false,
|
|
2977
|
+
error: "Identity resolution failed",
|
|
2978
|
+
error_code: "identity_resolution_failed",
|
|
2979
|
+
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
2980
|
+
}
|
|
2981
|
+
console.log("[ConsentService] ✅ Identity resolved", {
|
|
2982
|
+
userDid: identityResult.userDid.substring(0, 30) + "...",
|
|
2983
|
+
});
|
|
2984
|
+
// 4. Store token in IdpTokenStorage with usage metadata
|
|
2985
|
+
await this.storeCredentialToken({
|
|
2986
|
+
userDid: identityResult.userDid,
|
|
2987
|
+
provider: provider,
|
|
2988
|
+
sessionToken: authResult.sessionToken,
|
|
2989
|
+
providerConfig,
|
|
2990
|
+
expiresIn: authResult.expiresIn,
|
|
2991
|
+
scopes,
|
|
2992
|
+
});
|
|
2993
|
+
console.log("[ConsentService] ✅ Token stored");
|
|
2994
|
+
// 5. Create delegation using standard flow
|
|
2995
|
+
const approvalRequest = {
|
|
2996
|
+
tool: tool,
|
|
2997
|
+
scopes,
|
|
2998
|
+
agent_did: agent_did,
|
|
2999
|
+
session_id: session_id,
|
|
3000
|
+
project_id: project_id,
|
|
3001
|
+
termsAccepted: true,
|
|
3002
|
+
user_did: identityResult.userDid,
|
|
3003
|
+
};
|
|
3004
|
+
const delegationResult = await this.createDelegation(approvalRequest);
|
|
3005
|
+
if (!delegationResult.success) {
|
|
3006
|
+
return new Response(JSON.stringify({
|
|
3007
|
+
success: false,
|
|
3008
|
+
error: delegationResult.error || "Delegation creation failed",
|
|
3009
|
+
error_code: delegationResult.error_code || "delegation_failed",
|
|
3010
|
+
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
3011
|
+
}
|
|
3012
|
+
console.log("[ConsentService] ✅ Credential approval complete", {
|
|
3013
|
+
delegationId: delegationResult.delegation_id?.substring(0, 20) + "...",
|
|
3014
|
+
});
|
|
3015
|
+
// Store delegation token
|
|
3016
|
+
await this.storeDelegationToken(session_id, agent_did, delegationResult.delegation_token, delegationResult.delegation_id);
|
|
3017
|
+
// Return success with redirect URL
|
|
3018
|
+
const serverUrl = this.env.MCP_SERVER_URL || "";
|
|
3019
|
+
const redirectUrl = `${serverUrl}/consent/success?delegation_id=${encodeURIComponent(delegationResult.delegation_id)}&project_id=${encodeURIComponent(project_id)}`;
|
|
3020
|
+
return new Response(JSON.stringify({
|
|
3021
|
+
success: true,
|
|
3022
|
+
delegation_id: delegationResult.delegation_id,
|
|
3023
|
+
delegation_token: delegationResult.delegation_token,
|
|
3024
|
+
redirectUrl,
|
|
3025
|
+
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
3026
|
+
}
|
|
3027
|
+
catch (error) {
|
|
3028
|
+
console.error("[ConsentService] Credential approval error:", error);
|
|
3029
|
+
return new Response(JSON.stringify({
|
|
3030
|
+
success: false,
|
|
3031
|
+
error: error instanceof Error ? error.message : "Internal error",
|
|
3032
|
+
error_code: "internal_error",
|
|
3033
|
+
}), { status: 500, headers: { "Content-Type": "application/json" } });
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
/**
|
|
3037
|
+
* Parse scopes from form data, handling double JSON encoding
|
|
3038
|
+
*
|
|
3039
|
+
* The credential form stores scopes as a JSON string in a hidden input,
|
|
3040
|
+
* then the form submission script JSON-stringifies the entire form data.
|
|
3041
|
+
* This causes double-encoding: "[\"scope1\"]" instead of ["scope1"]
|
|
3042
|
+
*
|
|
3043
|
+
* @param rawScopes - Raw scopes value from form (may be string or array)
|
|
3044
|
+
* @returns Parsed string array of scopes
|
|
3045
|
+
*/
|
|
3046
|
+
parseScopes(rawScopes) {
|
|
3047
|
+
// Already an array - return as-is
|
|
3048
|
+
if (Array.isArray(rawScopes)) {
|
|
3049
|
+
return rawScopes;
|
|
3050
|
+
}
|
|
3051
|
+
// String that looks like JSON array - parse it
|
|
3052
|
+
if (typeof rawScopes === "string" && rawScopes.startsWith("[")) {
|
|
3053
|
+
try {
|
|
3054
|
+
const parsed = JSON.parse(rawScopes);
|
|
3055
|
+
if (Array.isArray(parsed)) {
|
|
3056
|
+
return parsed;
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
catch {
|
|
3060
|
+
console.warn("[ConsentService] Failed to parse scopes JSON:", rawScopes);
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
// Single string scope - wrap in array
|
|
3064
|
+
if (typeof rawScopes === "string" && rawScopes.length > 0) {
|
|
3065
|
+
return [rawScopes];
|
|
3066
|
+
}
|
|
3067
|
+
// Default to empty array
|
|
3068
|
+
return [];
|
|
3069
|
+
}
|
|
3070
|
+
/**
|
|
3071
|
+
* Fetch credential provider config from AgentShield
|
|
3072
|
+
*/
|
|
3073
|
+
async getCredentialProviderConfig(projectId, providerName) {
|
|
3074
|
+
// TODO: Implement once CRED-002 V1 API is complete
|
|
3075
|
+
// For now, try to fetch from the config endpoint
|
|
3076
|
+
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
3077
|
+
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
3078
|
+
if (!apiKey) {
|
|
3079
|
+
console.warn("[ConsentService] No AgentShield API key configured");
|
|
3080
|
+
return null;
|
|
3081
|
+
}
|
|
3082
|
+
try {
|
|
3083
|
+
const response = await fetch(`${agentShieldUrl}/api/v1/bouncer/projects/${projectId}/config`, {
|
|
3084
|
+
headers: {
|
|
3085
|
+
"X-API-Key": apiKey,
|
|
3086
|
+
"Content-Type": "application/json",
|
|
3087
|
+
},
|
|
3088
|
+
});
|
|
3089
|
+
if (!response.ok) {
|
|
3090
|
+
return null;
|
|
3091
|
+
}
|
|
3092
|
+
const data = (await response.json());
|
|
3093
|
+
const providers = data?.data?.providers;
|
|
3094
|
+
if (providers && providerName in providers) {
|
|
3095
|
+
const provider = providers[providerName];
|
|
3096
|
+
if (provider.type === "credential") {
|
|
3097
|
+
return provider;
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
return null;
|
|
3101
|
+
}
|
|
3102
|
+
catch (error) {
|
|
3103
|
+
console.error("[ConsentService] Failed to fetch provider config:", error);
|
|
3104
|
+
return null;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Resolve identity via AgentShield for credential-based auth
|
|
3109
|
+
*/
|
|
3110
|
+
async resolveCredentialIdentity(params) {
|
|
3111
|
+
const agentShieldUrl = this.env.AGENTSHIELD_API_URL || DEFAULT_AGENTSHIELD_URL;
|
|
3112
|
+
const apiKey = this.env.AGENTSHIELD_API_KEY;
|
|
3113
|
+
if (!apiKey) {
|
|
3114
|
+
console.warn("[ConsentService] No AgentShield API key for identity resolution");
|
|
3115
|
+
return { success: false };
|
|
3116
|
+
}
|
|
3117
|
+
try {
|
|
3118
|
+
const response = await fetch(`${agentShieldUrl}/api/v1/bouncer/identity/resolve`, {
|
|
3119
|
+
method: "POST",
|
|
3120
|
+
headers: {
|
|
3121
|
+
"X-API-Key": apiKey,
|
|
3122
|
+
"Content-Type": "application/json",
|
|
3123
|
+
},
|
|
3124
|
+
body: JSON.stringify({
|
|
3125
|
+
project_id: params.projectId,
|
|
3126
|
+
credential_result: {
|
|
3127
|
+
provider: params.provider,
|
|
3128
|
+
user_id: params.userId,
|
|
3129
|
+
email: params.userEmail,
|
|
3130
|
+
name: params.userDisplayName,
|
|
3131
|
+
},
|
|
3132
|
+
}),
|
|
3133
|
+
});
|
|
3134
|
+
if (!response.ok) {
|
|
3135
|
+
return { success: false };
|
|
3136
|
+
}
|
|
3137
|
+
const data = (await response.json());
|
|
3138
|
+
return {
|
|
3139
|
+
success: true,
|
|
3140
|
+
userDid: data?.data?.user_did,
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
catch (error) {
|
|
3144
|
+
console.error("[ConsentService] Identity resolution failed:", error);
|
|
3145
|
+
return { success: false };
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
/**
|
|
3149
|
+
* Store credential token in IdpTokenStorage with usage metadata
|
|
3150
|
+
*/
|
|
3151
|
+
async storeCredentialToken(params) {
|
|
3152
|
+
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3153
|
+
if (!delegationStorage) {
|
|
3154
|
+
console.warn("[ConsentService] No DELEGATION_STORAGE for credential tokens");
|
|
3155
|
+
return;
|
|
3156
|
+
}
|
|
3157
|
+
const oauthSecurityService = new OAuthSecurityService(delegationStorage, this.env.OAUTH_ENCRYPTION_SECRET);
|
|
3158
|
+
const idpTokenStorage = new IdpTokenStorage({
|
|
3159
|
+
storage: delegationStorage,
|
|
3160
|
+
oauthSecurityService,
|
|
3161
|
+
});
|
|
3162
|
+
// Default expiration: 7 days if not specified
|
|
3163
|
+
const expiresAt = params.expiresIn
|
|
3164
|
+
? Date.now() + params.expiresIn * 1000
|
|
3165
|
+
: Date.now() + 7 * 24 * 60 * 60 * 1000;
|
|
3166
|
+
await idpTokenStorage.storeToken(params.userDid, params.provider, params.scopes, {
|
|
3167
|
+
access_token: params.sessionToken,
|
|
3168
|
+
expires_at: expiresAt,
|
|
3169
|
+
token_type: "credential",
|
|
3170
|
+
// CRED-003: Include usage metadata
|
|
3171
|
+
tokenUsage: params.providerConfig.tokenUsage || "cookie",
|
|
3172
|
+
tokenHeader: params.providerConfig.tokenHeader,
|
|
3173
|
+
cookieFormat: params.providerConfig.cookieFormat,
|
|
3174
|
+
apiHeaders: params.providerConfig.apiHeaders,
|
|
3175
|
+
});
|
|
3176
|
+
}
|
|
3177
|
+
/**
|
|
3178
|
+
* Validate CSRF token for credential form submission
|
|
3179
|
+
*
|
|
3180
|
+
* Uses the same KV-based storage pattern as OAuth state validation.
|
|
3181
|
+
* Token is stored when rendering the credential form and validated on submission.
|
|
3182
|
+
*
|
|
3183
|
+
* @param token - CSRF token from form submission
|
|
3184
|
+
* @param sessionId - Session ID the token was issued for
|
|
3185
|
+
* @returns True if token is valid
|
|
3186
|
+
*/
|
|
3187
|
+
async validateCredentialCsrfToken(token, sessionId) {
|
|
3188
|
+
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3189
|
+
if (!delegationStorage) {
|
|
3190
|
+
console.warn("[ConsentService] No DELEGATION_STORAGE for CSRF validation, skipping");
|
|
3191
|
+
// Return true to allow flow to continue (graceful degradation)
|
|
3192
|
+
// This matches OAuth behavior when storage is unavailable
|
|
3193
|
+
return true;
|
|
3194
|
+
}
|
|
3195
|
+
try {
|
|
3196
|
+
const csrfKey = STORAGE_KEYS.credentialCsrf(sessionId);
|
|
3197
|
+
const storedToken = await delegationStorage.get(csrfKey);
|
|
3198
|
+
if (!storedToken) {
|
|
3199
|
+
console.warn("[ConsentService] No stored CSRF token found");
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
// Constant-time comparison to prevent timing attacks
|
|
3203
|
+
const isValid = this.constantTimeCompare(token, storedToken);
|
|
3204
|
+
// Delete token after use (one-time use)
|
|
3205
|
+
if (isValid) {
|
|
3206
|
+
await delegationStorage.delete(csrfKey);
|
|
3207
|
+
}
|
|
3208
|
+
return isValid;
|
|
3209
|
+
}
|
|
3210
|
+
catch (error) {
|
|
3211
|
+
console.error("[ConsentService] CSRF validation error:", error);
|
|
3212
|
+
return false;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
/**
|
|
3216
|
+
* Store CSRF token for credential form
|
|
3217
|
+
*
|
|
3218
|
+
* Called when rendering the credential form page.
|
|
3219
|
+
*
|
|
3220
|
+
* @param sessionId - Session ID to associate token with
|
|
3221
|
+
* @returns Generated CSRF token
|
|
3222
|
+
*/
|
|
3223
|
+
async storeCredentialCsrfToken(sessionId) {
|
|
3224
|
+
const delegationStorage = this.env.DELEGATION_STORAGE;
|
|
3225
|
+
// Generate cryptographically secure token
|
|
3226
|
+
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
|
|
3227
|
+
const token = btoa(String.fromCharCode(...tokenBytes))
|
|
3228
|
+
.replace(/\+/g, "-")
|
|
3229
|
+
.replace(/\//g, "_")
|
|
3230
|
+
.replace(/=/g, "");
|
|
3231
|
+
if (!delegationStorage) {
|
|
3232
|
+
console.warn("[ConsentService] No DELEGATION_STORAGE for CSRF storage");
|
|
3233
|
+
// Return token anyway - form will submit but validation will skip
|
|
3234
|
+
return token;
|
|
3235
|
+
}
|
|
3236
|
+
try {
|
|
3237
|
+
const csrfKey = STORAGE_KEYS.credentialCsrf(sessionId);
|
|
3238
|
+
// 10 minute TTL matches OAuth state TTL
|
|
3239
|
+
await delegationStorage.put(csrfKey, token, { expirationTtl: 600 });
|
|
3240
|
+
console.log("[ConsentService] CSRF token stored for credential form");
|
|
3241
|
+
}
|
|
3242
|
+
catch (error) {
|
|
3243
|
+
console.error("[ConsentService] CSRF token storage error:", error);
|
|
3244
|
+
}
|
|
3245
|
+
return token;
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* Constant-time string comparison to prevent timing attacks
|
|
3249
|
+
*/
|
|
3250
|
+
constantTimeCompare(a, b) {
|
|
3251
|
+
if (a.length !== b.length) {
|
|
3252
|
+
return false;
|
|
3253
|
+
}
|
|
3254
|
+
let result = 0;
|
|
3255
|
+
for (let i = 0; i < a.length; i++) {
|
|
3256
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
3257
|
+
}
|
|
3258
|
+
return result === 0;
|
|
3259
|
+
}
|
|
2767
3260
|
/**
|
|
2768
3261
|
* Build full DelegationRecord format request (future format)
|
|
2769
3262
|
*/
|
|
@@ -2773,7 +3266,7 @@ export class ConsentService {
|
|
|
2773
3266
|
const scopes = request.scopes ?? [];
|
|
2774
3267
|
const delegationRecord = {
|
|
2775
3268
|
id: crypto.randomUUID(),
|
|
2776
|
-
issuerDid: userDid
|
|
3269
|
+
issuerDid: userDid, // Phase 5: userDid is required; undefined if session is anonymous
|
|
2777
3270
|
subjectDid: request.agent_did,
|
|
2778
3271
|
constraints: {
|
|
2779
3272
|
scopes,
|
|
@@ -2834,7 +3327,7 @@ export class ConsentService {
|
|
|
2834
3327
|
});
|
|
2835
3328
|
}
|
|
2836
3329
|
else {
|
|
2837
|
-
console.log("[ConsentService] No user_identifier (
|
|
3330
|
+
console.log("[ConsentService] No user_identifier (session is anonymous) - delegation will proceed without it");
|
|
2838
3331
|
}
|
|
2839
3332
|
// Phase 2 VC-Only: Include credential_jwt if available
|
|
2840
3333
|
if (credentialJwt) {
|