@kya-os/mcp-i-cloudflare 1.6.11 → 1.6.13
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/runtime/oauth-handler.d.ts +2 -2
- package/dist/runtime/oauth-handler.d.ts.map +1 -1
- package/dist/runtime/oauth-handler.js +317 -148
- package/dist/runtime/oauth-handler.js.map +1 -1
- package/dist/services/consent.service.d.ts.map +1 -1
- package/dist/services/consent.service.js +7 -1
- package/dist/services/consent.service.js.map +1 -1
- package/package.json +2 -2
- 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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides reusable OAuth callback handler for agents using delegation flow
|
|
5
5
|
*/
|
|
6
|
-
import { STORAGE_KEYS } from
|
|
6
|
+
import { STORAGE_KEYS } from "../constants/storage-keys";
|
|
7
7
|
/**
|
|
8
8
|
* Default success page template
|
|
9
9
|
*/
|
|
@@ -94,7 +94,7 @@ const defaultSuccessTemplate = (data) => `
|
|
|
94
94
|
|
|
95
95
|
<div class="info">
|
|
96
96
|
<p>Authorized Scopes:</p>
|
|
97
|
-
<div class="code">${data.scopes.join(
|
|
97
|
+
<div class="code">${data.scopes.join(", ")}</div>
|
|
98
98
|
</div>
|
|
99
99
|
|
|
100
100
|
<p>You can now close this window and return to Claude Desktop. The authorized tools are ready to use.</p>
|
|
@@ -184,39 +184,39 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
184
184
|
return async (c) => {
|
|
185
185
|
const env = c.env;
|
|
186
186
|
// Get configuration with defaults
|
|
187
|
-
const { agentShieldApiUrl = env.AGENTSHIELD_API_URL ||
|
|
187
|
+
const { agentShieldApiUrl = env.AGENTSHIELD_API_URL || "https://hobbs.work", delegationStorage, consentService, oauthSecurityService, successTemplate = defaultSuccessTemplate, errorTemplate = defaultErrorTemplate, autoClose = true, autoCloseDelay = 5000, agentName, // MCP Server name for delegation notifications
|
|
188
188
|
} = config;
|
|
189
189
|
// Get query parameters
|
|
190
|
-
const code = c.req.query(
|
|
191
|
-
const stateParam = c.req.query(
|
|
192
|
-
const error = c.req.query(
|
|
190
|
+
const code = c.req.query("code");
|
|
191
|
+
const stateParam = c.req.query("state");
|
|
192
|
+
const error = c.req.query("error");
|
|
193
193
|
// Handle OAuth errors
|
|
194
194
|
if (error) {
|
|
195
|
-
const errorDescription = c.req.query(
|
|
196
|
-
console.error(
|
|
195
|
+
const errorDescription = c.req.query("error_description") || "Authorization failed";
|
|
196
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Error from provider:", {
|
|
197
197
|
error,
|
|
198
198
|
errorDescription,
|
|
199
199
|
timestamp: new Date().toISOString(),
|
|
200
|
-
eventType:
|
|
200
|
+
eventType: "oauth_provider_error",
|
|
201
201
|
});
|
|
202
202
|
const html = errorTemplate({
|
|
203
203
|
error,
|
|
204
|
-
description: errorDescription
|
|
204
|
+
description: errorDescription,
|
|
205
205
|
});
|
|
206
206
|
return c.html(html, 400);
|
|
207
207
|
}
|
|
208
208
|
// Validate required parameters
|
|
209
209
|
if (!code || !stateParam) {
|
|
210
|
-
console.error(
|
|
210
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Missing required parameters:", {
|
|
211
211
|
hasCode: !!code,
|
|
212
212
|
hasState: !!stateParam,
|
|
213
213
|
timestamp: new Date().toISOString(),
|
|
214
|
-
eventType:
|
|
215
|
-
reason:
|
|
214
|
+
eventType: "oauth_validation_failed",
|
|
215
|
+
reason: "missing_parameters",
|
|
216
216
|
});
|
|
217
217
|
const html = errorTemplate({
|
|
218
|
-
error:
|
|
219
|
-
description:
|
|
218
|
+
error: "invalid_request",
|
|
219
|
+
description: "Missing authorization code or state parameter",
|
|
220
220
|
});
|
|
221
221
|
return c.html(html, 400);
|
|
222
222
|
}
|
|
@@ -228,15 +228,15 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
228
228
|
try {
|
|
229
229
|
stateData = await oauthSecurityService.getOAuthState(stateParam);
|
|
230
230
|
if (!stateData) {
|
|
231
|
-
console.error(
|
|
232
|
-
stateParam: stateParam.substring(0, 20) +
|
|
231
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: State validation failed - state not found or expired:", {
|
|
232
|
+
stateParam: stateParam.substring(0, 20) + "...",
|
|
233
233
|
timestamp: new Date().toISOString(),
|
|
234
|
-
eventType:
|
|
235
|
-
reason:
|
|
234
|
+
eventType: "csrf_protection_failed",
|
|
235
|
+
reason: "state_not_found_or_expired",
|
|
236
236
|
});
|
|
237
237
|
const html = errorTemplate({
|
|
238
|
-
error:
|
|
239
|
-
description:
|
|
238
|
+
error: "invalid_state",
|
|
239
|
+
description: "Invalid or expired state parameter. This may be a CSRF attack or the authorization request has expired.",
|
|
240
240
|
});
|
|
241
241
|
return c.html(html, 400);
|
|
242
242
|
}
|
|
@@ -247,46 +247,46 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
247
247
|
session_id: stateData.session_id,
|
|
248
248
|
delegation_id: stateData.delegation_id,
|
|
249
249
|
};
|
|
250
|
-
console.log(
|
|
250
|
+
console.log("[OAuth] 🔒 SECURITY EVENT: State validated successfully:", {
|
|
251
251
|
projectId: state.project_id,
|
|
252
|
-
agentDid: state.agent_did.substring(0, 20) +
|
|
253
|
-
sessionId: state.session_id?.substring(0, 20) +
|
|
252
|
+
agentDid: state.agent_did.substring(0, 20) + "...",
|
|
253
|
+
sessionId: state.session_id?.substring(0, 20) + "...",
|
|
254
254
|
timestamp: new Date().toISOString(),
|
|
255
|
-
eventType:
|
|
256
|
-
stateStoredAt: stateData.storedAt
|
|
255
|
+
eventType: "csrf_protection_success",
|
|
256
|
+
stateStoredAt: stateData.storedAt,
|
|
257
257
|
});
|
|
258
258
|
}
|
|
259
259
|
catch (err) {
|
|
260
|
-
console.error(
|
|
260
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: State validation error:", {
|
|
261
261
|
error: err instanceof Error ? err.message : String(err),
|
|
262
|
-
stateParam: stateParam.substring(0, 20) +
|
|
262
|
+
stateParam: stateParam.substring(0, 20) + "...",
|
|
263
263
|
timestamp: new Date().toISOString(),
|
|
264
|
-
eventType:
|
|
265
|
-
reason:
|
|
264
|
+
eventType: "csrf_protection_error",
|
|
265
|
+
reason: "validation_exception",
|
|
266
266
|
});
|
|
267
267
|
const html = errorTemplate({
|
|
268
|
-
error:
|
|
269
|
-
description:
|
|
268
|
+
error: "invalid_state",
|
|
269
|
+
description: "Failed to validate state parameter",
|
|
270
270
|
});
|
|
271
271
|
return c.html(html, 400);
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
else {
|
|
275
275
|
// Fallback: Decode state parameter directly (less secure, but backward compatible)
|
|
276
|
-
console.warn(
|
|
276
|
+
console.warn("[OAuth] ⚠️ SECURITY WARNING: OAuthSecurityService not provided, using insecure state decoding");
|
|
277
277
|
try {
|
|
278
278
|
state = JSON.parse(atob(stateParam));
|
|
279
279
|
}
|
|
280
280
|
catch (err) {
|
|
281
|
-
console.error(
|
|
281
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Failed to decode state:", {
|
|
282
282
|
error: err instanceof Error ? err.message : String(err),
|
|
283
283
|
timestamp: new Date().toISOString(),
|
|
284
|
-
eventType:
|
|
285
|
-
reason:
|
|
284
|
+
eventType: "oauth_validation_failed",
|
|
285
|
+
reason: "state_decode_error",
|
|
286
286
|
});
|
|
287
287
|
const html = errorTemplate({
|
|
288
|
-
error:
|
|
289
|
-
description:
|
|
288
|
+
error: "invalid_state",
|
|
289
|
+
description: "Invalid state parameter",
|
|
290
290
|
});
|
|
291
291
|
return c.html(html, 400);
|
|
292
292
|
}
|
|
@@ -294,77 +294,233 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
294
294
|
const { project_id, agent_did, session_id, delegation_id } = state;
|
|
295
295
|
// Validate session ID
|
|
296
296
|
if (!session_id) {
|
|
297
|
-
console.error(
|
|
297
|
+
console.error("[OAuth] No session ID in state");
|
|
298
298
|
const html = errorTemplate({
|
|
299
|
-
error:
|
|
300
|
-
description:
|
|
299
|
+
error: "missing_session",
|
|
300
|
+
description: "No session ID provided in OAuth state",
|
|
301
301
|
});
|
|
302
302
|
return c.html(html, 400);
|
|
303
303
|
}
|
|
304
|
-
|
|
304
|
+
// Extract direct PKCE flow data from state
|
|
305
|
+
const isDirectPKCE = stateData?.direct_pkce === true;
|
|
306
|
+
const stateProvider = stateData?.provider;
|
|
307
|
+
const codeVerifier = stateData?.code_verifier;
|
|
308
|
+
const redirectUri = stateData?.redirect_uri;
|
|
309
|
+
const requestedScopes = stateData?.scopes || [];
|
|
310
|
+
console.log("[OAuth] 🔒 SECURITY EVENT: Processing authorization code exchange:", {
|
|
305
311
|
projectId: project_id,
|
|
306
|
-
agentDid: agent_did.substring(0, 20) +
|
|
307
|
-
sessionId: session_id?.substring(0, 20) +
|
|
312
|
+
agentDid: agent_did.substring(0, 20) + "...",
|
|
313
|
+
sessionId: session_id?.substring(0, 20) + "...",
|
|
308
314
|
delegationId: delegation_id,
|
|
309
315
|
timestamp: new Date().toISOString(),
|
|
310
|
-
eventType:
|
|
311
|
-
hasSecureState: !!oauthSecurityService
|
|
316
|
+
eventType: "oauth_code_exchange_start",
|
|
317
|
+
hasSecureState: !!oauthSecurityService,
|
|
318
|
+
isDirectPKCE,
|
|
319
|
+
provider: stateProvider,
|
|
320
|
+
hasCodeVerifier: !!codeVerifier,
|
|
312
321
|
});
|
|
322
|
+
let tokenData;
|
|
313
323
|
try {
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
'Content-Type': 'application/json',
|
|
320
|
-
'Accept': 'application/json'
|
|
321
|
-
},
|
|
322
|
-
body: JSON.stringify({
|
|
323
|
-
grant_type: 'authorization_code',
|
|
324
|
-
code: code,
|
|
325
|
-
agent_did: agent_did,
|
|
326
|
-
project_id: project_id
|
|
327
|
-
})
|
|
328
|
-
});
|
|
329
|
-
if (!tokenResponse.ok) {
|
|
330
|
-
const errorText = await tokenResponse.text();
|
|
331
|
-
console.error('[OAuth] 🔒 SECURITY EVENT: Token exchange failed:', {
|
|
332
|
-
status: tokenResponse.status,
|
|
333
|
-
error: errorText.substring(0, 200),
|
|
324
|
+
// Check if this is a direct PKCE flow (exchange with provider directly)
|
|
325
|
+
if (isDirectPKCE && stateProvider && codeVerifier && redirectUri) {
|
|
326
|
+
console.log("[OAuth] 🔐 Direct PKCE flow detected, exchanging with provider directly:", {
|
|
327
|
+
provider: stateProvider,
|
|
328
|
+
redirectUri,
|
|
334
329
|
projectId: project_id,
|
|
335
|
-
agentDid: agent_did.substring(0, 20) + '...',
|
|
336
|
-
timestamp: new Date().toISOString(),
|
|
337
|
-
eventType: 'oauth_token_exchange_failed'
|
|
338
330
|
});
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
331
|
+
// Import services for direct PKCE flow
|
|
332
|
+
const { OAuthService, OAuthConfigService } = await import("@kya-os/mcp-i-core");
|
|
333
|
+
const { KVOAuthConfigCache } = await import("../cache/kv-oauth-config-cache.js");
|
|
334
|
+
// Create fetch provider for OAuth services
|
|
335
|
+
const fetchProvider = {
|
|
336
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
337
|
+
// These methods are not needed for PKCE token exchange, stub them
|
|
338
|
+
resolveDID: async () => {
|
|
339
|
+
throw new Error("Not implemented");
|
|
340
|
+
},
|
|
341
|
+
fetchStatusList: async () => {
|
|
342
|
+
throw new Error("Not implemented");
|
|
343
|
+
},
|
|
344
|
+
fetchDelegationChain: async () => {
|
|
345
|
+
throw new Error("Not implemented");
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
// Initialize OAuth services
|
|
349
|
+
const oauthConfigCache = new KVOAuthConfigCache({
|
|
350
|
+
kv: delegationStorage,
|
|
351
|
+
});
|
|
352
|
+
const apiKey = env.AGENTSHIELD_API_KEY || "";
|
|
353
|
+
const oauthConfigService = new OAuthConfigService({
|
|
354
|
+
baseUrl: env.AGENTSHIELD_API_URL || "https://kya.vouched.id",
|
|
355
|
+
apiKey: apiKey,
|
|
356
|
+
fetchProvider: fetchProvider,
|
|
357
|
+
cache: oauthConfigCache,
|
|
358
|
+
});
|
|
359
|
+
const oauthService = new OAuthService({
|
|
360
|
+
configService: oauthConfigService,
|
|
361
|
+
fetchProvider: fetchProvider,
|
|
362
|
+
agentShieldApiUrl: env.AGENTSHIELD_API_URL || "https://kya.vouched.id",
|
|
363
|
+
agentShieldApiKey: apiKey,
|
|
364
|
+
projectId: project_id,
|
|
365
|
+
logger: (msg, data) => console.log(`[OAuthService] ${msg}`, data),
|
|
366
|
+
});
|
|
367
|
+
// Step 1: Exchange authorization code with OAuth provider (GitHub) directly
|
|
368
|
+
const idpTokens = await oauthService.exchangeToken(stateProvider, code, codeVerifier, redirectUri);
|
|
369
|
+
console.log("[OAuth] ✅ Direct PKCE token exchange successful:", {
|
|
370
|
+
provider: stateProvider,
|
|
371
|
+
hasAccessToken: !!idpTokens.access_token,
|
|
372
|
+
expiresAt: new Date(idpTokens.expires_at).toISOString(),
|
|
373
|
+
});
|
|
374
|
+
// Step 2: Get user info from provider (optional but useful for User DID)
|
|
375
|
+
let userInfo = null;
|
|
376
|
+
const oauthConfig = await oauthConfigService.getOAuthConfig(project_id);
|
|
377
|
+
const providerConfig = oauthConfig.providers[stateProvider];
|
|
378
|
+
if (providerConfig?.userInfoUrl) {
|
|
379
|
+
try {
|
|
380
|
+
const userInfoResponse = await fetch(providerConfig.userInfoUrl, {
|
|
381
|
+
headers: {
|
|
382
|
+
Authorization: `Bearer ${idpTokens.access_token}`,
|
|
383
|
+
Accept: "application/json",
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
if (userInfoResponse.ok) {
|
|
387
|
+
userInfo = await userInfoResponse.json();
|
|
388
|
+
console.log("[OAuth] ✅ User info retrieved:", {
|
|
389
|
+
provider: stateProvider,
|
|
390
|
+
hasEmail: !!userInfo?.email,
|
|
391
|
+
hasName: !!userInfo?.name,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
console.warn("[OAuth] Failed to get user info (non-fatal):", err);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Step 3: Create delegation on AgentShield
|
|
400
|
+
const delegationEndpoint = `${agentShieldApiUrl}/api/v1/bouncer/delegations`;
|
|
401
|
+
// Build user identifier from OAuth user info
|
|
402
|
+
const userIdentifier = userInfo?.email || userInfo?.login || userInfo?.sub || userInfo?.id;
|
|
403
|
+
const delegationResponse = await fetch(delegationEndpoint, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: {
|
|
406
|
+
"Content-Type": "application/json",
|
|
407
|
+
Authorization: `Bearer ${env.AGENTSHIELD_API_KEY}`,
|
|
408
|
+
"X-Project-Id": project_id,
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
agent_did: agent_did,
|
|
412
|
+
scopes: requestedScopes,
|
|
413
|
+
session_id: session_id,
|
|
414
|
+
provider: stateProvider,
|
|
415
|
+
user_identifier: userIdentifier,
|
|
416
|
+
// Include OAuth metadata for audit trail
|
|
417
|
+
metadata: {
|
|
418
|
+
oauth_provider: stateProvider,
|
|
419
|
+
oauth_flow: "direct_pkce",
|
|
420
|
+
oauth_user_id: userInfo?.sub || userInfo?.id || userInfo?.login,
|
|
421
|
+
},
|
|
422
|
+
}),
|
|
423
|
+
});
|
|
424
|
+
if (!delegationResponse.ok) {
|
|
425
|
+
const errorText = await delegationResponse.text();
|
|
426
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Delegation creation failed:", {
|
|
427
|
+
status: delegationResponse.status,
|
|
428
|
+
error: errorText.substring(0, 200),
|
|
429
|
+
projectId: project_id,
|
|
430
|
+
agentDid: agent_did.substring(0, 20) + "...",
|
|
431
|
+
timestamp: new Date().toISOString(),
|
|
432
|
+
eventType: "delegation_creation_failed",
|
|
433
|
+
});
|
|
434
|
+
const html = errorTemplate({
|
|
435
|
+
error: "delegation_failed",
|
|
436
|
+
description: "Failed to create delegation after OAuth authorization",
|
|
437
|
+
});
|
|
438
|
+
return c.html(html, delegationResponse.status);
|
|
439
|
+
}
|
|
440
|
+
const delegationData = await delegationResponse.json();
|
|
441
|
+
// Map delegation response to TokenExchangeResponse format
|
|
442
|
+
tokenData = {
|
|
443
|
+
delegation_token: delegationData.data?.delegation_token ||
|
|
444
|
+
delegationData.delegation_token,
|
|
445
|
+
token_type: "Bearer",
|
|
446
|
+
expires_in: delegationData.data?.expires_in ||
|
|
447
|
+
delegationData.expires_in ||
|
|
448
|
+
3600,
|
|
449
|
+
delegation_id: delegationData.data?.delegation_id ||
|
|
450
|
+
delegationData.delegation_id ||
|
|
451
|
+
`del_${Date.now()}`,
|
|
452
|
+
scopes: requestedScopes,
|
|
453
|
+
session_id: session_id,
|
|
454
|
+
};
|
|
455
|
+
// Attach provider and IDP tokens for downstream processing
|
|
456
|
+
tokenData.provider = stateProvider;
|
|
457
|
+
tokenData.idp_tokens = idpTokens;
|
|
458
|
+
tokenData.user_info = userInfo;
|
|
459
|
+
console.log("[OAuth] ✅ Direct PKCE flow complete:", {
|
|
460
|
+
delegationId: tokenData.delegation_id,
|
|
461
|
+
provider: stateProvider,
|
|
462
|
+
hasIdpTokens: true,
|
|
463
|
+
hasUserInfo: !!userInfo,
|
|
342
464
|
});
|
|
343
|
-
return c.html(html, tokenResponse.status);
|
|
344
465
|
}
|
|
345
|
-
|
|
466
|
+
else {
|
|
467
|
+
// Bouncer flow: Exchange via AgentShield's OAuth token endpoint
|
|
468
|
+
// This is the legacy flow for proxy mode or when PKCE is not available
|
|
469
|
+
console.log("[OAuth] 📡 Using bouncer flow for token exchange");
|
|
470
|
+
const tokenEndpoint = `${agentShieldApiUrl}/api/v1/bouncer/oauth/token`;
|
|
471
|
+
const tokenResponse = await fetch(tokenEndpoint, {
|
|
472
|
+
method: "POST",
|
|
473
|
+
headers: {
|
|
474
|
+
"Content-Type": "application/json",
|
|
475
|
+
Accept: "application/json",
|
|
476
|
+
},
|
|
477
|
+
body: JSON.stringify({
|
|
478
|
+
grant_type: "authorization_code",
|
|
479
|
+
code: code,
|
|
480
|
+
agent_did: agent_did,
|
|
481
|
+
project_id: project_id,
|
|
482
|
+
}),
|
|
483
|
+
});
|
|
484
|
+
if (!tokenResponse.ok) {
|
|
485
|
+
const errorText = await tokenResponse.text();
|
|
486
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Token exchange failed:", {
|
|
487
|
+
status: tokenResponse.status,
|
|
488
|
+
error: errorText.substring(0, 200),
|
|
489
|
+
projectId: project_id,
|
|
490
|
+
agentDid: agent_did.substring(0, 20) + "...",
|
|
491
|
+
timestamp: new Date().toISOString(),
|
|
492
|
+
eventType: "oauth_token_exchange_failed",
|
|
493
|
+
});
|
|
494
|
+
const html = errorTemplate({
|
|
495
|
+
error: "token_exchange_failed",
|
|
496
|
+
description: "Failed to exchange authorization code for delegation token",
|
|
497
|
+
});
|
|
498
|
+
return c.html(html, tokenResponse.status);
|
|
499
|
+
}
|
|
500
|
+
tokenData = await tokenResponse.json();
|
|
501
|
+
}
|
|
346
502
|
// Validate token response
|
|
347
503
|
if (!tokenData.delegation_token) {
|
|
348
|
-
console.error(
|
|
504
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Invalid token response:", {
|
|
349
505
|
hasDelegationToken: !!tokenData.delegation_token,
|
|
350
506
|
responseKeys: Object.keys(tokenData),
|
|
351
507
|
projectId: project_id,
|
|
352
508
|
timestamp: new Date().toISOString(),
|
|
353
|
-
eventType:
|
|
509
|
+
eventType: "oauth_invalid_token_response",
|
|
354
510
|
});
|
|
355
511
|
const html = errorTemplate({
|
|
356
|
-
error:
|
|
357
|
-
description:
|
|
512
|
+
error: "invalid_response",
|
|
513
|
+
description: "Invalid token response from authorization server",
|
|
358
514
|
});
|
|
359
515
|
return c.html(html, 500);
|
|
360
516
|
}
|
|
361
|
-
console.log(
|
|
517
|
+
console.log("[OAuth] 🔒 SECURITY EVENT: Token exchange successful:", {
|
|
362
518
|
delegationId: tokenData.delegation_id,
|
|
363
519
|
sessionId: tokenData.session_id || session_id,
|
|
364
520
|
expiresIn: tokenData.expires_in,
|
|
365
521
|
scopes: tokenData.scopes,
|
|
366
522
|
timestamp: new Date().toISOString(),
|
|
367
|
-
eventType:
|
|
523
|
+
eventType: "oauth_token_exchange_success",
|
|
368
524
|
});
|
|
369
525
|
// Phase 4 PR #3: Extract OAuth user info and link to User DID
|
|
370
526
|
let oauthIdentity = null;
|
|
@@ -380,13 +536,15 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
380
536
|
// 2. State parameter (if stored during OAuth initiation)
|
|
381
537
|
// 3. Environment variable (if configured)
|
|
382
538
|
// 4. Default fallback ('google')
|
|
383
|
-
const provider = tokenData.provider
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
539
|
+
const provider = tokenData.provider ||
|
|
540
|
+
state.provider ||
|
|
541
|
+
env.DEFAULT_OAUTH_PROVIDER ||
|
|
542
|
+
"google";
|
|
387
543
|
oauthIdentity = {
|
|
388
544
|
provider: provider,
|
|
389
|
-
subject: userInfoFromToken.sub ||
|
|
545
|
+
subject: userInfoFromToken.sub ||
|
|
546
|
+
userInfoFromToken.id ||
|
|
547
|
+
userInfoFromToken.email,
|
|
390
548
|
email: userInfoFromToken.email,
|
|
391
549
|
name: userInfoFromToken.name || userInfoFromToken.display_name,
|
|
392
550
|
};
|
|
@@ -396,8 +554,8 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
396
554
|
// This requires an access_token from the token exchange
|
|
397
555
|
// For now, we'll log a warning and skip OAuth linking
|
|
398
556
|
// TODO: Implement userinfo endpoint call if access_token is available
|
|
399
|
-
console.warn(
|
|
400
|
-
console.warn(
|
|
557
|
+
console.warn("[OAuth] User info not available in token response. OAuth linking skipped.");
|
|
558
|
+
console.warn("[OAuth] To enable OAuth linking, ensure AgentShield returns user info or access_token in token response.");
|
|
401
559
|
}
|
|
402
560
|
// Link OAuth identity to User DID if we have it
|
|
403
561
|
if (oauthIdentity && oauthIdentity.subject) {
|
|
@@ -405,25 +563,25 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
405
563
|
// Set OAuth identity cookie for consent page
|
|
406
564
|
const cookieValue = encodeURIComponent(JSON.stringify(oauthIdentity));
|
|
407
565
|
c.header("Set-Cookie", `oauth_identity=${cookieValue}; HttpOnly; Secure; SameSite=Lax; Max-Age=604800; Path=/`);
|
|
408
|
-
console.log(
|
|
566
|
+
console.log("[OAuth] 🔒 SECURITY EVENT: OAuth identity linked and cookie set:", {
|
|
409
567
|
provider: oauthIdentity.provider,
|
|
410
|
-
subject: oauthIdentity.subject.substring(0, 20) +
|
|
411
|
-
userDid: userDid.substring(0, 20) +
|
|
412
|
-
sessionId: session_id?.substring(0, 20) +
|
|
568
|
+
subject: oauthIdentity.subject.substring(0, 20) + "...",
|
|
569
|
+
userDid: userDid.substring(0, 20) + "...",
|
|
570
|
+
sessionId: session_id?.substring(0, 20) + "...",
|
|
413
571
|
timestamp: new Date().toISOString(),
|
|
414
|
-
eventType:
|
|
415
|
-
cookieSet: true
|
|
572
|
+
eventType: "oauth_identity_linked",
|
|
573
|
+
cookieSet: true,
|
|
416
574
|
});
|
|
417
575
|
}
|
|
418
576
|
}
|
|
419
577
|
catch (error) {
|
|
420
578
|
// OAuth linking errors are non-fatal - log but continue
|
|
421
|
-
console.error(
|
|
579
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Failed to link OAuth identity (non-fatal):", {
|
|
422
580
|
error: error instanceof Error ? error.message : String(error),
|
|
423
|
-
sessionId: session_id?.substring(0, 20) +
|
|
581
|
+
sessionId: session_id?.substring(0, 20) + "...",
|
|
424
582
|
timestamp: new Date().toISOString(),
|
|
425
|
-
eventType:
|
|
426
|
-
severity:
|
|
583
|
+
eventType: "oauth_identity_linking_failed",
|
|
584
|
+
severity: "warning",
|
|
427
585
|
});
|
|
428
586
|
}
|
|
429
587
|
}
|
|
@@ -431,7 +589,9 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
431
589
|
// After receiving delegation token, retrieve OAuth tokens separately
|
|
432
590
|
// Note: userDid may not be available if OAuth linking failed, but we still need to store tokens
|
|
433
591
|
// For PKCE flows, we can use session-based userDid lookup if needed
|
|
434
|
-
if (delegationStorage &&
|
|
592
|
+
if (delegationStorage &&
|
|
593
|
+
oauthSecurityService &&
|
|
594
|
+
tokenData.delegation_id) {
|
|
435
595
|
try {
|
|
436
596
|
// Extract provider from state or token response
|
|
437
597
|
const provider = tokenData.provider ||
|
|
@@ -451,7 +611,8 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
451
611
|
let effectiveUserDid = userDid;
|
|
452
612
|
if (!effectiveUserDid && session_id && consentService) {
|
|
453
613
|
try {
|
|
454
|
-
effectiveUserDid =
|
|
614
|
+
effectiveUserDid =
|
|
615
|
+
await consentService.getUserDidForSession(session_id);
|
|
455
616
|
}
|
|
456
617
|
catch (error) {
|
|
457
618
|
console.warn("[OAuth] Failed to get userDid from session, skipping token storage:", {
|
|
@@ -566,47 +727,49 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
566
727
|
if (delegationStorage) {
|
|
567
728
|
// Use != null to properly handle expires_in: 0 (immediate expiration)
|
|
568
729
|
// Truthiness check would incorrectly treat 0 as missing
|
|
569
|
-
const ttl = tokenData.expires_in != null
|
|
730
|
+
const ttl = tokenData.expires_in != null
|
|
731
|
+
? tokenData.expires_in
|
|
732
|
+
: 7 * 24 * 60 * 60; // Default 7 days
|
|
570
733
|
try {
|
|
571
734
|
// Get userDID from session if available (Phase 4)
|
|
572
735
|
// Use linked userDid if available, otherwise check session
|
|
573
736
|
let sessionUserDid = userDid;
|
|
574
737
|
const sessionKey = STORAGE_KEYS.session(session_id);
|
|
575
738
|
if (!sessionUserDid) {
|
|
576
|
-
const sessionData = await delegationStorage.get(sessionKey, "json");
|
|
739
|
+
const sessionData = (await delegationStorage.get(sessionKey, "json"));
|
|
577
740
|
sessionUserDid = sessionData?.userDid;
|
|
578
741
|
}
|
|
579
742
|
// Primary: User+Agent scoped (no conflicts) - Phase 4
|
|
580
743
|
if (sessionUserDid) {
|
|
581
744
|
const userAgentKey = STORAGE_KEYS.delegation(sessionUserDid, agent_did);
|
|
582
745
|
await delegationStorage.put(userAgentKey, tokenData.delegation_token, {
|
|
583
|
-
expirationTtl: ttl
|
|
746
|
+
expirationTtl: ttl,
|
|
584
747
|
});
|
|
585
|
-
console.log(
|
|
586
|
-
key: userAgentKey.substring(0, 50) +
|
|
748
|
+
console.log("[OAuth] 🔒 SECURITY EVENT: Delegation token stored with user+agent DID:", {
|
|
749
|
+
key: userAgentKey.substring(0, 50) + "...",
|
|
587
750
|
ttl,
|
|
588
|
-
agentDid: agent_did.substring(0, 20) +
|
|
589
|
-
userDid: sessionUserDid.substring(0, 20) +
|
|
751
|
+
agentDid: agent_did.substring(0, 20) + "...",
|
|
752
|
+
userDid: sessionUserDid.substring(0, 20) + "...",
|
|
590
753
|
delegationId: tokenData.delegation_id,
|
|
591
754
|
timestamp: new Date().toISOString(),
|
|
592
|
-
eventType:
|
|
593
|
-
storageType:
|
|
755
|
+
eventType: "delegation_token_stored",
|
|
756
|
+
storageType: "user_agent_scoped",
|
|
594
757
|
});
|
|
595
758
|
}
|
|
596
759
|
// Backward compatibility: Agent-only key (24 hour TTL)
|
|
597
760
|
const legacyKey = STORAGE_KEYS.legacyDelegation(agent_did);
|
|
598
761
|
await delegationStorage.put(legacyKey, tokenData.delegation_token, {
|
|
599
|
-
expirationTtl: 24 * 60 * 60 // 24 hours only
|
|
762
|
+
expirationTtl: 24 * 60 * 60, // 24 hours only
|
|
600
763
|
});
|
|
601
|
-
console.log(
|
|
602
|
-
key: legacyKey.substring(0, 50) +
|
|
764
|
+
console.log("[OAuth] 🔒 SECURITY EVENT: Delegation token stored with legacy agent key:", {
|
|
765
|
+
key: legacyKey.substring(0, 50) + "...",
|
|
603
766
|
ttl: 24 * 60 * 60,
|
|
604
|
-
agentDid: agent_did.substring(0, 20) +
|
|
767
|
+
agentDid: agent_did.substring(0, 20) + "...",
|
|
605
768
|
delegationId: tokenData.delegation_id,
|
|
606
769
|
timestamp: new Date().toISOString(),
|
|
607
|
-
eventType:
|
|
608
|
-
storageType:
|
|
609
|
-
warning:
|
|
770
|
+
eventType: "delegation_token_stored",
|
|
771
|
+
storageType: "legacy_agent_scoped",
|
|
772
|
+
warning: "Legacy format - migrate to user+agent scoped tokens",
|
|
610
773
|
});
|
|
611
774
|
// Session cache for fast lookup (shorter TTL for performance)
|
|
612
775
|
await delegationStorage.put(sessionKey, JSON.stringify({
|
|
@@ -615,16 +778,16 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
615
778
|
delegationToken: tokenData.delegation_token,
|
|
616
779
|
cachedAt: Date.now(),
|
|
617
780
|
}), {
|
|
618
|
-
expirationTtl: Math.min(ttl, 1800) // 30 minutes or token TTL, whichever is shorter
|
|
781
|
+
expirationTtl: Math.min(ttl, 1800), // 30 minutes or token TTL, whichever is shorter
|
|
619
782
|
});
|
|
620
|
-
console.log(
|
|
621
|
-
key: sessionKey.substring(0, 50) +
|
|
783
|
+
console.log("[OAuth] 🔒 SECURITY EVENT: Delegation token cached for session:", {
|
|
784
|
+
key: sessionKey.substring(0, 50) + "...",
|
|
622
785
|
ttl: Math.min(ttl, 1800),
|
|
623
|
-
sessionId: session_id?.substring(0, 20) +
|
|
624
|
-
userDid: sessionUserDid?.substring(0, 20) +
|
|
786
|
+
sessionId: session_id?.substring(0, 20) + "...",
|
|
787
|
+
userDid: sessionUserDid?.substring(0, 20) + "...",
|
|
625
788
|
timestamp: new Date().toISOString(),
|
|
626
|
-
eventType:
|
|
627
|
-
storageType:
|
|
789
|
+
eventType: "delegation_token_cached",
|
|
790
|
+
storageType: "session_cache",
|
|
628
791
|
});
|
|
629
792
|
// Fire-and-forget notification to AgentShield for audit trail
|
|
630
793
|
// This enables dashboard visibility into delegations created via direct OAuth flow
|
|
@@ -634,13 +797,13 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
634
797
|
// Use != null to properly handle expires_in: 0 (immediate expiration)
|
|
635
798
|
// Truthiness check would incorrectly treat 0 as missing
|
|
636
799
|
const expiresAt = tokenData.expires_in != null
|
|
637
|
-
? new Date(Date.now() +
|
|
800
|
+
? new Date(Date.now() + tokenData.expires_in * 1000).toISOString()
|
|
638
801
|
: null;
|
|
639
802
|
fetch(delegationNotifyUrl, {
|
|
640
|
-
method:
|
|
803
|
+
method: "POST",
|
|
641
804
|
headers: {
|
|
642
|
-
|
|
643
|
-
|
|
805
|
+
"Content-Type": "application/json",
|
|
806
|
+
"X-AgentShield-Key": env.AGENTSHIELD_API_KEY || "",
|
|
644
807
|
},
|
|
645
808
|
// AgentShield notifyDelegationSchema uses snake_case
|
|
646
809
|
body: JSON.stringify({
|
|
@@ -660,28 +823,30 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
660
823
|
expires_at: expiresAt || undefined,
|
|
661
824
|
// Metadata for audit trail and debugging
|
|
662
825
|
metadata: {
|
|
663
|
-
source:
|
|
664
|
-
oauth_flow:
|
|
826
|
+
source: "mcp-i-direct-oauth",
|
|
827
|
+
oauth_flow: "pkce",
|
|
665
828
|
mcp_server_name: agentName || undefined,
|
|
666
829
|
session_id: session_id || undefined,
|
|
667
830
|
},
|
|
668
831
|
}),
|
|
669
|
-
})
|
|
832
|
+
})
|
|
833
|
+
.then((response) => {
|
|
670
834
|
if (response.ok) {
|
|
671
|
-
console.log(
|
|
835
|
+
console.log("[OAuth] Delegation notification sent to AgentShield:", {
|
|
672
836
|
delegation_id: tokenData.delegation_id,
|
|
673
837
|
project_id: project_id,
|
|
674
838
|
agent_name: agentName,
|
|
675
839
|
});
|
|
676
840
|
}
|
|
677
841
|
else {
|
|
678
|
-
console.warn(
|
|
842
|
+
console.warn("[OAuth] Delegation notification failed (non-blocking):", {
|
|
679
843
|
status: response.status,
|
|
680
844
|
delegation_id: tokenData.delegation_id,
|
|
681
845
|
});
|
|
682
846
|
}
|
|
683
|
-
})
|
|
684
|
-
|
|
847
|
+
})
|
|
848
|
+
.catch((err) => {
|
|
849
|
+
console.warn("[OAuth] Delegation notification failed (non-blocking):", {
|
|
685
850
|
error: err instanceof Error ? err.message : String(err),
|
|
686
851
|
delegation_id: tokenData.delegation_id,
|
|
687
852
|
});
|
|
@@ -689,12 +854,14 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
689
854
|
}
|
|
690
855
|
catch (storageError) {
|
|
691
856
|
// Storage errors are non-fatal - log but continue
|
|
692
|
-
console.error(
|
|
693
|
-
error: storageError instanceof Error
|
|
694
|
-
|
|
857
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Storage error (non-fatal):", {
|
|
858
|
+
error: storageError instanceof Error
|
|
859
|
+
? storageError.message
|
|
860
|
+
: String(storageError),
|
|
861
|
+
sessionId: session_id?.substring(0, 20) + "...",
|
|
695
862
|
timestamp: new Date().toISOString(),
|
|
696
|
-
eventType:
|
|
697
|
-
severity:
|
|
863
|
+
eventType: "delegation_storage_error",
|
|
864
|
+
severity: "warning",
|
|
698
865
|
});
|
|
699
866
|
}
|
|
700
867
|
}
|
|
@@ -703,21 +870,23 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
703
870
|
delegationId: tokenData.delegation_id || delegation_id,
|
|
704
871
|
sessionId: session_id,
|
|
705
872
|
scopes: tokenData.scopes || [],
|
|
706
|
-
expiresIn: autoClose ? autoCloseDelay : 0
|
|
873
|
+
expiresIn: autoClose ? autoCloseDelay : 0,
|
|
707
874
|
});
|
|
708
875
|
return c.html(html);
|
|
709
876
|
}
|
|
710
877
|
catch (error) {
|
|
711
|
-
console.error(
|
|
878
|
+
console.error("[OAuth] 🔒 SECURITY EVENT: Unexpected error:", {
|
|
712
879
|
error: error instanceof Error ? error.message : String(error),
|
|
713
880
|
stack: error instanceof Error ? error.stack : undefined,
|
|
714
881
|
timestamp: new Date().toISOString(),
|
|
715
|
-
eventType:
|
|
716
|
-
severity:
|
|
882
|
+
eventType: "oauth_unexpected_error",
|
|
883
|
+
severity: "error",
|
|
717
884
|
});
|
|
718
885
|
const html = errorTemplate({
|
|
719
|
-
error:
|
|
720
|
-
description: error instanceof Error
|
|
886
|
+
error: "internal_error",
|
|
887
|
+
description: error instanceof Error
|
|
888
|
+
? error.message
|
|
889
|
+
: "An unexpected error occurred",
|
|
721
890
|
});
|
|
722
891
|
return c.html(html, 500);
|
|
723
892
|
}
|
|
@@ -736,17 +905,17 @@ export function createOAuthCallbackHandler(config = {}) {
|
|
|
736
905
|
*/
|
|
737
906
|
export function extractDelegationToken(c) {
|
|
738
907
|
// Check Authorization header
|
|
739
|
-
const authHeader = c.req.header(
|
|
740
|
-
if (authHeader?.startsWith(
|
|
908
|
+
const authHeader = c.req.header("Authorization");
|
|
909
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
741
910
|
return authHeader.substring(7);
|
|
742
911
|
}
|
|
743
912
|
// Check custom header
|
|
744
|
-
const customHeader = c.req.header(
|
|
913
|
+
const customHeader = c.req.header("x-delegation-token");
|
|
745
914
|
if (customHeader) {
|
|
746
915
|
return customHeader;
|
|
747
916
|
}
|
|
748
917
|
// Check query parameter
|
|
749
|
-
const queryParam = c.req.query(
|
|
918
|
+
const queryParam = c.req.query("delegation_token");
|
|
750
919
|
if (queryParam) {
|
|
751
920
|
return queryParam;
|
|
752
921
|
}
|