@mcp-z/client 1.1.1 → 1.2.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/README.md +5 -1
- package/dist/cjs/auth/capability-discovery.js +13 -7
- package/dist/cjs/auth/capability-discovery.js.map +1 -1
- package/dist/cjs/auth/discovery-fetch.d.cts +35 -0
- package/dist/cjs/auth/discovery-fetch.d.ts +35 -0
- package/dist/cjs/auth/discovery-fetch.js +658 -0
- package/dist/cjs/auth/discovery-fetch.js.map +1 -0
- package/dist/cjs/auth/index.d.cts +1 -0
- package/dist/cjs/auth/index.d.ts +1 -0
- package/dist/cjs/auth/index.js +7 -0
- package/dist/cjs/auth/index.js.map +1 -1
- package/dist/cjs/auth/interactive-oauth-flow.d.cts +12 -12
- package/dist/cjs/auth/interactive-oauth-flow.d.ts +12 -12
- package/dist/cjs/auth/interactive-oauth-flow.js +22 -16
- package/dist/cjs/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/cjs/auth/rfc9728-discovery.d.cts +12 -25
- package/dist/cjs/auth/rfc9728-discovery.d.ts +12 -25
- package/dist/cjs/auth/rfc9728-discovery.js +59 -48
- package/dist/cjs/auth/rfc9728-discovery.js.map +1 -1
- package/dist/cjs/auth/types.d.cts +16 -0
- package/dist/cjs/auth/types.d.ts +16 -0
- package/dist/cjs/auth/types.js.map +1 -1
- package/dist/cjs/dcr/dcr-authenticator.d.cts +3 -4
- package/dist/cjs/dcr/dcr-authenticator.d.ts +3 -4
- package/dist/cjs/dcr/dcr-authenticator.js +22 -12
- package/dist/cjs/dcr/dcr-authenticator.js.map +1 -1
- package/dist/cjs/dcr/dynamic-client-registrar.d.cts +6 -17
- package/dist/cjs/dcr/dynamic-client-registrar.d.ts +6 -17
- package/dist/cjs/dcr/dynamic-client-registrar.js +10 -16
- package/dist/cjs/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/cjs/index.d.cts +1 -0
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +7 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/auth/capability-discovery.js +12 -6
- package/dist/esm/auth/capability-discovery.js.map +1 -1
- package/dist/esm/auth/discovery-fetch.d.ts +35 -0
- package/dist/esm/auth/discovery-fetch.js +184 -0
- package/dist/esm/auth/discovery-fetch.js.map +1 -0
- package/dist/esm/auth/index.d.ts +1 -0
- package/dist/esm/auth/index.js +1 -0
- package/dist/esm/auth/index.js.map +1 -1
- package/dist/esm/auth/interactive-oauth-flow.d.ts +12 -12
- package/dist/esm/auth/interactive-oauth-flow.js +25 -16
- package/dist/esm/auth/interactive-oauth-flow.js.map +1 -1
- package/dist/esm/auth/rfc9728-discovery.d.ts +12 -25
- package/dist/esm/auth/rfc9728-discovery.js +49 -56
- package/dist/esm/auth/rfc9728-discovery.js.map +1 -1
- package/dist/esm/auth/types.d.ts +16 -0
- package/dist/esm/auth/types.js.map +1 -1
- package/dist/esm/dcr/dcr-authenticator.d.ts +3 -4
- package/dist/esm/dcr/dcr-authenticator.js +20 -11
- package/dist/esm/dcr/dcr-authenticator.js.map +1 -1
- package/dist/esm/dcr/dynamic-client-registrar.d.ts +6 -17
- package/dist/esm/dcr/dynamic-client-registrar.js +13 -18
- package/dist/esm/dcr/dynamic-client-registrar.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/package.json +2 -7
|
@@ -2,69 +2,50 @@
|
|
|
2
2
|
* RFC 9728 Protected Resource Metadata Discovery
|
|
3
3
|
* Probes .well-known/oauth-protected-resource endpoint
|
|
4
4
|
*/ import { joinWellKnown, normalizeUrl } from '../lib/url-utils.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* @param url - Full URL that may include a path
|
|
8
|
-
* @returns Origin (e.g., "https://example.com") or original string if invalid URL
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* getOrigin('https://example.com/mcp') // → 'https://example.com'
|
|
12
|
-
* getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'
|
|
13
|
-
*/ function getOrigin(url) {
|
|
5
|
+
import { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fetch.js';
|
|
6
|
+
/** Returns `url`'s origin (protocol + host), or the original string if it doesn't parse. */ function getOrigin(url) {
|
|
14
7
|
try {
|
|
15
8
|
return new URL(url).origin;
|
|
16
9
|
} catch {
|
|
17
|
-
// Invalid URL - return as-is for graceful degradation
|
|
18
10
|
return url;
|
|
19
11
|
}
|
|
20
12
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Extract path from a URL (without origin)
|
|
23
|
-
* @param url - Full URL
|
|
24
|
-
* @returns Path component (e.g., "/mcp", "/api/v1/mcp") or empty string if no path
|
|
25
|
-
*/ function getPath(url) {
|
|
13
|
+
/** Returns `url`'s pathname, or `''` for the root path or an unparseable URL. */ function getPath(url) {
|
|
26
14
|
try {
|
|
27
15
|
const parsed = new URL(url);
|
|
28
|
-
// pathname includes leading slash, e.g., "/mcp"
|
|
29
16
|
return parsed.pathname === '/' ? '' : parsed.pathname;
|
|
30
17
|
} catch {
|
|
31
18
|
return '';
|
|
32
19
|
}
|
|
33
20
|
}
|
|
34
21
|
/**
|
|
35
|
-
*
|
|
36
|
-
|
|
37
|
-
* Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)
|
|
38
|
-
* Probes .well-known/oauth-protected-resource endpoint
|
|
22
|
+
* Discovers RFC 9728 Protected Resource Metadata: the `WWW-Authenticate`
|
|
23
|
+
* header first, then `.well-known/oauth-protected-resource` at the origin root and, for a path-prefixed resource, its sub-path variant.
|
|
39
24
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}
|
|
43
|
-
*
|
|
44
|
-
* @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)
|
|
45
|
-
* @returns ProtectedResourceMetadata if discovered, null otherwise
|
|
46
|
-
*
|
|
47
|
-
* @example
|
|
48
|
-
* // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com
|
|
49
|
-
* const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');
|
|
50
|
-
* // Returns: { resource: "https://ai.todoist.net/mcp", authorization_servers: ["https://todoist.com"] }
|
|
25
|
+
* @param resourceUrl - URL of the protected resource (e.g. `https://ai.todoist.net/mcp`).
|
|
26
|
+
* @returns Discovered metadata, or `null` if none is found.
|
|
51
27
|
*/ export async function discoverProtectedResourceMetadata(resourceUrl) {
|
|
52
28
|
try {
|
|
53
29
|
const normalizedResourceUrl = normalizeUrl(resourceUrl);
|
|
54
|
-
|
|
30
|
+
// resourceUrl is the server the caller configured (never remote-supplied);
|
|
31
|
+
// its loopback-ness is the trust signal every fetch below relies on.
|
|
32
|
+
const allowLoopback = isLoopbackUrl(normalizedResourceUrl);
|
|
33
|
+
const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl, allowLoopback);
|
|
55
34
|
if (headerMetadata) return headerMetadata;
|
|
56
35
|
// Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)
|
|
57
36
|
const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');
|
|
58
37
|
try {
|
|
59
|
-
const response = await
|
|
38
|
+
const response = await discoveryFetch(localWellKnownUrl, {
|
|
60
39
|
method: 'GET',
|
|
61
40
|
headers: {
|
|
62
41
|
Accept: 'application/json',
|
|
63
42
|
Connection: 'close'
|
|
64
43
|
}
|
|
44
|
+
}, 'protected resource metadata (path-local)', {
|
|
45
|
+
allowLoopback
|
|
65
46
|
});
|
|
66
47
|
if (response.ok) {
|
|
67
|
-
return await response
|
|
48
|
+
return await readDiscoveryJson(response, 'protected resource metadata (path-local)');
|
|
68
49
|
}
|
|
69
50
|
} catch {
|
|
70
51
|
// Continue to origin-based discovery
|
|
@@ -74,15 +55,17 @@
|
|
|
74
55
|
// Strategy 1: Try root location (REQUIRED by RFC 9728)
|
|
75
56
|
const rootUrl = `${origin}/.well-known/oauth-protected-resource`;
|
|
76
57
|
try {
|
|
77
|
-
const response = await
|
|
58
|
+
const response = await discoveryFetch(rootUrl, {
|
|
78
59
|
method: 'GET',
|
|
79
60
|
headers: {
|
|
80
61
|
Accept: 'application/json',
|
|
81
62
|
Connection: 'close'
|
|
82
63
|
}
|
|
64
|
+
}, 'protected resource metadata (root)', {
|
|
65
|
+
allowLoopback
|
|
83
66
|
});
|
|
84
67
|
if (response.ok) {
|
|
85
|
-
const metadata = await response
|
|
68
|
+
const metadata = await readDiscoveryJson(response, 'protected resource metadata (root)');
|
|
86
69
|
// Check if the discovered resource matches what we're looking for
|
|
87
70
|
if (metadata.resource === normalizedResourceUrl) {
|
|
88
71
|
return metadata;
|
|
@@ -101,15 +84,17 @@
|
|
|
101
84
|
// Try sub-path location for more specific metadata
|
|
102
85
|
const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;
|
|
103
86
|
try {
|
|
104
|
-
const subPathResponse = await
|
|
87
|
+
const subPathResponse = await discoveryFetch(subPathUrl, {
|
|
105
88
|
method: 'GET',
|
|
106
89
|
headers: {
|
|
107
90
|
Accept: 'application/json',
|
|
108
91
|
Connection: 'close'
|
|
109
92
|
}
|
|
93
|
+
}, 'protected resource metadata (sub-path)', {
|
|
94
|
+
allowLoopback
|
|
110
95
|
});
|
|
111
96
|
if (subPathResponse.ok) {
|
|
112
|
-
return await subPathResponse
|
|
97
|
+
return await readDiscoveryJson(subPathResponse, 'protected resource metadata (sub-path)');
|
|
113
98
|
}
|
|
114
99
|
} catch {
|
|
115
100
|
// Sub-path failed, use root metadata
|
|
@@ -127,15 +112,17 @@
|
|
|
127
112
|
if (path) {
|
|
128
113
|
const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;
|
|
129
114
|
try {
|
|
130
|
-
const response = await
|
|
115
|
+
const response = await discoveryFetch(subPathUrl, {
|
|
131
116
|
method: 'GET',
|
|
132
117
|
headers: {
|
|
133
118
|
Accept: 'application/json',
|
|
134
119
|
Connection: 'close'
|
|
135
120
|
}
|
|
121
|
+
}, 'protected resource metadata (sub-path)', {
|
|
122
|
+
allowLoopback
|
|
136
123
|
});
|
|
137
124
|
if (response.ok) {
|
|
138
|
-
return await response
|
|
125
|
+
return await readDiscoveryJson(response, 'protected resource metadata (sub-path)');
|
|
139
126
|
}
|
|
140
127
|
} catch {
|
|
141
128
|
// Fall through to return null
|
|
@@ -148,7 +135,7 @@
|
|
|
148
135
|
return null;
|
|
149
136
|
}
|
|
150
137
|
}
|
|
151
|
-
async function discoverProtectedResourceMetadataFromHeader(resourceUrl) {
|
|
138
|
+
async function discoverProtectedResourceMetadataFromHeader(resourceUrl, allowLoopback) {
|
|
152
139
|
try {
|
|
153
140
|
const response = await fetch(resourceUrl, {
|
|
154
141
|
method: 'GET',
|
|
@@ -173,59 +160,65 @@ async function discoverProtectedResourceMetadataFromHeader(resourceUrl) {
|
|
|
173
160
|
if (!header) return null;
|
|
174
161
|
const match = header.match(/resource_metadata="([^"]+)"/i);
|
|
175
162
|
if (!match || !match[1]) return null;
|
|
163
|
+
// metadataUrl is remote-server-chosen (the primary SSRF vector here);
|
|
164
|
+
// allowLoopback reflects trust in resourceUrl, not in metadataUrl.
|
|
176
165
|
const metadataUrl = match[1];
|
|
177
|
-
const metadataResponse = await
|
|
166
|
+
const metadataResponse = await discoveryFetch(metadataUrl, {
|
|
178
167
|
method: 'GET',
|
|
179
168
|
headers: {
|
|
180
169
|
Accept: 'application/json',
|
|
181
170
|
Connection: 'close'
|
|
182
171
|
}
|
|
172
|
+
}, 'resource_metadata URL', {
|
|
173
|
+
allowLoopback
|
|
183
174
|
});
|
|
184
175
|
if (!metadataResponse.ok) {
|
|
185
176
|
return null;
|
|
186
177
|
}
|
|
187
|
-
return await metadataResponse
|
|
178
|
+
return await readDiscoveryJson(metadataResponse, 'resource_metadata URL');
|
|
188
179
|
} catch (_error) {
|
|
189
180
|
return null;
|
|
190
181
|
}
|
|
191
182
|
}
|
|
192
183
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)
|
|
197
|
-
* @returns AuthorizationServerMetadata if discovered, null otherwise
|
|
184
|
+
* Discovers RFC 8414 Authorization Server Metadata at
|
|
185
|
+
* `.well-known/oauth-authorization-server`, path-local variant first.
|
|
198
186
|
*
|
|
199
|
-
* @
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*/ export async function discoverAuthorizationServerMetadata(authServerUrl) {
|
|
187
|
+
* @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery).
|
|
188
|
+
* @param options.allowLoopback - Loopback trust grant computed from the server the caller is actually talking to, never from `authServerUrl` itself. Defaults to `false`.
|
|
189
|
+
* @returns Discovered metadata, or `null` if none is found.
|
|
190
|
+
*/ export async function discoverAuthorizationServerMetadata(authServerUrl, options = {}) {
|
|
191
|
+
const { allowLoopback = false } = options;
|
|
203
192
|
try {
|
|
204
193
|
const normalizedAuthServerUrl = normalizeUrl(authServerUrl);
|
|
205
194
|
const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');
|
|
206
|
-
const localResponse = await
|
|
195
|
+
const localResponse = await discoveryFetch(localWellKnownUrl, {
|
|
207
196
|
method: 'GET',
|
|
208
197
|
headers: {
|
|
209
198
|
Accept: 'application/json',
|
|
210
199
|
Connection: 'close'
|
|
211
200
|
}
|
|
201
|
+
}, 'authorization server metadata (path-local)', {
|
|
202
|
+
allowLoopback
|
|
212
203
|
});
|
|
213
204
|
if (localResponse.ok) {
|
|
214
|
-
return await localResponse
|
|
205
|
+
return await readDiscoveryJson(localResponse, 'authorization server metadata (path-local)');
|
|
215
206
|
}
|
|
216
207
|
const origin = getOrigin(normalizedAuthServerUrl);
|
|
217
208
|
const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;
|
|
218
|
-
const response = await
|
|
209
|
+
const response = await discoveryFetch(wellKnownUrl, {
|
|
219
210
|
method: 'GET',
|
|
220
211
|
headers: {
|
|
221
212
|
Accept: 'application/json',
|
|
222
213
|
Connection: 'close'
|
|
223
214
|
}
|
|
215
|
+
}, 'authorization server metadata', {
|
|
216
|
+
allowLoopback
|
|
224
217
|
});
|
|
225
218
|
if (!response.ok) {
|
|
226
219
|
return null;
|
|
227
220
|
}
|
|
228
|
-
return await response
|
|
221
|
+
return await readDiscoveryJson(response, 'authorization server metadata');
|
|
229
222
|
} catch (_error) {
|
|
230
223
|
return null;
|
|
231
224
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport { joinWellKnown, normalizeUrl } from '../lib/url-utils.ts';\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/**\n * Extract origin (protocol + host) from a URL\n * @param url - Full URL that may include a path\n * @returns Origin (e.g., \"https://example.com\") or original string if invalid URL\n *\n * @example\n * getOrigin('https://example.com/mcp') // → 'https://example.com'\n * getOrigin('http://localhost:9999/api/v1/mcp') // → 'http://localhost:9999'\n */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n // Invalid URL - return as-is for graceful degradation\n return url;\n }\n}\n\n/**\n * Extract path from a URL (without origin)\n * @param url - Full URL\n * @returns Path component (e.g., \"/mcp\", \"/api/v1/mcp\") or empty string if no path\n */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n // pathname includes leading slash, e.g., \"/mcp\"\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Normalize a resource URL by stripping query/hash and trailing slashes.\n */\n/**\n * Discover OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Probes .well-known/oauth-protected-resource endpoint\n *\n * Discovery Strategy:\n * 1. Try origin root: {origin}/.well-known/oauth-protected-resource\n * 2. If 404, try sub-path: {origin}/.well-known/oauth-protected-resource{path}\n *\n * @param resourceUrl - URL of the protected resource (e.g., https://ai.todoist.net/mcp)\n * @returns ProtectedResourceMetadata if discovered, null otherwise\n *\n * @example\n * // Todoist case: MCP at ai.todoist.net/mcp, OAuth at todoist.com\n * const metadata = await discoverProtectedResourceMetadata('https://ai.todoist.net/mcp');\n * // Returns: { resource: \"https://ai.todoist.net/mcp\", authorization_servers: [\"https://todoist.com\"] }\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const normalizedResourceUrl = normalizeUrl(resourceUrl);\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl);\n if (headerMetadata) return headerMetadata;\n\n // Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)\n const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');\n try {\n const response = await fetch(localWellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Continue to origin-based discovery\n }\n\n const origin = getOrigin(normalizedResourceUrl);\n const path = getPath(normalizedResourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await fetch(rootUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n const metadata = (await response.json()) as ProtectedResourceMetadata;\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === normalizedResourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (normalizedResourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n if (subPathResponse.ok) {\n return (await subPathResponse.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await fetch(subPathUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (response.ok) {\n return (await response.json()) as ProtectedResourceMetadata;\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n let header = response.headers.get('www-authenticate');\n if (!header) {\n const postResponse = await fetch(resourceUrl, {\n method: 'POST',\n headers: { Accept: 'application/json', Connection: 'close', 'Content-Type': 'application/json' },\n body: '{}',\n });\n header = postResponse.headers.get('www-authenticate');\n }\n\n if (!header) return null;\n\n const match = header.match(/resource_metadata=\"([^\"]+)\"/i);\n if (!match || !match[1]) return null;\n\n const metadataUrl = match[1];\n const metadataResponse = await fetch(metadataUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return (await metadataResponse.json()) as ProtectedResourceMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Probes .well-known/oauth-authorization-server endpoint\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery)\n * @returns AuthorizationServerMetadata if discovered, null otherwise\n *\n * @example\n * const metadata = await discoverAuthorizationServerMetadata('https://todoist.com');\n * // Returns: { issuer: \"https://todoist.com\", authorization_endpoint: \"...\", ... }\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string): Promise<AuthorizationServerMetadata | null> {\n try {\n const normalizedAuthServerUrl = normalizeUrl(authServerUrl);\n const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');\n const localResponse = await fetch(localWellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (localResponse.ok) {\n return (await localResponse.json()) as AuthorizationServerMetadata;\n }\n\n const origin = getOrigin(normalizedAuthServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await fetch(wellKnownUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n if (!response.ok) {\n return null;\n }\n\n return (await response.json()) as AuthorizationServerMetadata;\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth Authorization Server Issuer from resource response (RFC 9207)\n *\n * @param resourceUrl - URL of the protected resource\n * @returns Issuer URL if present in WWW-Authenticate header, null otherwise\n */\nexport async function discoverAuthorizationServerIssuer(resourceUrl: string): Promise<string | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n const header = response.headers.get('www-authenticate');\n if (!header) return null;\n\n const match = header.match(/(?:authorization_server|issuer)=\"([^\"]+)\"/i);\n if (!match) return null;\n\n return match[1] ?? null;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["joinWellKnown","normalizeUrl","getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","normalizedResourceUrl","headerMetadata","discoverProtectedResourceMetadataFromHeader","localWellKnownUrl","response","fetch","method","headers","Accept","Connection","ok","json","path","rootUrl","metadata","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","header","get","postResponse","body","match","metadataUrl","metadataResponse","discoverAuthorizationServerMetadata","authServerUrl","normalizedAuthServerUrl","localResponse","wellKnownUrl","discoverAuthorizationServerIssuer"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/rfc9728-discovery.ts"],"sourcesContent":["/**\n * RFC 9728 Protected Resource Metadata Discovery\n * Probes .well-known/oauth-protected-resource endpoint\n */\n\nimport { joinWellKnown, normalizeUrl } from '../lib/url-utils.ts';\nimport { discoveryFetch, isLoopbackUrl, readDiscoveryJson } from './discovery-fetch.ts';\nimport type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types.ts';\n\n/** Returns `url`'s origin (protocol + host), or the original string if it doesn't parse. */\nfunction getOrigin(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return url;\n }\n}\n\n/** Returns `url`'s pathname, or `''` for the root path or an unparseable URL. */\nfunction getPath(url: string): string {\n try {\n const parsed = new URL(url);\n return parsed.pathname === '/' ? '' : parsed.pathname;\n } catch {\n return '';\n }\n}\n\n/**\n * Discovers RFC 9728 Protected Resource Metadata: the `WWW-Authenticate`\n * header first, then `.well-known/oauth-protected-resource` at the origin root and, for a path-prefixed resource, its sub-path variant.\n *\n * @param resourceUrl - URL of the protected resource (e.g. `https://ai.todoist.net/mcp`).\n * @returns Discovered metadata, or `null` if none is found.\n */\nexport async function discoverProtectedResourceMetadata(resourceUrl: string): Promise<ProtectedResourceMetadata | null> {\n try {\n const normalizedResourceUrl = normalizeUrl(resourceUrl);\n // resourceUrl is the server the caller configured (never remote-supplied);\n // its loopback-ness is the trust signal every fetch below relies on.\n const allowLoopback = isLoopbackUrl(normalizedResourceUrl);\n const headerMetadata = await discoverProtectedResourceMetadataFromHeader(normalizedResourceUrl, allowLoopback);\n if (headerMetadata) return headerMetadata;\n\n // Strategy 0: Try path-local well-known (supports path-prefixed deployments like /outlook)\n const localWellKnownUrl = joinWellKnown(normalizedResourceUrl, '/.well-known/oauth-protected-resource');\n try {\n const response = await discoveryFetch(\n localWellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'protected resource metadata (path-local)',\n { allowLoopback }\n );\n if (response.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (path-local)');\n }\n } catch {\n // Continue to origin-based discovery\n }\n\n const origin = getOrigin(normalizedResourceUrl);\n const path = getPath(normalizedResourceUrl);\n\n // Strategy 1: Try root location (REQUIRED by RFC 9728)\n const rootUrl = `${origin}/.well-known/oauth-protected-resource`;\n\n try {\n const response = await discoveryFetch(\n rootUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'protected resource metadata (root)',\n { allowLoopback }\n );\n\n if (response.ok) {\n const metadata = await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (root)');\n // Check if the discovered resource matches what we're looking for\n if (metadata.resource === normalizedResourceUrl) {\n return metadata;\n }\n // If there's no path component, return root metadata\n // (e.g., looking for http://example.com and found it)\n if (!path) {\n return metadata;\n }\n // If requested URL starts with metadata.resource, the root metadata applies to sub-paths\n // (e.g., looking for http://example.com/api/v1/mcp, found http://example.com)\n if (normalizedResourceUrl.startsWith(metadata.resource)) {\n // Still try sub-path location to see if there's more specific metadata\n // But save root metadata as fallback\n const rootMetadata = metadata;\n\n // Try sub-path location for more specific metadata\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n try {\n const subPathResponse = await discoveryFetch(\n subPathUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'protected resource metadata (sub-path)',\n { allowLoopback }\n );\n if (subPathResponse.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(subPathResponse, 'protected resource metadata (sub-path)');\n }\n } catch {\n // Sub-path failed, use root metadata\n }\n\n // Return root metadata as it applies to this resource\n return rootMetadata;\n }\n // Otherwise, try sub-path location before giving up\n }\n } catch {\n // Continue to sub-path location\n }\n\n // Strategy 2: Try sub-path location (MCP spec extension)\n // Only try if there's a path component\n if (path) {\n const subPathUrl = `${origin}/.well-known/oauth-protected-resource${path}`;\n\n try {\n const response = await discoveryFetch(\n subPathUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'protected resource metadata (sub-path)',\n { allowLoopback }\n );\n\n if (response.ok) {\n return await readDiscoveryJson<ProtectedResourceMetadata>(response, 'protected resource metadata (sub-path)');\n }\n } catch {\n // Fall through to return null\n }\n }\n\n // Neither location found or resource didn't match\n return null;\n } catch (_error) {\n // Network error, invalid URL, or other failure\n return null;\n }\n}\n\nasync function discoverProtectedResourceMetadataFromHeader(resourceUrl: string, allowLoopback: boolean): Promise<ProtectedResourceMetadata | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n let header = response.headers.get('www-authenticate');\n if (!header) {\n const postResponse = await fetch(resourceUrl, {\n method: 'POST',\n headers: { Accept: 'application/json', Connection: 'close', 'Content-Type': 'application/json' },\n body: '{}',\n });\n header = postResponse.headers.get('www-authenticate');\n }\n\n if (!header) return null;\n\n const match = header.match(/resource_metadata=\"([^\"]+)\"/i);\n if (!match || !match[1]) return null;\n\n // metadataUrl is remote-server-chosen (the primary SSRF vector here);\n // allowLoopback reflects trust in resourceUrl, not in metadataUrl.\n const metadataUrl = match[1];\n const metadataResponse = await discoveryFetch(\n metadataUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'resource_metadata URL',\n { allowLoopback }\n );\n\n if (!metadataResponse.ok) {\n return null;\n }\n\n return await readDiscoveryJson<ProtectedResourceMetadata>(metadataResponse, 'resource_metadata URL');\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discovers RFC 8414 Authorization Server Metadata at\n * `.well-known/oauth-authorization-server`, path-local variant first.\n *\n * @param authServerUrl - URL of the authorization server (typically from RFC 9728 discovery).\n * @param options.allowLoopback - Loopback trust grant computed from the server the caller is actually talking to, never from `authServerUrl` itself. Defaults to `false`.\n * @returns Discovered metadata, or `null` if none is found.\n */\nexport async function discoverAuthorizationServerMetadata(authServerUrl: string, options: { allowLoopback?: boolean } = {}): Promise<AuthorizationServerMetadata | null> {\n const { allowLoopback = false } = options;\n try {\n const normalizedAuthServerUrl = normalizeUrl(authServerUrl);\n const localWellKnownUrl = joinWellKnown(normalizedAuthServerUrl, '/.well-known/oauth-authorization-server');\n const localResponse = await discoveryFetch(\n localWellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'authorization server metadata (path-local)',\n { allowLoopback }\n );\n\n if (localResponse.ok) {\n return await readDiscoveryJson<AuthorizationServerMetadata>(localResponse, 'authorization server metadata (path-local)');\n }\n\n const origin = getOrigin(normalizedAuthServerUrl);\n const wellKnownUrl = `${origin}/.well-known/oauth-authorization-server`;\n\n const response = await discoveryFetch(\n wellKnownUrl,\n {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n },\n 'authorization server metadata',\n { allowLoopback }\n );\n\n if (!response.ok) {\n return null;\n }\n\n return await readDiscoveryJson<AuthorizationServerMetadata>(response, 'authorization server metadata');\n } catch (_error) {\n return null;\n }\n}\n\n/**\n * Discover OAuth Authorization Server Issuer from resource response (RFC 9207)\n *\n * @param resourceUrl - URL of the protected resource\n * @returns Issuer URL if present in WWW-Authenticate header, null otherwise\n */\nexport async function discoverAuthorizationServerIssuer(resourceUrl: string): Promise<string | null> {\n try {\n const response = await fetch(resourceUrl, {\n method: 'GET',\n headers: { Accept: 'application/json', Connection: 'close' },\n });\n\n const header = response.headers.get('www-authenticate');\n if (!header) return null;\n\n const match = header.match(/(?:authorization_server|issuer)=\"([^\"]+)\"/i);\n if (!match) return null;\n\n return match[1] ?? null;\n } catch (_error) {\n return null;\n }\n}\n"],"names":["joinWellKnown","normalizeUrl","discoveryFetch","isLoopbackUrl","readDiscoveryJson","getOrigin","url","URL","origin","getPath","parsed","pathname","discoverProtectedResourceMetadata","resourceUrl","normalizedResourceUrl","allowLoopback","headerMetadata","discoverProtectedResourceMetadataFromHeader","localWellKnownUrl","response","method","headers","Accept","Connection","ok","path","rootUrl","metadata","resource","startsWith","rootMetadata","subPathUrl","subPathResponse","_error","fetch","header","get","postResponse","body","match","metadataUrl","metadataResponse","discoverAuthorizationServerMetadata","authServerUrl","options","normalizedAuthServerUrl","localResponse","wellKnownUrl","discoverAuthorizationServerIssuer"],"mappings":"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"}
|
package/dist/esm/auth/types.d.ts
CHANGED
|
@@ -113,6 +113,14 @@ export interface DcrRegistrationOptions {
|
|
|
113
113
|
clientName?: string;
|
|
114
114
|
/** Redirect URI for OAuth callback */
|
|
115
115
|
redirectUri?: string;
|
|
116
|
+
/**
|
|
117
|
+
* Loopback trust grant for the registration_endpoint fetch (SSRF
|
|
118
|
+
* mitigation - see `src/auth/discovery-fetch.ts`). Compute this from the
|
|
119
|
+
* MCP server the caller is actually talking to, never from
|
|
120
|
+
* `registrationEndpoint` itself (which is typically sourced from
|
|
121
|
+
* remote-controlled AS metadata). Defaults to `false`.
|
|
122
|
+
*/
|
|
123
|
+
allowLoopback?: boolean;
|
|
116
124
|
}
|
|
117
125
|
/**
|
|
118
126
|
* Options for OAuth authorization flow
|
|
@@ -134,4 +142,12 @@ export interface OAuthFlowOptions {
|
|
|
134
142
|
timeout?: number;
|
|
135
143
|
/** Optional logger for debug output (defaults to singleton logger) */
|
|
136
144
|
logger?: import('../utils/logger.js').Logger;
|
|
145
|
+
/**
|
|
146
|
+
* Loopback trust grant for the token endpoint fetch (SSRF mitigation - see
|
|
147
|
+
* `src/auth/discovery-fetch.ts`). Compute this from the MCP server the
|
|
148
|
+
* caller is actually talking to, never from `tokenEndpoint` itself (which
|
|
149
|
+
* is typically sourced from remote-controlled AS metadata). Defaults to
|
|
150
|
+
* `false`.
|
|
151
|
+
*/
|
|
152
|
+
allowLoopback?: boolean;
|
|
137
153
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/types.ts"],"sourcesContent":["/**\n * Shared types for OAuth and DCR authentication\n */\n\n/**\n * OAuth callback result from authorization server\n */\nexport interface CallbackResult {\n /** Authorization code from OAuth server */\n code: string;\n /** State parameter for CSRF protection */\n state?: string;\n}\n\n/**\n * PKCE (Proof Key for Code Exchange) parameters (RFC 7636)\n * Used to secure OAuth 2.0 authorization code flow for public clients\n */\nexport interface PkceParams {\n /** Code verifier - cryptographically random string (43-128 characters) */\n codeVerifier: string;\n /** Code challenge - derived from code verifier using challenge method */\n codeChallenge: string;\n /** Code challenge method - S256 (SHA-256) or plain */\n codeChallengeMethod: 'S256' | 'plain';\n}\n\n/**\n * OAuth token set with access and refresh tokens\n */\nexport interface TokenSet {\n /** Access token for API requests */\n accessToken: string;\n /** Refresh token for obtaining new access tokens */\n refreshToken: string;\n /** Timestamp when access token expires (milliseconds since epoch) */\n expiresAt: number;\n /** Scopes granted for this token set */\n scopes?: string[];\n /** Client ID used for DCR registration (stored for future use) */\n clientId?: string;\n /** Client secret used for DCR registration (stored for future use) */\n clientSecret?: string;\n}\n\n/**\n * OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Response from .well-known/oauth-protected-resource endpoint\n */\nexport interface ProtectedResourceMetadata {\n /** The protected resource identifier */\n resource: string;\n /** List of authorization server URLs that can issue tokens for this resource */\n authorization_servers: string[];\n /** Optional list of scopes supported by this resource */\n scopes_supported?: string[];\n /** Optional list of bearer token methods supported (header, query, body) */\n bearer_methods_supported?: string[];\n}\n\n/**\n * OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Response from .well-known/oauth-authorization-server endpoint\n */\nexport interface AuthorizationServerMetadata {\n /** The authorization server's issuer identifier */\n issuer?: string;\n /** URL of the authorization endpoint */\n authorization_endpoint?: string;\n /** URL of the token endpoint */\n token_endpoint?: string;\n /** URL of the client registration endpoint (DCR - RFC 7591) */\n registration_endpoint?: string;\n /** URL of the token introspection endpoint */\n introspection_endpoint?: string;\n /** List of OAuth scopes supported by the authorization server */\n scopes_supported?: string[];\n /** Response types supported (code, token, etc.) */\n response_types_supported?: string[];\n /** Grant types supported (authorization_code, refresh_token, etc.) */\n grant_types_supported?: string[];\n /** Token endpoint authentication methods supported */\n token_endpoint_auth_methods_supported?: string[];\n}\n\n/**\n * OAuth server capabilities discovered from .well-known endpoint\n */\nexport interface AuthCapabilities {\n /** Whether the server supports Dynamic Client Registration (RFC 7591) */\n supportsDcr: boolean;\n /** DCR client registration endpoint */\n registrationEndpoint?: string;\n /** OAuth authorization endpoint */\n authorizationEndpoint?: string;\n /** OAuth token endpoint */\n tokenEndpoint?: string;\n /** Token introspection endpoint */\n introspectionEndpoint?: string;\n /** Supported OAuth scopes */\n scopes?: string[];\n}\n\n/**\n * Client credentials from DCR registration\n */\nexport interface ClientCredentials {\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Timestamp when client was registered */\n issuedAt?: number;\n}\n\n/**\n * Options for DCR client registration\n */\nexport interface DcrRegistrationOptions {\n /** Client name to register */\n clientName?: string;\n /** Redirect URI for OAuth callback */\n redirectUri?: string;\n}\n\n/**\n * Options for OAuth authorization flow\n */\nexport interface OAuthFlowOptions {\n /** Port for OAuth callback listener (required - use get-port to find available port) */\n port: number;\n /** Redirect URI for OAuth callback (optional - will be built from port if not provided) */\n redirectUri?: string;\n /** OAuth scopes to request */\n scopes?: string[];\n /** Resource parameter (RFC 8707) - target resource server identifier */\n resource?: string;\n /** Enable PKCE (RFC 7636) - recommended for all clients, required for public clients */\n pkce?: boolean;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Timeout for callback (milliseconds) */\n timeout?: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: import('../utils/logger.ts').Logger;\n}\n"],"names":[],"mappings":"AAAA;;CAEC,GAED;;CAEC,
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/auth/types.ts"],"sourcesContent":["/**\n * Shared types for OAuth and DCR authentication\n */\n\n/**\n * OAuth callback result from authorization server\n */\nexport interface CallbackResult {\n /** Authorization code from OAuth server */\n code: string;\n /** State parameter for CSRF protection */\n state?: string;\n}\n\n/**\n * PKCE (Proof Key for Code Exchange) parameters (RFC 7636)\n * Used to secure OAuth 2.0 authorization code flow for public clients\n */\nexport interface PkceParams {\n /** Code verifier - cryptographically random string (43-128 characters) */\n codeVerifier: string;\n /** Code challenge - derived from code verifier using challenge method */\n codeChallenge: string;\n /** Code challenge method - S256 (SHA-256) or plain */\n codeChallengeMethod: 'S256' | 'plain';\n}\n\n/**\n * OAuth token set with access and refresh tokens\n */\nexport interface TokenSet {\n /** Access token for API requests */\n accessToken: string;\n /** Refresh token for obtaining new access tokens */\n refreshToken: string;\n /** Timestamp when access token expires (milliseconds since epoch) */\n expiresAt: number;\n /** Scopes granted for this token set */\n scopes?: string[];\n /** Client ID used for DCR registration (stored for future use) */\n clientId?: string;\n /** Client secret used for DCR registration (stored for future use) */\n clientSecret?: string;\n}\n\n/**\n * OAuth 2.0 Protected Resource Metadata (RFC 9728)\n * Response from .well-known/oauth-protected-resource endpoint\n */\nexport interface ProtectedResourceMetadata {\n /** The protected resource identifier */\n resource: string;\n /** List of authorization server URLs that can issue tokens for this resource */\n authorization_servers: string[];\n /** Optional list of scopes supported by this resource */\n scopes_supported?: string[];\n /** Optional list of bearer token methods supported (header, query, body) */\n bearer_methods_supported?: string[];\n}\n\n/**\n * OAuth 2.0 Authorization Server Metadata (RFC 8414)\n * Response from .well-known/oauth-authorization-server endpoint\n */\nexport interface AuthorizationServerMetadata {\n /** The authorization server's issuer identifier */\n issuer?: string;\n /** URL of the authorization endpoint */\n authorization_endpoint?: string;\n /** URL of the token endpoint */\n token_endpoint?: string;\n /** URL of the client registration endpoint (DCR - RFC 7591) */\n registration_endpoint?: string;\n /** URL of the token introspection endpoint */\n introspection_endpoint?: string;\n /** List of OAuth scopes supported by the authorization server */\n scopes_supported?: string[];\n /** Response types supported (code, token, etc.) */\n response_types_supported?: string[];\n /** Grant types supported (authorization_code, refresh_token, etc.) */\n grant_types_supported?: string[];\n /** Token endpoint authentication methods supported */\n token_endpoint_auth_methods_supported?: string[];\n}\n\n/**\n * OAuth server capabilities discovered from .well-known endpoint\n */\nexport interface AuthCapabilities {\n /** Whether the server supports Dynamic Client Registration (RFC 7591) */\n supportsDcr: boolean;\n /** DCR client registration endpoint */\n registrationEndpoint?: string;\n /** OAuth authorization endpoint */\n authorizationEndpoint?: string;\n /** OAuth token endpoint */\n tokenEndpoint?: string;\n /** Token introspection endpoint */\n introspectionEndpoint?: string;\n /** Supported OAuth scopes */\n scopes?: string[];\n}\n\n/**\n * Client credentials from DCR registration\n */\nexport interface ClientCredentials {\n /** OAuth client ID */\n clientId: string;\n /** OAuth client secret */\n clientSecret: string;\n /** Timestamp when client was registered */\n issuedAt?: number;\n}\n\n/**\n * Options for DCR client registration\n */\nexport interface DcrRegistrationOptions {\n /** Client name to register */\n clientName?: string;\n /** Redirect URI for OAuth callback */\n redirectUri?: string;\n /**\n * Loopback trust grant for the registration_endpoint fetch (SSRF\n * mitigation - see `src/auth/discovery-fetch.ts`). Compute this from the\n * MCP server the caller is actually talking to, never from\n * `registrationEndpoint` itself (which is typically sourced from\n * remote-controlled AS metadata). Defaults to `false`.\n */\n allowLoopback?: boolean;\n}\n\n/**\n * Options for OAuth authorization flow\n */\nexport interface OAuthFlowOptions {\n /** Port for OAuth callback listener (required - use get-port to find available port) */\n port: number;\n /** Redirect URI for OAuth callback (optional - will be built from port if not provided) */\n redirectUri?: string;\n /** OAuth scopes to request */\n scopes?: string[];\n /** Resource parameter (RFC 8707) - target resource server identifier */\n resource?: string;\n /** Enable PKCE (RFC 7636) - recommended for all clients, required for public clients */\n pkce?: boolean;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Timeout for callback (milliseconds) */\n timeout?: number;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: import('../utils/logger.ts').Logger;\n /**\n * Loopback trust grant for the token endpoint fetch (SSRF mitigation - see\n * `src/auth/discovery-fetch.ts`). Compute this from the MCP server the\n * caller is actually talking to, never from `tokenEndpoint` itself (which\n * is typically sourced from remote-controlled AS metadata). Defaults to\n * `false`.\n */\n allowLoopback?: boolean;\n}\n"],"names":[],"mappings":"AAAA;;CAEC,GAED;;CAEC,GA+HD;;CAEC,GACD,WAyBC"}
|
|
@@ -58,12 +58,11 @@ export declare class DcrAuthenticator {
|
|
|
58
58
|
* Self-hosted servers manage their own token storage via /oauth/verify
|
|
59
59
|
*/
|
|
60
60
|
private ensureAuthenticatedSelfHosted;
|
|
61
|
-
/**
|
|
62
|
-
* Handle authentication for external OAuth providers (original implementation)
|
|
63
|
-
*/
|
|
61
|
+
/** Handles authentication for external (non-self-hosted) OAuth providers. */
|
|
64
62
|
private ensureAuthenticatedExternal;
|
|
65
63
|
/**
|
|
66
|
-
*
|
|
64
|
+
* Refreshes an access token using a refresh token.
|
|
65
|
+
* @param allowLoopback - Loopback trust grant computed from the server actually being talked to (SSRF mitigation, see discovery-fetch.ts). Defaults to `false`.
|
|
67
66
|
*/
|
|
68
67
|
private refreshTokens;
|
|
69
68
|
/**
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import Keyv from 'keyv';
|
|
7
7
|
import { KeyvFile } from 'keyv-file';
|
|
8
|
+
import { isLoopbackUrl } from '../auth/discovery-fetch.js';
|
|
8
9
|
import { InteractiveOAuthFlow } from '../auth/interactive-oauth-flow.js';
|
|
9
10
|
import { logger as defaultLogger } from '../utils/logger.js';
|
|
10
11
|
import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
@@ -57,6 +58,9 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
57
58
|
* Handle authentication for self-hosted DCR servers
|
|
58
59
|
* Self-hosted servers manage their own token storage via /oauth/verify
|
|
59
60
|
*/ async ensureAuthenticatedSelfHosted(baseUrl, capabilities) {
|
|
61
|
+
// Loopback trust for every discovery-derived fetch below, computed from
|
|
62
|
+
// the server we're talking to, never from capabilities' endpoints (see discovery-fetch.ts).
|
|
63
|
+
const allowLoopback = isLoopbackUrl(baseUrl);
|
|
60
64
|
const dcrTokenKey = `dcr-tokens:${baseUrl}`;
|
|
61
65
|
// 1. Check for existing DCR tokens (different from external tokens)
|
|
62
66
|
let tokens = await this.tokenStore.get(dcrTokenKey);
|
|
@@ -94,7 +98,8 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
94
98
|
// Register OAuth client via DCR
|
|
95
99
|
this.logger.debug('📝 Registering OAuth client with self-hosted server...');
|
|
96
100
|
const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {
|
|
97
|
-
redirectUri: this.redirectUri
|
|
101
|
+
redirectUri: this.redirectUri,
|
|
102
|
+
allowLoopback
|
|
98
103
|
});
|
|
99
104
|
// Perform OAuth authorization flow with PKCE (RFC 7636)
|
|
100
105
|
const flowOptions = {
|
|
@@ -102,7 +107,8 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
102
107
|
headless: this.headless,
|
|
103
108
|
redirectUri: this.redirectUri,
|
|
104
109
|
pkce: true,
|
|
105
|
-
logger: this.logger
|
|
110
|
+
logger: this.logger,
|
|
111
|
+
allowLoopback
|
|
106
112
|
};
|
|
107
113
|
if (capabilities.scopes) {
|
|
108
114
|
flowOptions.scopes = capabilities.scopes;
|
|
@@ -134,9 +140,9 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
134
140
|
this.logger.debug('✅ Self-hosted DCR authentication successful, tokens saved');
|
|
135
141
|
return tokens;
|
|
136
142
|
}
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
/** Handles authentication for external (non-self-hosted) OAuth providers. */ async ensureAuthenticatedExternal(baseUrl, capabilities) {
|
|
144
|
+
// See ensureAuthenticatedSelfHosted - same loopback trust rule.
|
|
145
|
+
const allowLoopback = isLoopbackUrl(baseUrl);
|
|
140
146
|
const tokenKey = `tokens:${baseUrl}`;
|
|
141
147
|
// 1. Check for existing tokens
|
|
142
148
|
let tokens = await this.tokenStore.get(tokenKey);
|
|
@@ -145,7 +151,7 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
145
151
|
if (tokens.expiresAt < Date.now() + REFRESH_BUFFER_MS) {
|
|
146
152
|
this.logger.debug('🔄 Refreshing access token...');
|
|
147
153
|
try {
|
|
148
|
-
tokens = await this.refreshTokens(tokens, capabilities.tokenEndpoint);
|
|
154
|
+
tokens = await this.refreshTokens(tokens, capabilities.tokenEndpoint, allowLoopback);
|
|
149
155
|
await this.tokenStore.set(tokenKey, tokens);
|
|
150
156
|
this.logger.debug('✅ Token refreshed successfully');
|
|
151
157
|
} catch (_error) {
|
|
@@ -169,7 +175,8 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
169
175
|
// Register OAuth client via DCR
|
|
170
176
|
this.logger.debug('📝 Registering OAuth client...');
|
|
171
177
|
const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {
|
|
172
|
-
redirectUri: this.redirectUri
|
|
178
|
+
redirectUri: this.redirectUri,
|
|
179
|
+
allowLoopback
|
|
173
180
|
});
|
|
174
181
|
// Perform OAuth authorization flow with PKCE (RFC 7636)
|
|
175
182
|
const flowOptions = {
|
|
@@ -177,7 +184,8 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
177
184
|
headless: this.headless,
|
|
178
185
|
redirectUri: this.redirectUri,
|
|
179
186
|
pkce: true,
|
|
180
|
-
logger: this.logger
|
|
187
|
+
logger: this.logger,
|
|
188
|
+
allowLoopback
|
|
181
189
|
};
|
|
182
190
|
if (capabilities.scopes) {
|
|
183
191
|
flowOptions.scopes = capabilities.scopes;
|
|
@@ -189,8 +197,9 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
189
197
|
return tokens;
|
|
190
198
|
}
|
|
191
199
|
/**
|
|
192
|
-
*
|
|
193
|
-
|
|
200
|
+
* Refreshes an access token using a refresh token.
|
|
201
|
+
* @param allowLoopback - Loopback trust grant computed from the server actually being talked to (SSRF mitigation, see discovery-fetch.ts). Defaults to `false`.
|
|
202
|
+
*/ async refreshTokens(tokens, tokenEndpoint, allowLoopback = false) {
|
|
194
203
|
if (!tokenEndpoint) {
|
|
195
204
|
throw new Error('Token endpoint not available for refresh');
|
|
196
205
|
}
|
|
@@ -200,7 +209,7 @@ import { DynamicClientRegistrar } from './dynamic-client-registrar.js';
|
|
|
200
209
|
if (!tokens.clientId || !tokens.clientSecret) {
|
|
201
210
|
throw new Error('Client credentials not available for refresh');
|
|
202
211
|
}
|
|
203
|
-
return await this.oauthFlow.refreshTokens(tokenEndpoint, tokens.refreshToken, tokens.clientId, tokens.clientSecret);
|
|
212
|
+
return await this.oauthFlow.refreshTokens(tokenEndpoint, tokens.refreshToken, tokens.clientId, tokens.clientSecret, allowLoopback);
|
|
204
213
|
}
|
|
205
214
|
/**
|
|
206
215
|
* Delete stored tokens for a server
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/dcr/dcr-authenticator.ts"],"sourcesContent":["/**\n * DCR Authenticator\n * Consolidates DCR and OAuth flow logic for MCP HTTP servers\n */\n\nimport path from 'node:path';\nimport * as fs from 'fs';\nimport Keyv from 'keyv';\nimport { KeyvFile } from 'keyv-file';\nimport { InteractiveOAuthFlow } from '../auth/interactive-oauth-flow.ts';\nimport type { AuthCapabilities, TokenSet } from '../auth/types.ts';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { DynamicClientRegistrar } from './dynamic-client-registrar.ts';\n\n/**\n * DcrAuthenticator configuration options\n */\nexport interface DcrAuthenticatorOptions {\n /** Custom Keyv store (for testing) - if not provided, uses default ~/.mcpeasy/tokens.json */\n tokenStore?: Keyv;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Required redirect URI for OAuth callback */\n redirectUri: string;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * Buffer time before token expiry to trigger proactive refresh (5 minutes)\n */\nconst REFRESH_BUFFER_MS = 5 * 60 * 1000;\n\n/**\n * DcrAuthenticator manages authentication for MCP HTTP servers\n * Handles DCR registration, OAuth flows, and token management\n */\nexport class DcrAuthenticator {\n private tokenStore: Keyv;\n private dcrClient: DynamicClientRegistrar;\n private oauthFlow: InteractiveOAuthFlow;\n private headless: boolean;\n private redirectUri: string;\n private logger: Logger;\n\n constructor(options: DcrAuthenticatorOptions) {\n if (options.tokenStore) {\n this.tokenStore = options.tokenStore;\n } else {\n // Default CLI store in .mcp-z directory (per-project)\n const storePath = path.join(process.cwd(), '.mcp-z', 'tokens.json');\n\n // Ensure directory exists before creating store\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n\n this.tokenStore = new Keyv({\n store: new KeyvFile({ filename: storePath }),\n });\n }\n this.dcrClient = new DynamicClientRegistrar();\n this.oauthFlow = new InteractiveOAuthFlow();\n this.headless = options.headless || false;\n this.redirectUri = options.redirectUri;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Detect if server is self-hosted DCR (vs external OAuth provider)\n * Self-hosted servers have their own OAuth endpoints and manage token storage\n */\n private async detectSelfHostedMode(baseUrl: string): Promise<boolean> {\n try {\n // Self-hosted DCR servers typically run their own OAuth server\n // Check if this is a self-hosted instance by testing OAuth metadata\n // For now, assume self-hosted if baseUrl matches common localhost patterns\n // TODO: Implement proper self-hosted detection logic\n return baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1');\n } catch (_error) {\n return false; // Assume external mode if detection fails\n }\n }\n\n /**\n * Ensure server is authenticated, performing DCR and OAuth if needed\n * Proactively refreshes tokens if they're within 5 minutes of expiry\n *\n * @param baseUrl - Base URL of the server (e.g., https://example.com)\n * @param capabilities - Auth capabilities from .well-known endpoint\n * @returns Valid token set ready to use\n *\n * @throws Error if authentication fails\n *\n * @example\n * const authenticator = new DcrAuthenticator({ redirectUri: 'http://localhost:3000/callback' });\n * const tokens = await authenticator.ensureAuthenticated(\n * 'https://example.com',\n * capabilities\n * );\n */\n async ensureAuthenticated(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // Auto-detect server mode\n const isSelfHosted = await this.detectSelfHostedMode(baseUrl);\n\n if (isSelfHosted) {\n return this.ensureAuthenticatedSelfHosted(baseUrl, capabilities);\n }\n return this.ensureAuthenticatedExternal(baseUrl, capabilities);\n }\n\n /**\n * Handle authentication for self-hosted DCR servers\n * Self-hosted servers manage their own token storage via /oauth/verify\n */\n private async ensureAuthenticatedSelfHosted(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n const dcrTokenKey = `dcr-tokens:${baseUrl}`;\n\n // 1. Check for existing DCR tokens (different from external tokens)\n let tokens = (await this.tokenStore.get(dcrTokenKey)) as TokenSet | undefined;\n\n if (tokens) {\n // 2. Verify token is still valid by calling /oauth/verify\n try {\n const verifyUrl = `${baseUrl}/oauth/verify`;\n const verifyResponse = await fetch(verifyUrl, {\n headers: { Authorization: `Bearer ${tokens.accessToken}`, Connection: 'close' },\n });\n\n if (verifyResponse.ok) {\n const verifyData = (await verifyResponse.json()) as { token?: string };\n if (verifyData.token === tokens.accessToken) {\n // Token is still valid with the self-hosted server\n return tokens;\n }\n }\n } catch (_error) {\n // Token verification failed - need to re-authenticate\n }\n\n // Token is expired or invalid\n await this.tokenStore.delete(dcrTokenKey);\n tokens = undefined;\n }\n\n // 3. No valid tokens - perform full DCR + OAuth flow\n if (!capabilities.registrationEndpoint || !capabilities.authorizationEndpoint || !capabilities.tokenEndpoint) {\n throw new Error('Server does not provide required OAuth endpoints');\n }\n\n this.logger.debug('🔐 No valid tokens found, starting self-hosted DCR authentication...');\n\n // Extract port from pre-resolved redirectUri\n const port = parseInt(new URL(this.redirectUri).port, 10) || (this.redirectUri.startsWith('https:') ? 443 : 80);\n\n // Register OAuth client via DCR\n this.logger.debug('📝 Registering OAuth client with self-hosted server...');\n const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {\n redirectUri: this.redirectUri,\n });\n\n // Perform OAuth authorization flow with PKCE (RFC 7636)\n const flowOptions: { port: number; headless: boolean; scopes?: string[]; redirectUri: string; pkce: boolean; logger: Logger } = {\n port,\n headless: this.headless,\n redirectUri: this.redirectUri,\n pkce: true,\n logger: this.logger,\n };\n if (capabilities.scopes) {\n flowOptions.scopes = capabilities.scopes;\n }\n\n tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);\n\n // For self-hosted mode, verify the token works with /oauth/verify immediately\n try {\n const verifyUrl = `${baseUrl}/oauth/verify`;\n const verifyResponse = await fetch(verifyUrl, {\n headers: { Authorization: `Bearer ${tokens.accessToken}`, Connection: 'close' },\n });\n\n if (!verifyResponse.ok) {\n throw new Error(`DCR token verification failed after authentication: ${verifyResponse.status}`);\n }\n\n const verifyData = (await verifyResponse.json()) as { token?: string };\n if (verifyData.token !== tokens.accessToken) {\n throw new Error('DCR server returned different token in verification');\n }\n\n this.logger.debug('✅ DCR token verified with self-hosted server');\n } catch (error) {\n this.logger.error('❌ DCR token verification failed:', error instanceof Error ? error.message : String(error));\n throw new Error('Self-hosted DCR authentication completed but token verification failed');\n }\n\n // Save tokens for future use\n await this.tokenStore.set(dcrTokenKey, tokens);\n this.logger.debug('✅ Self-hosted DCR authentication successful, tokens saved');\n\n return tokens;\n }\n\n /**\n * Handle authentication for external OAuth providers (original implementation)\n */\n private async ensureAuthenticatedExternal(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n const tokenKey = `tokens:${baseUrl}`;\n\n // 1. Check for existing tokens\n let tokens = (await this.tokenStore.get(tokenKey)) as TokenSet | undefined;\n\n if (tokens) {\n // 2. Proactive refresh if token expires within 5 minutes\n if (tokens.expiresAt < Date.now() + REFRESH_BUFFER_MS) {\n this.logger.debug('🔄 Refreshing access token...');\n\n try {\n tokens = await this.refreshTokens(tokens, capabilities.tokenEndpoint);\n await this.tokenStore.set(tokenKey, tokens);\n this.logger.debug('✅ Token refreshed successfully');\n } catch (_error) {\n // Refresh failed - clear tokens and re-authenticate\n this.logger.warn('⚠️ Token refresh failed, re-authenticating...');\n await this.tokenStore.delete(tokenKey);\n tokens = undefined;\n }\n }\n\n if (tokens) {\n return tokens;\n }\n }\n\n // 3. No valid tokens - perform DCR + OAuth flow\n if (!capabilities.registrationEndpoint || !capabilities.authorizationEndpoint || !capabilities.tokenEndpoint) {\n throw new Error('Server does not provide required OAuth endpoints');\n }\n\n this.logger.debug('🔐 No valid tokens found, starting external OAuth authentication...');\n\n // Extract port from pre-resolved redirectUri\n const port = parseInt(new URL(this.redirectUri).port, 10) || (this.redirectUri.startsWith('https:') ? 443 : 80);\n\n // Register OAuth client via DCR\n this.logger.debug('📝 Registering OAuth client...');\n const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {\n redirectUri: this.redirectUri,\n });\n\n // Perform OAuth authorization flow with PKCE (RFC 7636)\n const flowOptions: { port: number; headless: boolean; scopes?: string[]; redirectUri: string; pkce: boolean; logger: Logger } = {\n port,\n headless: this.headless,\n redirectUri: this.redirectUri,\n pkce: true,\n logger: this.logger,\n };\n if (capabilities.scopes) {\n flowOptions.scopes = capabilities.scopes;\n }\n\n tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);\n\n // Save tokens for future use\n await this.tokenStore.set(tokenKey, tokens);\n this.logger.debug('✅ Authentication successful, tokens saved');\n\n return tokens;\n }\n\n /**\n * Refresh access token using refresh token\n */\n private async refreshTokens(tokens: TokenSet, tokenEndpoint?: string): Promise<TokenSet> {\n if (!tokenEndpoint) {\n throw new Error('Token endpoint not available for refresh');\n }\n\n if (!tokens.refreshToken) {\n throw new Error('No refresh token available');\n }\n\n if (!tokens.clientId || !tokens.clientSecret) {\n throw new Error('Client credentials not available for refresh');\n }\n\n return await this.oauthFlow.refreshTokens(tokenEndpoint, tokens.refreshToken, tokens.clientId, tokens.clientSecret);\n }\n\n /**\n * Delete stored tokens for a server\n */\n async deleteTokens(baseUrl: string): Promise<void> {\n const tokenKey = `tokens:${baseUrl}`;\n await this.tokenStore.delete(tokenKey);\n this.logger.debug(`🗑️ Deleted tokens for ${baseUrl}`);\n }\n}\n"],"names":["path","fs","Keyv","KeyvFile","InteractiveOAuthFlow","logger","defaultLogger","DynamicClientRegistrar","REFRESH_BUFFER_MS","DcrAuthenticator","detectSelfHostedMode","baseUrl","includes","_error","ensureAuthenticated","capabilities","isSelfHosted","ensureAuthenticatedSelfHosted","ensureAuthenticatedExternal","dcrTokenKey","tokens","tokenStore","get","verifyUrl","verifyResponse","fetch","headers","Authorization","accessToken","Connection","ok","verifyData","json","token","delete","undefined","registrationEndpoint","authorizationEndpoint","tokenEndpoint","Error","debug","port","parseInt","URL","redirectUri","startsWith","client","dcrClient","registerClient","flowOptions","headless","pkce","scopes","oauthFlow","performAuthFlow","clientId","clientSecret","status","error","message","String","set","tokenKey","expiresAt","Date","now","refreshTokens","warn","refreshToken","deleteTokens","options","storePath","join","process","cwd","mkdirSync","dirname","recursive","store","filename"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/mcp-z/client/src/dcr/dcr-authenticator.ts"],"sourcesContent":["/**\n * DCR Authenticator\n * Consolidates DCR and OAuth flow logic for MCP HTTP servers\n */\n\nimport path from 'node:path';\nimport * as fs from 'fs';\nimport Keyv from 'keyv';\nimport { KeyvFile } from 'keyv-file';\nimport { isLoopbackUrl } from '../auth/discovery-fetch.ts';\nimport { InteractiveOAuthFlow } from '../auth/interactive-oauth-flow.ts';\nimport type { AuthCapabilities, TokenSet } from '../auth/types.ts';\nimport { logger as defaultLogger, type Logger } from '../utils/logger.ts';\nimport { DynamicClientRegistrar } from './dynamic-client-registrar.ts';\n\n/**\n * DcrAuthenticator configuration options\n */\nexport interface DcrAuthenticatorOptions {\n /** Custom Keyv store (for testing) - if not provided, uses default ~/.mcpeasy/tokens.json */\n tokenStore?: Keyv;\n /** Headless mode (don't open browser) */\n headless?: boolean;\n /** Required redirect URI for OAuth callback */\n redirectUri: string;\n /** Optional logger for debug output (defaults to singleton logger) */\n logger?: Logger;\n}\n\n/**\n * Buffer time before token expiry to trigger proactive refresh (5 minutes)\n */\nconst REFRESH_BUFFER_MS = 5 * 60 * 1000;\n\n/**\n * DcrAuthenticator manages authentication for MCP HTTP servers\n * Handles DCR registration, OAuth flows, and token management\n */\nexport class DcrAuthenticator {\n private tokenStore: Keyv;\n private dcrClient: DynamicClientRegistrar;\n private oauthFlow: InteractiveOAuthFlow;\n private headless: boolean;\n private redirectUri: string;\n private logger: Logger;\n\n constructor(options: DcrAuthenticatorOptions) {\n if (options.tokenStore) {\n this.tokenStore = options.tokenStore;\n } else {\n // Default CLI store in .mcp-z directory (per-project)\n const storePath = path.join(process.cwd(), '.mcp-z', 'tokens.json');\n\n // Ensure directory exists before creating store\n fs.mkdirSync(path.dirname(storePath), { recursive: true });\n\n this.tokenStore = new Keyv({\n store: new KeyvFile({ filename: storePath }),\n });\n }\n this.dcrClient = new DynamicClientRegistrar();\n this.oauthFlow = new InteractiveOAuthFlow();\n this.headless = options.headless || false;\n this.redirectUri = options.redirectUri;\n this.logger = options.logger ?? defaultLogger;\n }\n\n /**\n * Detect if server is self-hosted DCR (vs external OAuth provider)\n * Self-hosted servers have their own OAuth endpoints and manage token storage\n */\n private async detectSelfHostedMode(baseUrl: string): Promise<boolean> {\n try {\n // Self-hosted DCR servers typically run their own OAuth server\n // Check if this is a self-hosted instance by testing OAuth metadata\n // For now, assume self-hosted if baseUrl matches common localhost patterns\n // TODO: Implement proper self-hosted detection logic\n return baseUrl.includes('localhost') || baseUrl.includes('127.0.0.1');\n } catch (_error) {\n return false; // Assume external mode if detection fails\n }\n }\n\n /**\n * Ensure server is authenticated, performing DCR and OAuth if needed\n * Proactively refreshes tokens if they're within 5 minutes of expiry\n *\n * @param baseUrl - Base URL of the server (e.g., https://example.com)\n * @param capabilities - Auth capabilities from .well-known endpoint\n * @returns Valid token set ready to use\n *\n * @throws Error if authentication fails\n *\n * @example\n * const authenticator = new DcrAuthenticator({ redirectUri: 'http://localhost:3000/callback' });\n * const tokens = await authenticator.ensureAuthenticated(\n * 'https://example.com',\n * capabilities\n * );\n */\n async ensureAuthenticated(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // Auto-detect server mode\n const isSelfHosted = await this.detectSelfHostedMode(baseUrl);\n\n if (isSelfHosted) {\n return this.ensureAuthenticatedSelfHosted(baseUrl, capabilities);\n }\n return this.ensureAuthenticatedExternal(baseUrl, capabilities);\n }\n\n /**\n * Handle authentication for self-hosted DCR servers\n * Self-hosted servers manage their own token storage via /oauth/verify\n */\n private async ensureAuthenticatedSelfHosted(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // Loopback trust for every discovery-derived fetch below, computed from\n // the server we're talking to, never from capabilities' endpoints (see discovery-fetch.ts).\n const allowLoopback = isLoopbackUrl(baseUrl);\n const dcrTokenKey = `dcr-tokens:${baseUrl}`;\n\n // 1. Check for existing DCR tokens (different from external tokens)\n let tokens = (await this.tokenStore.get(dcrTokenKey)) as TokenSet | undefined;\n\n if (tokens) {\n // 2. Verify token is still valid by calling /oauth/verify\n try {\n const verifyUrl = `${baseUrl}/oauth/verify`;\n const verifyResponse = await fetch(verifyUrl, {\n headers: { Authorization: `Bearer ${tokens.accessToken}`, Connection: 'close' },\n });\n\n if (verifyResponse.ok) {\n const verifyData = (await verifyResponse.json()) as { token?: string };\n if (verifyData.token === tokens.accessToken) {\n // Token is still valid with the self-hosted server\n return tokens;\n }\n }\n } catch (_error) {\n // Token verification failed - need to re-authenticate\n }\n\n // Token is expired or invalid\n await this.tokenStore.delete(dcrTokenKey);\n tokens = undefined;\n }\n\n // 3. No valid tokens - perform full DCR + OAuth flow\n if (!capabilities.registrationEndpoint || !capabilities.authorizationEndpoint || !capabilities.tokenEndpoint) {\n throw new Error('Server does not provide required OAuth endpoints');\n }\n\n this.logger.debug('🔐 No valid tokens found, starting self-hosted DCR authentication...');\n\n // Extract port from pre-resolved redirectUri\n const port = parseInt(new URL(this.redirectUri).port, 10) || (this.redirectUri.startsWith('https:') ? 443 : 80);\n\n // Register OAuth client via DCR\n this.logger.debug('📝 Registering OAuth client with self-hosted server...');\n const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {\n redirectUri: this.redirectUri,\n allowLoopback,\n });\n\n // Perform OAuth authorization flow with PKCE (RFC 7636)\n const flowOptions: { port: number; headless: boolean; scopes?: string[]; redirectUri: string; pkce: boolean; logger: Logger; allowLoopback: boolean } = {\n port,\n headless: this.headless,\n redirectUri: this.redirectUri,\n pkce: true,\n logger: this.logger,\n allowLoopback,\n };\n if (capabilities.scopes) {\n flowOptions.scopes = capabilities.scopes;\n }\n\n tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);\n\n // For self-hosted mode, verify the token works with /oauth/verify immediately\n try {\n const verifyUrl = `${baseUrl}/oauth/verify`;\n const verifyResponse = await fetch(verifyUrl, {\n headers: { Authorization: `Bearer ${tokens.accessToken}`, Connection: 'close' },\n });\n\n if (!verifyResponse.ok) {\n throw new Error(`DCR token verification failed after authentication: ${verifyResponse.status}`);\n }\n\n const verifyData = (await verifyResponse.json()) as { token?: string };\n if (verifyData.token !== tokens.accessToken) {\n throw new Error('DCR server returned different token in verification');\n }\n\n this.logger.debug('✅ DCR token verified with self-hosted server');\n } catch (error) {\n this.logger.error('❌ DCR token verification failed:', error instanceof Error ? error.message : String(error));\n throw new Error('Self-hosted DCR authentication completed but token verification failed');\n }\n\n // Save tokens for future use\n await this.tokenStore.set(dcrTokenKey, tokens);\n this.logger.debug('✅ Self-hosted DCR authentication successful, tokens saved');\n\n return tokens;\n }\n\n /** Handles authentication for external (non-self-hosted) OAuth providers. */\n private async ensureAuthenticatedExternal(baseUrl: string, capabilities: AuthCapabilities): Promise<TokenSet> {\n // See ensureAuthenticatedSelfHosted - same loopback trust rule.\n const allowLoopback = isLoopbackUrl(baseUrl);\n const tokenKey = `tokens:${baseUrl}`;\n\n // 1. Check for existing tokens\n let tokens = (await this.tokenStore.get(tokenKey)) as TokenSet | undefined;\n\n if (tokens) {\n // 2. Proactive refresh if token expires within 5 minutes\n if (tokens.expiresAt < Date.now() + REFRESH_BUFFER_MS) {\n this.logger.debug('🔄 Refreshing access token...');\n\n try {\n tokens = await this.refreshTokens(tokens, capabilities.tokenEndpoint, allowLoopback);\n await this.tokenStore.set(tokenKey, tokens);\n this.logger.debug('✅ Token refreshed successfully');\n } catch (_error) {\n // Refresh failed - clear tokens and re-authenticate\n this.logger.warn('⚠️ Token refresh failed, re-authenticating...');\n await this.tokenStore.delete(tokenKey);\n tokens = undefined;\n }\n }\n\n if (tokens) {\n return tokens;\n }\n }\n\n // 3. No valid tokens - perform DCR + OAuth flow\n if (!capabilities.registrationEndpoint || !capabilities.authorizationEndpoint || !capabilities.tokenEndpoint) {\n throw new Error('Server does not provide required OAuth endpoints');\n }\n\n this.logger.debug('🔐 No valid tokens found, starting external OAuth authentication...');\n\n // Extract port from pre-resolved redirectUri\n const port = parseInt(new URL(this.redirectUri).port, 10) || (this.redirectUri.startsWith('https:') ? 443 : 80);\n\n // Register OAuth client via DCR\n this.logger.debug('📝 Registering OAuth client...');\n const client = await this.dcrClient.registerClient(capabilities.registrationEndpoint, {\n redirectUri: this.redirectUri,\n allowLoopback,\n });\n\n // Perform OAuth authorization flow with PKCE (RFC 7636)\n const flowOptions: { port: number; headless: boolean; scopes?: string[]; redirectUri: string; pkce: boolean; logger: Logger; allowLoopback: boolean } = {\n port,\n headless: this.headless,\n redirectUri: this.redirectUri,\n pkce: true,\n logger: this.logger,\n allowLoopback,\n };\n if (capabilities.scopes) {\n flowOptions.scopes = capabilities.scopes;\n }\n\n tokens = await this.oauthFlow.performAuthFlow(capabilities.authorizationEndpoint, capabilities.tokenEndpoint, client.clientId, client.clientSecret, flowOptions);\n\n // Save tokens for future use\n await this.tokenStore.set(tokenKey, tokens);\n this.logger.debug('✅ Authentication successful, tokens saved');\n\n return tokens;\n }\n\n /**\n * Refreshes an access token using a refresh token.\n * @param allowLoopback - Loopback trust grant computed from the server actually being talked to (SSRF mitigation, see discovery-fetch.ts). Defaults to `false`.\n */\n private async refreshTokens(tokens: TokenSet, tokenEndpoint: string | undefined, allowLoopback = false): Promise<TokenSet> {\n if (!tokenEndpoint) {\n throw new Error('Token endpoint not available for refresh');\n }\n\n if (!tokens.refreshToken) {\n throw new Error('No refresh token available');\n }\n\n if (!tokens.clientId || !tokens.clientSecret) {\n throw new Error('Client credentials not available for refresh');\n }\n\n return await this.oauthFlow.refreshTokens(tokenEndpoint, tokens.refreshToken, tokens.clientId, tokens.clientSecret, allowLoopback);\n }\n\n /**\n * Delete stored tokens for a server\n */\n async deleteTokens(baseUrl: string): Promise<void> {\n const tokenKey = `tokens:${baseUrl}`;\n await this.tokenStore.delete(tokenKey);\n this.logger.debug(`🗑️ Deleted tokens for ${baseUrl}`);\n }\n}\n"],"names":["path","fs","Keyv","KeyvFile","isLoopbackUrl","InteractiveOAuthFlow","logger","defaultLogger","DynamicClientRegistrar","REFRESH_BUFFER_MS","DcrAuthenticator","detectSelfHostedMode","baseUrl","includes","_error","ensureAuthenticated","capabilities","isSelfHosted","ensureAuthenticatedSelfHosted","ensureAuthenticatedExternal","allowLoopback","dcrTokenKey","tokens","tokenStore","get","verifyUrl","verifyResponse","fetch","headers","Authorization","accessToken","Connection","ok","verifyData","json","token","delete","undefined","registrationEndpoint","authorizationEndpoint","tokenEndpoint","Error","debug","port","parseInt","URL","redirectUri","startsWith","client","dcrClient","registerClient","flowOptions","headless","pkce","scopes","oauthFlow","performAuthFlow","clientId","clientSecret","status","error","message","String","set","tokenKey","expiresAt","Date","now","refreshTokens","warn","refreshToken","deleteTokens","options","storePath","join","process","cwd","mkdirSync","dirname","recursive","store","filename"],"mappings":"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"}
|
|
@@ -3,26 +3,15 @@
|
|
|
3
3
|
* Implements RFC 7591 for OAuth client registration
|
|
4
4
|
*/
|
|
5
5
|
import type { ClientCredentials, DcrRegistrationOptions } from '../auth/types.js';
|
|
6
|
-
/**
|
|
7
|
-
* DynamicClientRegistrar handles Dynamic Client Registration with OAuth servers
|
|
8
|
-
*/
|
|
6
|
+
/** Handles Dynamic Client Registration with OAuth servers. */
|
|
9
7
|
export declare class DynamicClientRegistrar {
|
|
10
8
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param registrationEndpoint - DCR registration endpoint URL
|
|
14
|
-
* @param options - Registration options (client name, redirect URI)
|
|
15
|
-
* @returns Client credentials (client ID and secret)
|
|
16
|
-
*
|
|
17
|
-
* @throws Error if registration fails or server returns error
|
|
9
|
+
* Registers a new OAuth client with the authorization server (RFC 7591).
|
|
18
10
|
*
|
|
19
|
-
* @
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* { clientName: '@mcp-z/client', redirectUri: 'http://localhost:3000/callback' }
|
|
24
|
-
* );
|
|
25
|
-
* console.log('Client ID:', creds.clientId);
|
|
11
|
+
* @param registrationEndpoint - Often sourced from remote-controlled AS metadata; pass `options.allowLoopback` explicitly. Defaults to `false`.
|
|
12
|
+
* @param options - Registration options (client name, redirect URI, loopback trust).
|
|
13
|
+
* @returns Client credentials (client ID and secret).
|
|
14
|
+
* @throws Error if registration fails or the server returns an error.
|
|
26
15
|
*/
|
|
27
16
|
registerClient(registrationEndpoint: string, options?: DcrRegistrationOptions): Promise<ClientCredentials>;
|
|
28
17
|
}
|