@darbotlabs/darbot-browser-mcp 0.1.1 → 1.3.0
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/LICENSE +1 -1
- package/README.md +249 -158
- package/cli.js +1 -1
- package/config.d.ts +77 -1
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/ai/context.js +150 -0
- package/lib/ai/guardrails.js +382 -0
- package/lib/ai/integration.js +397 -0
- package/lib/ai/intent.js +237 -0
- package/lib/ai/manualPromise.js +111 -0
- package/lib/ai/memory.js +273 -0
- package/lib/ai/ml-scorer.js +265 -0
- package/lib/ai/orchestrator-tools.js +292 -0
- package/lib/ai/orchestrator.js +473 -0
- package/lib/ai/planner.js +300 -0
- package/lib/ai/reporter.js +493 -0
- package/lib/ai/workflow.js +407 -0
- package/lib/auth/apiKeyAuth.js +46 -0
- package/lib/auth/entraAuth.js +110 -0
- package/lib/auth/entraJwtVerifier.js +117 -0
- package/lib/auth/index.js +210 -0
- package/lib/auth/managedIdentityAuth.js +175 -0
- package/lib/auth/mcpOAuthProvider.js +186 -0
- package/lib/auth/tunnelAuth.js +120 -0
- package/lib/browserContextFactory.js +1 -1
- package/lib/browserServer.js +1 -1
- package/lib/cdpRelay.js +2 -2
- package/lib/common.js +68 -0
- package/lib/config.js +62 -3
- package/lib/connection.js +1 -1
- package/lib/context.js +1 -1
- package/lib/fileUtils.js +1 -1
- package/lib/guardrails.js +382 -0
- package/lib/health.js +178 -0
- package/lib/httpServer.js +1 -1
- package/lib/index.js +1 -1
- package/lib/javascript.js +1 -1
- package/lib/manualPromise.js +1 -1
- package/lib/memory.js +273 -0
- package/lib/openapi.js +373 -0
- package/lib/orchestrator.js +473 -0
- package/lib/package.js +1 -1
- package/lib/pageSnapshot.js +17 -2
- package/lib/planner.js +302 -0
- package/lib/program.js +17 -5
- package/lib/reporter.js +493 -0
- package/lib/resources/resource.js +1 -1
- package/lib/server.js +5 -3
- package/lib/tab.js +1 -1
- package/lib/tools/ai-native.js +298 -0
- package/lib/tools/autonomous.js +147 -0
- package/lib/tools/clock.js +183 -0
- package/lib/tools/common.js +1 -1
- package/lib/tools/console.js +1 -1
- package/lib/tools/diagnostics.js +132 -0
- package/lib/tools/dialogs.js +1 -1
- package/lib/tools/emulation.js +155 -0
- package/lib/tools/files.js +1 -1
- package/lib/tools/install.js +1 -1
- package/lib/tools/keyboard.js +1 -1
- package/lib/tools/navigate.js +1 -1
- package/lib/tools/network.js +1 -1
- package/lib/tools/pageSnapshot.js +58 -0
- package/lib/tools/pdf.js +1 -1
- package/lib/tools/profiles.js +76 -25
- package/lib/tools/screenshot.js +1 -1
- package/lib/tools/scroll.js +93 -0
- package/lib/tools/snapshot.js +1 -1
- package/lib/tools/storage.js +328 -0
- package/lib/tools/tab.js +16 -0
- package/lib/tools/tabs.js +1 -1
- package/lib/tools/testing.js +1 -1
- package/lib/tools/tool.js +1 -1
- package/lib/tools/utils.js +1 -1
- package/lib/tools/vision.js +1 -1
- package/lib/tools/wait.js +1 -1
- package/lib/tools.js +22 -1
- package/lib/transport.js +251 -31
- package/package.json +54 -21
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) DarbotLabs.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { createEntraIDAuthenticator } from './entraAuth.js';
|
|
17
|
+
import { createApiKeyAuthenticatorFromEnv } from './apiKeyAuth.js';
|
|
18
|
+
import { createDevTunnelAuthenticator } from './tunnelAuth.js';
|
|
19
|
+
import { initializeManagedIdentityAuth, createManagedIdentityConfig, } from './managedIdentityAuth.js';
|
|
20
|
+
/**
|
|
21
|
+
* Unified Authenticator that combines all auth methods
|
|
22
|
+
*/
|
|
23
|
+
export class UnifiedAuthenticator {
|
|
24
|
+
config;
|
|
25
|
+
entraAuth;
|
|
26
|
+
apiKeyAuth;
|
|
27
|
+
tunnelAuth;
|
|
28
|
+
managedIdentityConfig;
|
|
29
|
+
managedIdentityInitialized = false;
|
|
30
|
+
constructor(config) {
|
|
31
|
+
// Build configuration from environment and overrides
|
|
32
|
+
this.config = {
|
|
33
|
+
allowAnonymous: process.env.ALLOW_ANONYMOUS_ACCESS === 'true',
|
|
34
|
+
enableApiKey: process.env.API_KEY_AUTH_ENABLED === 'true',
|
|
35
|
+
enableEntra: process.env.ENTRA_AUTH_ENABLED === 'true',
|
|
36
|
+
enableTunnel: process.env.TUNNEL_AUTH_ENABLED === 'true' ||
|
|
37
|
+
!!process.env.TUNNEL_URL ||
|
|
38
|
+
!!process.env.CODESPACE_NAME,
|
|
39
|
+
enableManagedIdentity: process.env.MANAGED_IDENTITY_ENABLED === 'true' ||
|
|
40
|
+
process.env.AZURE_USE_MANAGED_IDENTITY === 'true' ||
|
|
41
|
+
!!process.env.IDENTITY_ENDPOINT,
|
|
42
|
+
requiredRoles: process.env.REQUIRED_ROLES?.split(',').map(r => r.trim()),
|
|
43
|
+
...config,
|
|
44
|
+
};
|
|
45
|
+
this.entraAuth = createEntraIDAuthenticator();
|
|
46
|
+
this.apiKeyAuth = createApiKeyAuthenticatorFromEnv();
|
|
47
|
+
this.tunnelAuth = createDevTunnelAuthenticator();
|
|
48
|
+
this.managedIdentityConfig = createManagedIdentityConfig();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Initialize async authentication providers
|
|
52
|
+
*/
|
|
53
|
+
async initialize() {
|
|
54
|
+
// Initialize Managed Identity if enabled
|
|
55
|
+
if (this.config.enableManagedIdentity && !this.managedIdentityInitialized) {
|
|
56
|
+
this.managedIdentityInitialized = await initializeManagedIdentityAuth();
|
|
57
|
+
if (this.managedIdentityInitialized) {
|
|
58
|
+
// Reload Entra auth with secrets from Key Vault
|
|
59
|
+
this.entraAuth = createEntraIDAuthenticator();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if any authentication is enabled
|
|
65
|
+
*/
|
|
66
|
+
isAuthEnabled() {
|
|
67
|
+
return this.config.enableApiKey ||
|
|
68
|
+
this.config.enableEntra ||
|
|
69
|
+
this.config.enableTunnel ||
|
|
70
|
+
this.config.enableManagedIdentity;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if a user has the required roles
|
|
74
|
+
*/
|
|
75
|
+
hasRequiredRoles(user) {
|
|
76
|
+
if (!this.config.requiredRoles || this.config.requiredRoles.length === 0)
|
|
77
|
+
return true;
|
|
78
|
+
return this.config.requiredRoles.some(role => user.roles.includes(role));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Authenticate a request using all available methods
|
|
82
|
+
*/
|
|
83
|
+
async authenticate(req) {
|
|
84
|
+
// If no auth is enabled and anonymous access is allowed
|
|
85
|
+
if (!this.isAuthEnabled() || this.config.allowAnonymous) {
|
|
86
|
+
return {
|
|
87
|
+
authenticated: true,
|
|
88
|
+
method: 'anonymous',
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// 1. Check dev tunnel first (highest priority - trusted transport)
|
|
92
|
+
if (this.config.enableTunnel && this.tunnelAuth.isTunnelRequest(req)) {
|
|
93
|
+
const tunnelResult = await this.tunnelAuth.authenticate(req);
|
|
94
|
+
if (tunnelResult.authenticated) {
|
|
95
|
+
return {
|
|
96
|
+
authenticated: true,
|
|
97
|
+
method: 'tunnel',
|
|
98
|
+
tunnel: tunnelResult,
|
|
99
|
+
user: {
|
|
100
|
+
userId: tunnelResult.githubUser || 'tunnel-user',
|
|
101
|
+
tenantId: 'github',
|
|
102
|
+
roles: ['user'],
|
|
103
|
+
permissions: ['browser:read', 'browser:write'],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// 2. Check Entra ID Bearer token
|
|
109
|
+
if (this.config.enableEntra) {
|
|
110
|
+
const user = await this.entraAuth.authenticate(req);
|
|
111
|
+
if (user) {
|
|
112
|
+
// Check RBAC roles if configured
|
|
113
|
+
if (!this.hasRequiredRoles(user)) {
|
|
114
|
+
return {
|
|
115
|
+
authenticated: false,
|
|
116
|
+
method: 'none',
|
|
117
|
+
error: 'Insufficient permissions. Required roles: ' + this.config.requiredRoles?.join(', '),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
authenticated: true,
|
|
122
|
+
method: 'entra',
|
|
123
|
+
user,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 3. Check API Key (legacy)
|
|
128
|
+
if (this.config.enableApiKey && this.apiKeyAuth.authenticate(req)) {
|
|
129
|
+
return {
|
|
130
|
+
authenticated: true,
|
|
131
|
+
method: 'api-key',
|
|
132
|
+
user: {
|
|
133
|
+
userId: 'api-key-user',
|
|
134
|
+
tenantId: 'local',
|
|
135
|
+
roles: ['user'],
|
|
136
|
+
permissions: ['browser:read', 'browser:write'],
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// 4. Check Managed Identity context
|
|
141
|
+
// This is for Azure-to-Azure calls where the caller is another Azure service
|
|
142
|
+
if (this.config.enableManagedIdentity && this.managedIdentityInitialized) {
|
|
143
|
+
// For Azure service-to-service, the token would come in the Bearer header
|
|
144
|
+
// and be validated through the Entra flow above. This is a fallback
|
|
145
|
+
// for when we're making outbound calls.
|
|
146
|
+
return {
|
|
147
|
+
authenticated: false,
|
|
148
|
+
method: 'none',
|
|
149
|
+
error: 'Managed Identity is configured but no valid token provided',
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
authenticated: false,
|
|
154
|
+
method: 'none',
|
|
155
|
+
error: 'No valid authentication provided',
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Express-style middleware
|
|
160
|
+
*/
|
|
161
|
+
middleware() {
|
|
162
|
+
return async (req, res, next) => {
|
|
163
|
+
const result = await this.authenticate(req);
|
|
164
|
+
if (!result.authenticated) {
|
|
165
|
+
res.statusCode = 401;
|
|
166
|
+
res.setHeader('Content-Type', 'application/json');
|
|
167
|
+
res.end(JSON.stringify({
|
|
168
|
+
error: 'unauthorized',
|
|
169
|
+
message: result.error || 'Authentication required',
|
|
170
|
+
methods_available: this.getAvailableMethods(),
|
|
171
|
+
}));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
// Attach auth result to request
|
|
175
|
+
req.auth = result;
|
|
176
|
+
req.user = result.user;
|
|
177
|
+
next();
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Get list of available authentication methods
|
|
182
|
+
*/
|
|
183
|
+
getAvailableMethods() {
|
|
184
|
+
const methods = [];
|
|
185
|
+
if (this.config.enableTunnel)
|
|
186
|
+
methods.push('VS Code Dev Tunnel');
|
|
187
|
+
if (this.config.enableEntra)
|
|
188
|
+
methods.push('Entra ID Bearer Token');
|
|
189
|
+
if (this.config.enableApiKey)
|
|
190
|
+
methods.push('API Key (X-API-Key header)');
|
|
191
|
+
if (this.config.enableManagedIdentity)
|
|
192
|
+
methods.push('Azure Managed Identity');
|
|
193
|
+
if (this.config.allowAnonymous)
|
|
194
|
+
methods.push('Anonymous');
|
|
195
|
+
return methods;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Create a unified authenticator from environment
|
|
200
|
+
*/
|
|
201
|
+
export function createUnifiedAuthenticator(config) {
|
|
202
|
+
return new UnifiedAuthenticator(config);
|
|
203
|
+
}
|
|
204
|
+
// Re-export all auth modules
|
|
205
|
+
export * from './entraAuth.js';
|
|
206
|
+
export * from './apiKeyAuth.js';
|
|
207
|
+
export * from './tunnelAuth.js';
|
|
208
|
+
export * from './managedIdentityAuth.js';
|
|
209
|
+
export * from './mcpOAuthProvider.js';
|
|
210
|
+
export * from './entraJwtVerifier.js';
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) DarbotLabs.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Azure Managed Identity Authentication
|
|
18
|
+
*
|
|
19
|
+
* Enables automatic authentication for Azure services using Managed Identity.
|
|
20
|
+
* This eliminates the need for API keys or secrets when running in Azure.
|
|
21
|
+
*
|
|
22
|
+
* Supports:
|
|
23
|
+
* - System-assigned Managed Identity
|
|
24
|
+
* - User-assigned Managed Identity
|
|
25
|
+
* - Azure Key Vault secret retrieval
|
|
26
|
+
*/
|
|
27
|
+
import { DefaultAzureCredential, ManagedIdentityCredential } from '@azure/identity';
|
|
28
|
+
import { SecretClient } from '@azure/keyvault-secrets';
|
|
29
|
+
// Cached credential instance
|
|
30
|
+
let cachedCredential = null;
|
|
31
|
+
let cachedSecretClient = null;
|
|
32
|
+
/**
|
|
33
|
+
* Get Azure credential for Managed Identity
|
|
34
|
+
*/
|
|
35
|
+
export function getManagedIdentityCredential(config) {
|
|
36
|
+
if (cachedCredential)
|
|
37
|
+
return cachedCredential;
|
|
38
|
+
if (config.userAssignedClientId) {
|
|
39
|
+
// User-assigned managed identity
|
|
40
|
+
cachedCredential = new ManagedIdentityCredential(config.userAssignedClientId);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
// System-assigned or default credential chain
|
|
44
|
+
cachedCredential = new DefaultAzureCredential();
|
|
45
|
+
}
|
|
46
|
+
return cachedCredential;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Verify that Managed Identity is available and working
|
|
50
|
+
*/
|
|
51
|
+
export async function verifyManagedIdentity(config) {
|
|
52
|
+
if (!config.enabled) {
|
|
53
|
+
return {
|
|
54
|
+
authenticated: false,
|
|
55
|
+
identityType: 'none',
|
|
56
|
+
error: 'Managed Identity is not enabled',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const credential = getManagedIdentityCredential(config);
|
|
61
|
+
// Try to get a token for Azure Resource Manager to verify identity works
|
|
62
|
+
const token = await credential.getToken('https://management.azure.com/.default');
|
|
63
|
+
if (token) {
|
|
64
|
+
return {
|
|
65
|
+
authenticated: true,
|
|
66
|
+
identityType: config.userAssignedClientId ? 'user-assigned' : 'system',
|
|
67
|
+
clientId: config.userAssignedClientId,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
authenticated: false,
|
|
72
|
+
identityType: 'none',
|
|
73
|
+
error: 'Failed to acquire token',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return {
|
|
78
|
+
authenticated: false,
|
|
79
|
+
identityType: 'none',
|
|
80
|
+
error: error.message || 'Managed Identity authentication failed',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get a secret from Azure Key Vault using Managed Identity
|
|
86
|
+
*/
|
|
87
|
+
export async function getKeyVaultSecret(secretName, config) {
|
|
88
|
+
if (!config.keyVaultUrl) {
|
|
89
|
+
// eslint-disable-next-line no-console
|
|
90
|
+
console.error('[ManagedIdentity] Key Vault URL not configured');
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
if (!cachedSecretClient) {
|
|
95
|
+
const credential = getManagedIdentityCredential(config);
|
|
96
|
+
cachedSecretClient = new SecretClient(config.keyVaultUrl, credential);
|
|
97
|
+
}
|
|
98
|
+
const secret = await cachedSecretClient.getSecret(secretName);
|
|
99
|
+
return secret.value || null;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// eslint-disable-next-line no-console
|
|
103
|
+
console.error(`[ManagedIdentity] Failed to get secret '${secretName}':`, error.message);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Load configuration from Key Vault secrets
|
|
109
|
+
*/
|
|
110
|
+
export async function loadSecretsFromKeyVault(config) {
|
|
111
|
+
const secrets = {};
|
|
112
|
+
if (!config.keyVaultUrl || !config.enabled)
|
|
113
|
+
return secrets;
|
|
114
|
+
// Standard secret names to load
|
|
115
|
+
const secretNames = [
|
|
116
|
+
'AZURE-TENANT-ID',
|
|
117
|
+
'AZURE-CLIENT-ID',
|
|
118
|
+
'AZURE-CLIENT-SECRET',
|
|
119
|
+
];
|
|
120
|
+
for (const secretName of secretNames) {
|
|
121
|
+
const value = await getKeyVaultSecret(secretName, config);
|
|
122
|
+
if (value) {
|
|
123
|
+
// Convert Key Vault naming (AZURE-TENANT-ID) to env var naming (AZURE_TENANT_ID)
|
|
124
|
+
const envName = secretName.replace(/-/g, '_');
|
|
125
|
+
secrets[envName] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return secrets;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create Managed Identity configuration from environment
|
|
132
|
+
*/
|
|
133
|
+
export function createManagedIdentityConfig() {
|
|
134
|
+
return {
|
|
135
|
+
enabled: process.env.MANAGED_IDENTITY_ENABLED === 'true' ||
|
|
136
|
+
process.env.AZURE_USE_MANAGED_IDENTITY === 'true' ||
|
|
137
|
+
// Auto-detect Azure environment
|
|
138
|
+
!!process.env.IDENTITY_ENDPOINT,
|
|
139
|
+
userAssignedClientId: process.env.AZURE_CLIENT_ID_MANAGED_IDENTITY,
|
|
140
|
+
keyVaultUrl: process.env.AZURE_KEY_VAULT_URL || process.env.KEY_VAULT_URL,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Initialize authentication using Managed Identity
|
|
145
|
+
* Loads secrets from Key Vault and sets environment variables
|
|
146
|
+
*/
|
|
147
|
+
export async function initializeManagedIdentityAuth() {
|
|
148
|
+
const config = createManagedIdentityConfig();
|
|
149
|
+
if (!config.enabled) {
|
|
150
|
+
// eslint-disable-next-line no-console
|
|
151
|
+
console.error('[ManagedIdentity] Not enabled or not running in Azure');
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
// Verify Managed Identity is working
|
|
155
|
+
const verifyResult = await verifyManagedIdentity(config);
|
|
156
|
+
if (!verifyResult.authenticated) {
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.error('[ManagedIdentity] Verification failed:', verifyResult.error);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
// eslint-disable-next-line no-console
|
|
162
|
+
console.error(`[ManagedIdentity] Authenticated using ${verifyResult.identityType} identity`);
|
|
163
|
+
// Load secrets from Key Vault if configured
|
|
164
|
+
if (config.keyVaultUrl) {
|
|
165
|
+
const secrets = await loadSecretsFromKeyVault(config);
|
|
166
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
167
|
+
if (!process.env[key]) {
|
|
168
|
+
process.env[key] = value;
|
|
169
|
+
// eslint-disable-next-line no-console
|
|
170
|
+
console.error(`[ManagedIdentity] Loaded ${key} from Key Vault`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) DarbotLabs.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* MCP OAuth Provider using Entra ID as the upstream authorization server.
|
|
18
|
+
*
|
|
19
|
+
* This implements the MCP OAuth protocol by proxying to Microsoft Entra ID,
|
|
20
|
+
* enabling VS Code's MCP client to authenticate users through standard OAuth flow.
|
|
21
|
+
*
|
|
22
|
+
* Supports Dynamic Client Registration (RFC 7591) for VS Code compatibility.
|
|
23
|
+
*/
|
|
24
|
+
import crypto from 'node:crypto';
|
|
25
|
+
import { ProxyOAuthServerProvider } from '@modelcontextprotocol/sdk/server/auth/providers/proxyProvider.js';
|
|
26
|
+
import { verifyEntraJwt } from './entraJwtVerifier.js';
|
|
27
|
+
/**
|
|
28
|
+
* Get Entra ID OAuth endpoints for a given tenant
|
|
29
|
+
*/
|
|
30
|
+
function getEntraEndpoints(tenantId) {
|
|
31
|
+
const authority = `https://login.microsoftonline.com/${tenantId}`;
|
|
32
|
+
return {
|
|
33
|
+
authorizationUrl: `${authority}/oauth2/v2.0/authorize`,
|
|
34
|
+
tokenUrl: `${authority}/oauth2/v2.0/token`,
|
|
35
|
+
// We handle dynamic client registration ourselves
|
|
36
|
+
registrationUrl: undefined,
|
|
37
|
+
// Entra doesn't have a standard revocation endpoint
|
|
38
|
+
revocationUrl: undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Verify an Entra ID access token and return AuthInfo
|
|
43
|
+
*/
|
|
44
|
+
async function verifyEntraToken(token, config) {
|
|
45
|
+
const payload = await verifyEntraJwt(token, {
|
|
46
|
+
tenantId: config.tenantId,
|
|
47
|
+
clientId: config.clientId,
|
|
48
|
+
clientSecret: config.clientSecret,
|
|
49
|
+
});
|
|
50
|
+
return {
|
|
51
|
+
token,
|
|
52
|
+
clientId: config.clientId,
|
|
53
|
+
scopes: payload.scp?.split(' ') || [],
|
|
54
|
+
expiresAt: payload.exp ? payload.exp * 1000 : undefined,
|
|
55
|
+
// Additional user info from token
|
|
56
|
+
extra: {
|
|
57
|
+
sub: payload.sub,
|
|
58
|
+
oid: payload.oid,
|
|
59
|
+
tid: payload.tid,
|
|
60
|
+
roles: payload.roles,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* In-memory OAuth clients store with dynamic registration support.
|
|
66
|
+
* This allows VS Code's MCP client to register automatically.
|
|
67
|
+
*/
|
|
68
|
+
class DynamicClientsStore {
|
|
69
|
+
clients = new Map();
|
|
70
|
+
config;
|
|
71
|
+
constructor(config) {
|
|
72
|
+
this.config = config;
|
|
73
|
+
// Pre-register our own Azure AD app as a known client
|
|
74
|
+
this.registerStaticClient(config);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Register the main Azure AD app as a static client
|
|
78
|
+
*/
|
|
79
|
+
registerStaticClient(config) {
|
|
80
|
+
this.clients.set(config.clientId, {
|
|
81
|
+
client_id: config.clientId,
|
|
82
|
+
client_secret: config.clientSecret,
|
|
83
|
+
redirect_uris: [
|
|
84
|
+
// VS Code localhost redirect (accepts any port)
|
|
85
|
+
'http://127.0.0.1/callback',
|
|
86
|
+
// VS Code web redirect
|
|
87
|
+
'https://vscode.dev/redirect',
|
|
88
|
+
// Azure AD redirect
|
|
89
|
+
`${config.serverBaseUrl}/auth/callback`,
|
|
90
|
+
],
|
|
91
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
92
|
+
response_types: ['code'],
|
|
93
|
+
token_endpoint_auth_method: 'client_secret_post',
|
|
94
|
+
client_name: 'Darbot Browser MCP',
|
|
95
|
+
scope: 'openid profile email User.Read',
|
|
96
|
+
client_id_issued_at: Math.floor(Date.now() / 1000),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get a registered client by ID
|
|
101
|
+
*/
|
|
102
|
+
async getClient(clientId) {
|
|
103
|
+
return this.clients.get(clientId);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Dynamic Client Registration (RFC 7591)
|
|
107
|
+
* VS Code's MCP client will call this to register itself automatically.
|
|
108
|
+
* We create a client that proxies to our Entra ID app.
|
|
109
|
+
*/
|
|
110
|
+
async registerClient(clientMetadata) {
|
|
111
|
+
// Generate a unique client ID for this dynamically registered client
|
|
112
|
+
const clientId = `vscode-mcp-${crypto.randomUUID()}`;
|
|
113
|
+
const clientIdIssuedAt = Math.floor(Date.now() / 1000);
|
|
114
|
+
// For dynamically registered clients, we use our Entra app's credentials
|
|
115
|
+
// This is safe because the OAuth flow still goes through Entra ID validation
|
|
116
|
+
const registeredClient = {
|
|
117
|
+
...clientMetadata,
|
|
118
|
+
client_id: clientId,
|
|
119
|
+
client_id_issued_at: clientIdIssuedAt,
|
|
120
|
+
// Use our Entra app's secret - this allows the proxy to work
|
|
121
|
+
client_secret: this.config.clientSecret,
|
|
122
|
+
// Ensure proper redirect URIs are set
|
|
123
|
+
redirect_uris: clientMetadata.redirect_uris || [
|
|
124
|
+
'http://127.0.0.1/callback',
|
|
125
|
+
'https://vscode.dev/redirect',
|
|
126
|
+
],
|
|
127
|
+
// Set default grant types if not provided
|
|
128
|
+
grant_types: clientMetadata.grant_types || ['authorization_code', 'refresh_token'],
|
|
129
|
+
response_types: clientMetadata.response_types || ['code'],
|
|
130
|
+
token_endpoint_auth_method: clientMetadata.token_endpoint_auth_method || 'client_secret_post',
|
|
131
|
+
};
|
|
132
|
+
this.clients.set(clientId, registeredClient);
|
|
133
|
+
// eslint-disable-next-line no-console
|
|
134
|
+
console.error(`[OAuth] Dynamic client registered: ${clientId} (${clientMetadata.client_name || 'unnamed'})`);
|
|
135
|
+
return registeredClient;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Create an MCP OAuth provider that proxies to Entra ID
|
|
140
|
+
* with support for Dynamic Client Registration
|
|
141
|
+
*/
|
|
142
|
+
export function createMcpOAuthProvider(config) {
|
|
143
|
+
const endpoints = getEntraEndpoints(config.tenantId);
|
|
144
|
+
const clientsStore = new DynamicClientsStore(config);
|
|
145
|
+
const options = {
|
|
146
|
+
endpoints,
|
|
147
|
+
verifyAccessToken: async (token) => verifyEntraToken(token, config),
|
|
148
|
+
getClient: async (clientId) => clientsStore.getClient(clientId),
|
|
149
|
+
};
|
|
150
|
+
const provider = new ProxyOAuthServerProvider(options);
|
|
151
|
+
// Override the clientsStore to enable dynamic registration
|
|
152
|
+
// The SDK checks clientsStore.registerClient to determine if registration is supported
|
|
153
|
+
Object.defineProperty(provider, 'clientsStore', {
|
|
154
|
+
get: () => clientsStore,
|
|
155
|
+
configurable: true,
|
|
156
|
+
});
|
|
157
|
+
// Skip local PKCE validation since Entra handles it
|
|
158
|
+
provider.skipLocalPkceValidation = true;
|
|
159
|
+
return provider;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if OAuth is properly configured
|
|
163
|
+
*/
|
|
164
|
+
export function isOAuthConfigured() {
|
|
165
|
+
return !!(process.env.AZURE_TENANT_ID &&
|
|
166
|
+
process.env.AZURE_CLIENT_ID &&
|
|
167
|
+
process.env.AZURE_CLIENT_SECRET);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get OAuth configuration from environment
|
|
171
|
+
*/
|
|
172
|
+
export function getOAuthConfig() {
|
|
173
|
+
const tenantId = process.env.AZURE_TENANT_ID;
|
|
174
|
+
const clientId = process.env.AZURE_CLIENT_ID;
|
|
175
|
+
const clientSecret = process.env.AZURE_CLIENT_SECRET;
|
|
176
|
+
const serverBaseUrl = process.env.SERVER_BASE_URL || 'https://darbot-browser-mcp.azurewebsites.net';
|
|
177
|
+
if (!tenantId || !clientId || !clientSecret) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
tenantId,
|
|
182
|
+
clientId,
|
|
183
|
+
clientSecret,
|
|
184
|
+
serverBaseUrl,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) DarbotLabs.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Check if request is coming through a VS Code dev tunnel
|
|
18
|
+
*/
|
|
19
|
+
export function isDevTunnelRequest(req, config) {
|
|
20
|
+
if (!config.enabled)
|
|
21
|
+
return false;
|
|
22
|
+
// Check X-Forwarded-Host for tunnel domain
|
|
23
|
+
const forwardedHost = req.headers['x-forwarded-host'];
|
|
24
|
+
if (forwardedHost) {
|
|
25
|
+
return config.allowedDomains.some(domain => forwardedHost.endsWith(domain));
|
|
26
|
+
}
|
|
27
|
+
// Check Host header
|
|
28
|
+
const host = req.headers.host;
|
|
29
|
+
if (host) {
|
|
30
|
+
return config.allowedDomains.some(domain => host.endsWith(domain));
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract tunnel information from request headers
|
|
36
|
+
*/
|
|
37
|
+
export function extractTunnelInfo(req) {
|
|
38
|
+
// VS Code dev tunnels add specific headers when proxying requests
|
|
39
|
+
// X-VS-Tunnel-User: GitHub username
|
|
40
|
+
// X-VS-Tunnel-Session: Session ID
|
|
41
|
+
// X-Original-URL: Original URL before tunnel
|
|
42
|
+
const tunnelUser = req.headers['x-vs-tunnel-user'];
|
|
43
|
+
const tunnelSession = req.headers['x-vs-tunnel-session'];
|
|
44
|
+
// Alternative headers that GitHub Codespaces uses
|
|
45
|
+
const codespaceUser = req.headers['x-github-user'];
|
|
46
|
+
const forwardedUser = req.headers['x-forwarded-user'];
|
|
47
|
+
const githubUser = tunnelUser || codespaceUser || forwardedUser;
|
|
48
|
+
if (githubUser) {
|
|
49
|
+
return {
|
|
50
|
+
authenticated: true,
|
|
51
|
+
githubUser,
|
|
52
|
+
tunnelId: tunnelSession,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// If we're in a tunnel but no user header, the tunnel is in public mode
|
|
56
|
+
// This is acceptable for public tunnels
|
|
57
|
+
return {
|
|
58
|
+
authenticated: true,
|
|
59
|
+
tunnelId: tunnelSession,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Authenticate a request from a dev tunnel
|
|
64
|
+
*/
|
|
65
|
+
export async function authenticateTunnelRequest(req, config) {
|
|
66
|
+
if (!isDevTunnelRequest(req, config)) {
|
|
67
|
+
return {
|
|
68
|
+
authenticated: false,
|
|
69
|
+
error: 'Not a dev tunnel request',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return extractTunnelInfo(req);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Create tunnel auth configuration from environment
|
|
76
|
+
*/
|
|
77
|
+
export function createTunnelAuthConfig() {
|
|
78
|
+
const domains = process.env.TUNNEL_ALLOWED_DOMAINS || '.devtunnels.ms,.tunnels.api.visualstudio.com,.github.dev';
|
|
79
|
+
return {
|
|
80
|
+
enabled: process.env.TUNNEL_AUTH_ENABLED === 'true' ||
|
|
81
|
+
process.env.VS_TUNNEL_ENABLED === 'true' ||
|
|
82
|
+
// Auto-detect if we're in a tunnel
|
|
83
|
+
!!process.env.TUNNEL_URL ||
|
|
84
|
+
!!process.env.CODESPACE_NAME,
|
|
85
|
+
allowedDomains: domains.split(',').map(d => d.trim()),
|
|
86
|
+
trustForwardedHeaders: process.env.TRUST_PROXY === 'true' ||
|
|
87
|
+
process.env.NODE_ENV === 'production',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Dev Tunnel Authenticator class for middleware use
|
|
92
|
+
*/
|
|
93
|
+
export class DevTunnelAuthenticator {
|
|
94
|
+
config;
|
|
95
|
+
constructor(config) {
|
|
96
|
+
const defaultConfig = createTunnelAuthConfig();
|
|
97
|
+
this.config = { ...defaultConfig, ...config };
|
|
98
|
+
}
|
|
99
|
+
get enabled() {
|
|
100
|
+
return this.config.enabled;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Check if request is from a dev tunnel
|
|
104
|
+
*/
|
|
105
|
+
isTunnelRequest(req) {
|
|
106
|
+
return isDevTunnelRequest(req, this.config);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Authenticate a tunnel request
|
|
110
|
+
*/
|
|
111
|
+
async authenticate(req) {
|
|
112
|
+
return authenticateTunnelRequest(req, this.config);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create a dev tunnel authenticator from environment
|
|
117
|
+
*/
|
|
118
|
+
export function createDevTunnelAuthenticator() {
|
|
119
|
+
return new DevTunnelAuthenticator();
|
|
120
|
+
}
|