@atxp/client 0.3.0 → 0.5.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/dist/index.cjs +745 -142
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +745 -142
- package/dist/index.js.map +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js +369 -99
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/client/auth.js.map +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js +18 -18
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/client/streamableHttp.js.map +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/server/auth/errors.js +162 -0
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/server/auth/errors.js.map +1 -0
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/auth.js +86 -14
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/auth.js.map +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js +47 -9
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/shared/protocol.js.map +1 -1
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js +64 -5
- package/dist/node_modules/@modelcontextprotocol/sdk/dist/esm/types.js.map +1 -1
- package/dist/node_modules/eventsource-parser/dist/index.js +1 -1
- package/dist/node_modules/eventsource-parser/dist/index.js.map +1 -1
- package/dist/node_modules/zod/{dist/esm/v3 → v3}/ZodError.js +3 -2
- package/dist/node_modules/zod/v3/ZodError.js.map +1 -0
- package/dist/node_modules/zod/v3/errors.js.map +1 -0
- package/dist/node_modules/zod/v3/helpers/errorUtil.js.map +1 -0
- package/dist/node_modules/zod/v3/helpers/parseUtil.js.map +1 -0
- package/dist/node_modules/zod/v3/helpers/util.js.map +1 -0
- package/dist/node_modules/zod/{dist/esm/v3 → v3}/locales/en.js +2 -0
- package/dist/node_modules/zod/v3/locales/en.js.map +1 -0
- package/dist/node_modules/zod/{dist/esm/v3 → v3}/types.js +5 -2
- package/dist/node_modules/zod/v3/types.js.map +1 -0
- package/package.json +2 -2
- package/dist/node_modules/zod/dist/esm/v3/ZodError.js.map +0 -1
- package/dist/node_modules/zod/dist/esm/v3/errors.js.map +0 -1
- package/dist/node_modules/zod/dist/esm/v3/helpers/errorUtil.js.map +0 -1
- package/dist/node_modules/zod/dist/esm/v3/helpers/parseUtil.js.map +0 -1
- package/dist/node_modules/zod/dist/esm/v3/helpers/util.js.map +0 -1
- package/dist/node_modules/zod/dist/esm/v3/locales/en.js.map +0 -1
- package/dist/node_modules/zod/dist/esm/v3/types.js.map +0 -1
- /package/dist/node_modules/zod/{dist/esm/v3 → v3}/errors.js +0 -0
- /package/dist/node_modules/zod/{dist/esm/v3 → v3}/helpers/errorUtil.js +0 -0
- /package/dist/node_modules/zod/{dist/esm/v3 → v3}/helpers/parseUtil.js +0 -0
- /package/dist/node_modules/zod/{dist/esm/v3 → v3}/helpers/util.js +0 -0
|
@@ -1,24 +1,156 @@
|
|
|
1
1
|
import pkceChallenge from '../../../../../pkce-challenge/dist/index.browser.js';
|
|
2
2
|
import { LATEST_PROTOCOL_VERSION } from '../types.js';
|
|
3
|
-
import { OAuthProtectedResourceMetadataSchema, OAuthMetadataSchema, OAuthClientInformationFullSchema, OAuthTokensSchema } from '../shared/auth.js';
|
|
3
|
+
import { OAuthProtectedResourceMetadataSchema, OAuthMetadataSchema, OpenIdProviderDiscoveryMetadataSchema, OAuthClientInformationFullSchema, OAuthTokensSchema, OAuthErrorResponseSchema } from '../shared/auth.js';
|
|
4
4
|
import { resourceUrlFromServerUrl, checkResourceAllowed } from '../shared/auth-utils.js';
|
|
5
|
+
import { InvalidClientError, UnauthorizedClientError, InvalidGrantError, OAuthError, ServerError, OAUTH_ERRORS } from '../server/auth/errors.js';
|
|
5
6
|
|
|
6
7
|
class UnauthorizedError extends Error {
|
|
7
8
|
constructor(message) {
|
|
8
9
|
super(message !== null && message !== void 0 ? message : "Unauthorized");
|
|
9
10
|
}
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Determines the best client authentication method to use based on server support and client configuration.
|
|
14
|
+
*
|
|
15
|
+
* Priority order (highest to lowest):
|
|
16
|
+
* 1. client_secret_basic (if client secret is available)
|
|
17
|
+
* 2. client_secret_post (if client secret is available)
|
|
18
|
+
* 3. none (for public clients)
|
|
19
|
+
*
|
|
20
|
+
* @param clientInformation - OAuth client information containing credentials
|
|
21
|
+
* @param supportedMethods - Authentication methods supported by the authorization server
|
|
22
|
+
* @returns The selected authentication method
|
|
23
|
+
*/
|
|
24
|
+
function selectClientAuthMethod(clientInformation, supportedMethods) {
|
|
25
|
+
const hasClientSecret = clientInformation.client_secret !== undefined;
|
|
26
|
+
// If server doesn't specify supported methods, use RFC 6749 defaults
|
|
27
|
+
if (supportedMethods.length === 0) {
|
|
28
|
+
return hasClientSecret ? "client_secret_post" : "none";
|
|
29
|
+
}
|
|
30
|
+
// Try methods in priority order (most secure first)
|
|
31
|
+
if (hasClientSecret && supportedMethods.includes("client_secret_basic")) {
|
|
32
|
+
return "client_secret_basic";
|
|
33
|
+
}
|
|
34
|
+
if (hasClientSecret && supportedMethods.includes("client_secret_post")) {
|
|
35
|
+
return "client_secret_post";
|
|
36
|
+
}
|
|
37
|
+
if (supportedMethods.includes("none")) {
|
|
38
|
+
return "none";
|
|
39
|
+
}
|
|
40
|
+
// Fallback: use what we have
|
|
41
|
+
return hasClientSecret ? "client_secret_post" : "none";
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Applies client authentication to the request based on the specified method.
|
|
45
|
+
*
|
|
46
|
+
* Implements OAuth 2.1 client authentication methods:
|
|
47
|
+
* - client_secret_basic: HTTP Basic authentication (RFC 6749 Section 2.3.1)
|
|
48
|
+
* - client_secret_post: Credentials in request body (RFC 6749 Section 2.3.1)
|
|
49
|
+
* - none: Public client authentication (RFC 6749 Section 2.1)
|
|
50
|
+
*
|
|
51
|
+
* @param method - The authentication method to use
|
|
52
|
+
* @param clientInformation - OAuth client information containing credentials
|
|
53
|
+
* @param headers - HTTP headers object to modify
|
|
54
|
+
* @param params - URL search parameters to modify
|
|
55
|
+
* @throws {Error} When required credentials are missing
|
|
56
|
+
*/
|
|
57
|
+
function applyClientAuthentication(method, clientInformation, headers, params) {
|
|
58
|
+
const { client_id, client_secret } = clientInformation;
|
|
59
|
+
switch (method) {
|
|
60
|
+
case "client_secret_basic":
|
|
61
|
+
applyBasicAuth(client_id, client_secret, headers);
|
|
62
|
+
return;
|
|
63
|
+
case "client_secret_post":
|
|
64
|
+
applyPostAuth(client_id, client_secret, params);
|
|
65
|
+
return;
|
|
66
|
+
case "none":
|
|
67
|
+
applyPublicAuth(client_id, params);
|
|
68
|
+
return;
|
|
69
|
+
default:
|
|
70
|
+
throw new Error(`Unsupported client authentication method: ${method}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Applies HTTP Basic authentication (RFC 6749 Section 2.3.1)
|
|
75
|
+
*/
|
|
76
|
+
function applyBasicAuth(clientId, clientSecret, headers) {
|
|
77
|
+
if (!clientSecret) {
|
|
78
|
+
throw new Error("client_secret_basic authentication requires a client_secret");
|
|
79
|
+
}
|
|
80
|
+
const credentials = btoa(`${clientId}:${clientSecret}`);
|
|
81
|
+
headers.set("Authorization", `Basic ${credentials}`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Applies POST body authentication (RFC 6749 Section 2.3.1)
|
|
85
|
+
*/
|
|
86
|
+
function applyPostAuth(clientId, clientSecret, params) {
|
|
87
|
+
params.set("client_id", clientId);
|
|
88
|
+
if (clientSecret) {
|
|
89
|
+
params.set("client_secret", clientSecret);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Applies public client authentication (RFC 6749 Section 2.1)
|
|
94
|
+
*/
|
|
95
|
+
function applyPublicAuth(clientId, params) {
|
|
96
|
+
params.set("client_id", clientId);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parses an OAuth error response from a string or Response object.
|
|
100
|
+
*
|
|
101
|
+
* If the input is a standard OAuth2.0 error response, it will be parsed according to the spec
|
|
102
|
+
* and an instance of the appropriate OAuthError subclass will be returned.
|
|
103
|
+
* If parsing fails, it falls back to a generic ServerError that includes
|
|
104
|
+
* the response status (if available) and original content.
|
|
105
|
+
*
|
|
106
|
+
* @param input - A Response object or string containing the error response
|
|
107
|
+
* @returns A Promise that resolves to an OAuthError instance
|
|
108
|
+
*/
|
|
109
|
+
async function parseErrorResponse(input) {
|
|
110
|
+
const statusCode = input instanceof Response ? input.status : undefined;
|
|
111
|
+
const body = input instanceof Response ? await input.text() : input;
|
|
112
|
+
try {
|
|
113
|
+
const result = OAuthErrorResponseSchema.parse(JSON.parse(body));
|
|
114
|
+
const { error, error_description, error_uri } = result;
|
|
115
|
+
const errorClass = OAUTH_ERRORS[error] || ServerError;
|
|
116
|
+
return new errorClass(error_description || '', error_uri);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
// Not a valid OAuth error response, but try to inform the user of the raw data anyway
|
|
120
|
+
const errorMessage = `${statusCode ? `HTTP ${statusCode}: ` : ''}Invalid OAuth error response: ${error}. Raw body: ${body}`;
|
|
121
|
+
return new ServerError(errorMessage);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
11
124
|
/**
|
|
12
125
|
* Orchestrates the full auth flow with a server.
|
|
13
126
|
*
|
|
14
127
|
* This can be used as a single entry point for all authorization functionality,
|
|
15
128
|
* instead of linking together the other lower-level functions in this module.
|
|
16
129
|
*/
|
|
17
|
-
async function auth(provider,
|
|
130
|
+
async function auth(provider, options) {
|
|
131
|
+
var _a, _b;
|
|
132
|
+
try {
|
|
133
|
+
return await authInternal(provider, options);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
// Handle recoverable error types by invalidating credentials and retrying
|
|
137
|
+
if (error instanceof InvalidClientError || error instanceof UnauthorizedClientError) {
|
|
138
|
+
await ((_a = provider.invalidateCredentials) === null || _a === void 0 ? void 0 : _a.call(provider, 'all'));
|
|
139
|
+
return await authInternal(provider, options);
|
|
140
|
+
}
|
|
141
|
+
else if (error instanceof InvalidGrantError) {
|
|
142
|
+
await ((_b = provider.invalidateCredentials) === null || _b === void 0 ? void 0 : _b.call(provider, 'tokens'));
|
|
143
|
+
return await authInternal(provider, options);
|
|
144
|
+
}
|
|
145
|
+
// Throw otherwise
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn, }) {
|
|
18
150
|
let resourceMetadata;
|
|
19
|
-
let authorizationServerUrl
|
|
151
|
+
let authorizationServerUrl;
|
|
20
152
|
try {
|
|
21
|
-
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl });
|
|
153
|
+
resourceMetadata = await discoverOAuthProtectedResourceMetadata(serverUrl, { resourceMetadataUrl }, fetchFn);
|
|
22
154
|
if (resourceMetadata.authorization_servers && resourceMetadata.authorization_servers.length > 0) {
|
|
23
155
|
authorizationServerUrl = resourceMetadata.authorization_servers[0];
|
|
24
156
|
}
|
|
@@ -26,8 +158,17 @@ async function auth(provider, { serverUrl, authorizationCode, scope, resourceMet
|
|
|
26
158
|
catch (_a) {
|
|
27
159
|
// Ignore errors and fall back to /.well-known/oauth-authorization-server
|
|
28
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* If we don't get a valid authorization server metadata from protected resource metadata,
|
|
163
|
+
* fallback to the legacy MCP spec's implementation (version 2025-03-26): MCP server acts as the Authorization server.
|
|
164
|
+
*/
|
|
165
|
+
if (!authorizationServerUrl) {
|
|
166
|
+
authorizationServerUrl = serverUrl;
|
|
167
|
+
}
|
|
29
168
|
const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
|
|
30
|
-
const metadata = await
|
|
169
|
+
const metadata = await discoverAuthorizationServerMetadata(authorizationServerUrl, {
|
|
170
|
+
fetchFn,
|
|
171
|
+
});
|
|
31
172
|
// Handle client registration if needed
|
|
32
173
|
let clientInformation = await Promise.resolve(provider.clientInformation());
|
|
33
174
|
if (!clientInformation) {
|
|
@@ -40,6 +181,7 @@ async function auth(provider, { serverUrl, authorizationCode, scope, resourceMet
|
|
|
40
181
|
const fullInformation = await registerClient(authorizationServerUrl, {
|
|
41
182
|
metadata,
|
|
42
183
|
clientMetadata: provider.clientMetadata,
|
|
184
|
+
fetchFn,
|
|
43
185
|
});
|
|
44
186
|
await provider.saveClientInformation(fullInformation);
|
|
45
187
|
clientInformation = fullInformation;
|
|
@@ -54,6 +196,8 @@ async function auth(provider, { serverUrl, authorizationCode, scope, resourceMet
|
|
|
54
196
|
codeVerifier,
|
|
55
197
|
redirectUri: provider.redirectUrl,
|
|
56
198
|
resource,
|
|
199
|
+
addClientAuthentication: provider.addClientAuthentication,
|
|
200
|
+
fetchFn: fetchFn,
|
|
57
201
|
});
|
|
58
202
|
await provider.saveTokens(tokens);
|
|
59
203
|
return "AUTHORIZED";
|
|
@@ -68,12 +212,19 @@ async function auth(provider, { serverUrl, authorizationCode, scope, resourceMet
|
|
|
68
212
|
clientInformation,
|
|
69
213
|
refreshToken: tokens.refresh_token,
|
|
70
214
|
resource,
|
|
215
|
+
addClientAuthentication: provider.addClientAuthentication,
|
|
216
|
+
fetchFn,
|
|
71
217
|
});
|
|
72
218
|
await provider.saveTokens(newTokens);
|
|
73
219
|
return "AUTHORIZED";
|
|
74
220
|
}
|
|
75
|
-
catch (
|
|
76
|
-
//
|
|
221
|
+
catch (error) {
|
|
222
|
+
// If this is a ServerError, or an unknown type, log it out and try to continue. Otherwise, escalate so we can fix things and retry.
|
|
223
|
+
if (!(error instanceof OAuthError) || error instanceof ServerError) ;
|
|
224
|
+
else {
|
|
225
|
+
// Refresh failed for another reason, re-throw
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
77
228
|
}
|
|
78
229
|
}
|
|
79
230
|
const state = provider.state ? await provider.state() : undefined;
|
|
@@ -137,33 +288,12 @@ function extractResourceMetadataUrl(res) {
|
|
|
137
288
|
* If the server returns a 404 for the well-known endpoint, this function will
|
|
138
289
|
* return `undefined`. Any other errors will be thrown as exceptions.
|
|
139
290
|
*/
|
|
140
|
-
async function discoverOAuthProtectedResourceMetadata(serverUrl, opts) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
else {
|
|
147
|
-
url = new URL("/.well-known/oauth-protected-resource", serverUrl);
|
|
148
|
-
}
|
|
149
|
-
let response;
|
|
150
|
-
try {
|
|
151
|
-
response = await fetch(url, {
|
|
152
|
-
headers: {
|
|
153
|
-
"MCP-Protocol-Version": (_a = opts === null || opts === void 0 ? void 0 : opts.protocolVersion) !== null && _a !== void 0 ? _a : LATEST_PROTOCOL_VERSION
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
catch (error) {
|
|
158
|
-
// CORS errors come back as TypeError
|
|
159
|
-
if (error instanceof TypeError) {
|
|
160
|
-
response = await fetch(url);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
throw error;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (response.status === 404) {
|
|
291
|
+
async function discoverOAuthProtectedResourceMetadata(serverUrl, opts, fetchFn = fetch) {
|
|
292
|
+
const response = await discoverMetadataWithFallback(serverUrl, 'oauth-protected-resource', fetchFn, {
|
|
293
|
+
protocolVersion: opts === null || opts === void 0 ? void 0 : opts.protocolVersion,
|
|
294
|
+
metadataUrl: opts === null || opts === void 0 ? void 0 : opts.resourceMetadataUrl,
|
|
295
|
+
});
|
|
296
|
+
if (!response || response.status === 404) {
|
|
167
297
|
throw new Error(`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`);
|
|
168
298
|
}
|
|
169
299
|
if (!response.ok) {
|
|
@@ -174,15 +304,15 @@ async function discoverOAuthProtectedResourceMetadata(serverUrl, opts) {
|
|
|
174
304
|
/**
|
|
175
305
|
* Helper function to handle fetch with CORS retry logic
|
|
176
306
|
*/
|
|
177
|
-
async function fetchWithCorsRetry(url, headers) {
|
|
307
|
+
async function fetchWithCorsRetry(url, headers, fetchFn = fetch) {
|
|
178
308
|
try {
|
|
179
|
-
return await
|
|
309
|
+
return await fetchFn(url, { headers });
|
|
180
310
|
}
|
|
181
311
|
catch (error) {
|
|
182
312
|
if (error instanceof TypeError) {
|
|
183
313
|
if (headers) {
|
|
184
314
|
// CORS errors come back as TypeError, retry without headers
|
|
185
|
-
return fetchWithCorsRetry(url);
|
|
315
|
+
return fetchWithCorsRetry(url, undefined, fetchFn);
|
|
186
316
|
}
|
|
187
317
|
else {
|
|
188
318
|
// We're getting CORS errors on retry too, return undefined
|
|
@@ -193,57 +323,162 @@ async function fetchWithCorsRetry(url, headers) {
|
|
|
193
323
|
}
|
|
194
324
|
}
|
|
195
325
|
/**
|
|
196
|
-
* Constructs the well-known path for
|
|
326
|
+
* Constructs the well-known path for auth-related metadata discovery
|
|
197
327
|
*/
|
|
198
|
-
function buildWellKnownPath(pathname) {
|
|
199
|
-
|
|
328
|
+
function buildWellKnownPath(wellKnownPrefix, pathname = '', options = {}) {
|
|
329
|
+
// Strip trailing slash from pathname to avoid double slashes
|
|
200
330
|
if (pathname.endsWith('/')) {
|
|
201
|
-
|
|
202
|
-
wellKnownPath = wellKnownPath.slice(0, -1);
|
|
331
|
+
pathname = pathname.slice(0, -1);
|
|
203
332
|
}
|
|
204
|
-
return
|
|
333
|
+
return options.prependPathname
|
|
334
|
+
? `${pathname}/.well-known/${wellKnownPrefix}`
|
|
335
|
+
: `/.well-known/${wellKnownPrefix}${pathname}`;
|
|
205
336
|
}
|
|
206
337
|
/**
|
|
207
338
|
* Tries to discover OAuth metadata at a specific URL
|
|
208
339
|
*/
|
|
209
|
-
async function tryMetadataDiscovery(url, protocolVersion) {
|
|
340
|
+
async function tryMetadataDiscovery(url, protocolVersion, fetchFn = fetch) {
|
|
210
341
|
const headers = {
|
|
211
342
|
"MCP-Protocol-Version": protocolVersion
|
|
212
343
|
};
|
|
213
|
-
return await fetchWithCorsRetry(url, headers);
|
|
344
|
+
return await fetchWithCorsRetry(url, headers, fetchFn);
|
|
214
345
|
}
|
|
215
346
|
/**
|
|
216
347
|
* Determines if fallback to root discovery should be attempted
|
|
217
348
|
*/
|
|
218
349
|
function shouldAttemptFallback(response, pathname) {
|
|
219
|
-
return !response || response.status
|
|
350
|
+
return !response || (response.status >= 400 && response.status < 500) && pathname !== '/';
|
|
220
351
|
}
|
|
221
352
|
/**
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
* If the server returns a 404 for the well-known endpoint, this function will
|
|
225
|
-
* return `undefined`. Any other errors will be thrown as exceptions.
|
|
353
|
+
* Generic function for discovering OAuth metadata with fallback support
|
|
226
354
|
*/
|
|
227
|
-
async function
|
|
228
|
-
var _a;
|
|
229
|
-
const issuer = new URL(
|
|
230
|
-
const protocolVersion = (_a = void 0 ) !== null && _a !== void 0 ? _a : LATEST_PROTOCOL_VERSION;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
let response = await tryMetadataDiscovery(pathAwareUrl, protocolVersion);
|
|
235
|
-
// If path-aware discovery fails with 404, try fallback to root discovery
|
|
236
|
-
if (shouldAttemptFallback(response, issuer.pathname)) {
|
|
237
|
-
const rootUrl = new URL("/.well-known/oauth-authorization-server", issuer);
|
|
238
|
-
response = await tryMetadataDiscovery(rootUrl, protocolVersion);
|
|
355
|
+
async function discoverMetadataWithFallback(serverUrl, wellKnownType, fetchFn, opts) {
|
|
356
|
+
var _a, _b;
|
|
357
|
+
const issuer = new URL(serverUrl);
|
|
358
|
+
const protocolVersion = (_a = opts === null || opts === void 0 ? void 0 : opts.protocolVersion) !== null && _a !== void 0 ? _a : LATEST_PROTOCOL_VERSION;
|
|
359
|
+
let url;
|
|
360
|
+
if (opts === null || opts === void 0 ? void 0 : opts.metadataUrl) {
|
|
361
|
+
url = new URL(opts.metadataUrl);
|
|
239
362
|
}
|
|
240
|
-
|
|
241
|
-
|
|
363
|
+
else {
|
|
364
|
+
// Try path-aware discovery first
|
|
365
|
+
const wellKnownPath = buildWellKnownPath(wellKnownType, issuer.pathname);
|
|
366
|
+
url = new URL(wellKnownPath, (_b = opts === null || opts === void 0 ? void 0 : opts.metadataServerUrl) !== null && _b !== void 0 ? _b : issuer);
|
|
367
|
+
url.search = issuer.search;
|
|
368
|
+
}
|
|
369
|
+
let response = await tryMetadataDiscovery(url, protocolVersion, fetchFn);
|
|
370
|
+
// If path-aware discovery fails with 404 and we're not already at root, try fallback to root discovery
|
|
371
|
+
if (!(opts === null || opts === void 0 ? void 0 : opts.metadataUrl) && shouldAttemptFallback(response, issuer.pathname)) {
|
|
372
|
+
const rootUrl = new URL(`/.well-known/${wellKnownType}`, issuer);
|
|
373
|
+
response = await tryMetadataDiscovery(rootUrl, protocolVersion, fetchFn);
|
|
374
|
+
}
|
|
375
|
+
return response;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Builds a list of discovery URLs to try for authorization server metadata.
|
|
379
|
+
* URLs are returned in priority order:
|
|
380
|
+
* 1. OAuth metadata at the given URL
|
|
381
|
+
* 2. OAuth metadata at root (if URL has path)
|
|
382
|
+
* 3. OIDC metadata endpoints
|
|
383
|
+
*/
|
|
384
|
+
function buildDiscoveryUrls(authorizationServerUrl) {
|
|
385
|
+
const url = typeof authorizationServerUrl === 'string' ? new URL(authorizationServerUrl) : authorizationServerUrl;
|
|
386
|
+
const hasPath = url.pathname !== '/';
|
|
387
|
+
const urlsToTry = [];
|
|
388
|
+
if (!hasPath) {
|
|
389
|
+
// Root path: https://example.com/.well-known/oauth-authorization-server
|
|
390
|
+
urlsToTry.push({
|
|
391
|
+
url: new URL('/.well-known/oauth-authorization-server', url.origin),
|
|
392
|
+
type: 'oauth'
|
|
393
|
+
});
|
|
394
|
+
// OIDC: https://example.com/.well-known/openid-configuration
|
|
395
|
+
urlsToTry.push({
|
|
396
|
+
url: new URL(`/.well-known/openid-configuration`, url.origin),
|
|
397
|
+
type: 'oidc'
|
|
398
|
+
});
|
|
399
|
+
return urlsToTry;
|
|
242
400
|
}
|
|
243
|
-
|
|
244
|
-
|
|
401
|
+
// Strip trailing slash from pathname to avoid double slashes
|
|
402
|
+
let pathname = url.pathname;
|
|
403
|
+
if (pathname.endsWith('/')) {
|
|
404
|
+
pathname = pathname.slice(0, -1);
|
|
405
|
+
}
|
|
406
|
+
// 1. OAuth metadata at the given URL
|
|
407
|
+
// Insert well-known before the path: https://example.com/.well-known/oauth-authorization-server/tenant1
|
|
408
|
+
urlsToTry.push({
|
|
409
|
+
url: new URL(`/.well-known/oauth-authorization-server${pathname}`, url.origin),
|
|
410
|
+
type: 'oauth'
|
|
411
|
+
});
|
|
412
|
+
// Root path: https://example.com/.well-known/oauth-authorization-server
|
|
413
|
+
urlsToTry.push({
|
|
414
|
+
url: new URL('/.well-known/oauth-authorization-server', url.origin),
|
|
415
|
+
type: 'oauth'
|
|
416
|
+
});
|
|
417
|
+
// 3. OIDC metadata endpoints
|
|
418
|
+
// RFC 8414 style: Insert /.well-known/openid-configuration before the path
|
|
419
|
+
urlsToTry.push({
|
|
420
|
+
url: new URL(`/.well-known/openid-configuration${pathname}`, url.origin),
|
|
421
|
+
type: 'oidc'
|
|
422
|
+
});
|
|
423
|
+
// OIDC Discovery 1.0 style: Append /.well-known/openid-configuration after the path
|
|
424
|
+
urlsToTry.push({
|
|
425
|
+
url: new URL(`${pathname}/.well-known/openid-configuration`, url.origin),
|
|
426
|
+
type: 'oidc'
|
|
427
|
+
});
|
|
428
|
+
return urlsToTry;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Discovers authorization server metadata with support for RFC 8414 OAuth 2.0 Authorization Server Metadata
|
|
432
|
+
* and OpenID Connect Discovery 1.0 specifications.
|
|
433
|
+
*
|
|
434
|
+
* This function implements a fallback strategy for authorization server discovery:
|
|
435
|
+
* 1. Attempts RFC 8414 OAuth metadata discovery first
|
|
436
|
+
* 2. If OAuth discovery fails, falls back to OpenID Connect Discovery
|
|
437
|
+
*
|
|
438
|
+
* @param authorizationServerUrl - The authorization server URL obtained from the MCP Server's
|
|
439
|
+
* protected resource metadata, or the MCP server's URL if the
|
|
440
|
+
* metadata was not found.
|
|
441
|
+
* @param options - Configuration options
|
|
442
|
+
* @param options.fetchFn - Optional fetch function for making HTTP requests, defaults to global fetch
|
|
443
|
+
* @param options.protocolVersion - MCP protocol version to use, defaults to LATEST_PROTOCOL_VERSION
|
|
444
|
+
* @returns Promise resolving to authorization server metadata, or undefined if discovery fails
|
|
445
|
+
*/
|
|
446
|
+
async function discoverAuthorizationServerMetadata(authorizationServerUrl, { fetchFn = fetch, protocolVersion = LATEST_PROTOCOL_VERSION, } = {}) {
|
|
447
|
+
var _a;
|
|
448
|
+
const headers = { 'MCP-Protocol-Version': protocolVersion };
|
|
449
|
+
// Get the list of URLs to try
|
|
450
|
+
const urlsToTry = buildDiscoveryUrls(authorizationServerUrl);
|
|
451
|
+
// Try each URL in order
|
|
452
|
+
for (const { url: endpointUrl, type } of urlsToTry) {
|
|
453
|
+
const response = await fetchWithCorsRetry(endpointUrl, headers, fetchFn);
|
|
454
|
+
if (!response) {
|
|
455
|
+
/**
|
|
456
|
+
* CORS error occurred - don't throw as the endpoint may not allow CORS,
|
|
457
|
+
* continue trying other possible endpoints
|
|
458
|
+
*/
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
// Continue looking for any 4xx response code.
|
|
463
|
+
if (response.status >= 400 && response.status < 500) {
|
|
464
|
+
continue; // Try next URL
|
|
465
|
+
}
|
|
466
|
+
throw new Error(`HTTP ${response.status} trying to load ${type === 'oauth' ? 'OAuth' : 'OpenID provider'} metadata from ${endpointUrl}`);
|
|
467
|
+
}
|
|
468
|
+
// Parse and validate based on type
|
|
469
|
+
if (type === 'oauth') {
|
|
470
|
+
return OAuthMetadataSchema.parse(await response.json());
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
const metadata = OpenIdProviderDiscoveryMetadataSchema.parse(await response.json());
|
|
474
|
+
// MCP spec requires OIDC providers to support S256 PKCE
|
|
475
|
+
if (!((_a = metadata.code_challenge_methods_supported) === null || _a === void 0 ? void 0 : _a.includes('S256'))) {
|
|
476
|
+
throw new Error(`Incompatible OIDC provider at ${endpointUrl}: does not support S256 code challenge method required by MCP specification`);
|
|
477
|
+
}
|
|
478
|
+
return metadata;
|
|
479
|
+
}
|
|
245
480
|
}
|
|
246
|
-
return
|
|
481
|
+
return undefined;
|
|
247
482
|
}
|
|
248
483
|
/**
|
|
249
484
|
* Begins the authorization flow with the given server, by generating a PKCE challenge and constructing the authorization URL.
|
|
@@ -280,6 +515,12 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
|
|
|
280
515
|
if (scope) {
|
|
281
516
|
authorizationUrl.searchParams.set("scope", scope);
|
|
282
517
|
}
|
|
518
|
+
if (scope === null || scope === void 0 ? void 0 : scope.includes("offline_access")) {
|
|
519
|
+
// if the request includes the OIDC-only "offline_access" scope,
|
|
520
|
+
// we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
|
|
521
|
+
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
|
522
|
+
authorizationUrl.searchParams.append("prompt", "consent");
|
|
523
|
+
}
|
|
283
524
|
if (resource) {
|
|
284
525
|
authorizationUrl.searchParams.set("resource", resource.href);
|
|
285
526
|
}
|
|
@@ -287,50 +528,73 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
|
|
|
287
528
|
}
|
|
288
529
|
/**
|
|
289
530
|
* Exchanges an authorization code for an access token with the given server.
|
|
531
|
+
*
|
|
532
|
+
* Supports multiple client authentication methods as specified in OAuth 2.1:
|
|
533
|
+
* - Automatically selects the best authentication method based on server support
|
|
534
|
+
* - Falls back to appropriate defaults when server metadata is unavailable
|
|
535
|
+
*
|
|
536
|
+
* @param authorizationServerUrl - The authorization server's base URL
|
|
537
|
+
* @param options - Configuration object containing client info, auth code, etc.
|
|
538
|
+
* @returns Promise resolving to OAuth tokens
|
|
539
|
+
* @throws {Error} When token exchange fails or authentication is invalid
|
|
290
540
|
*/
|
|
291
|
-
async function exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, codeVerifier, redirectUri, resource, }) {
|
|
541
|
+
async function exchangeAuthorization(authorizationServerUrl, { metadata, clientInformation, authorizationCode, codeVerifier, redirectUri, resource, addClientAuthentication, fetchFn, }) {
|
|
542
|
+
var _a;
|
|
292
543
|
const grantType = "authorization_code";
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
else {
|
|
302
|
-
tokenUrl = new URL("/token", authorizationServerUrl);
|
|
544
|
+
const tokenUrl = (metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint)
|
|
545
|
+
? new URL(metadata.token_endpoint)
|
|
546
|
+
: new URL("/token", authorizationServerUrl);
|
|
547
|
+
if ((metadata === null || metadata === void 0 ? void 0 : metadata.grant_types_supported) &&
|
|
548
|
+
!metadata.grant_types_supported.includes(grantType)) {
|
|
549
|
+
throw new Error(`Incompatible auth server: does not support grant type ${grantType}`);
|
|
303
550
|
}
|
|
304
551
|
// Exchange code for tokens
|
|
552
|
+
const headers = new Headers({
|
|
553
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
554
|
+
"Accept": "application/json",
|
|
555
|
+
});
|
|
305
556
|
const params = new URLSearchParams({
|
|
306
557
|
grant_type: grantType,
|
|
307
|
-
client_id: clientInformation.client_id,
|
|
308
558
|
code: authorizationCode,
|
|
309
559
|
code_verifier: codeVerifier,
|
|
310
560
|
redirect_uri: String(redirectUri),
|
|
311
561
|
});
|
|
312
|
-
if (
|
|
313
|
-
|
|
562
|
+
if (addClientAuthentication) {
|
|
563
|
+
addClientAuthentication(headers, params, authorizationServerUrl, metadata);
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
// Determine and apply client authentication method
|
|
567
|
+
const supportedMethods = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint_auth_methods_supported) !== null && _a !== void 0 ? _a : [];
|
|
568
|
+
const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
|
|
569
|
+
applyClientAuthentication(authMethod, clientInformation, headers, params);
|
|
314
570
|
}
|
|
315
571
|
if (resource) {
|
|
316
572
|
params.set("resource", resource.href);
|
|
317
573
|
}
|
|
318
|
-
const response = await fetch(tokenUrl, {
|
|
574
|
+
const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(tokenUrl, {
|
|
319
575
|
method: "POST",
|
|
320
|
-
headers
|
|
321
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
322
|
-
},
|
|
576
|
+
headers,
|
|
323
577
|
body: params,
|
|
324
578
|
});
|
|
325
579
|
if (!response.ok) {
|
|
326
|
-
throw
|
|
580
|
+
throw await parseErrorResponse(response);
|
|
327
581
|
}
|
|
328
582
|
return OAuthTokensSchema.parse(await response.json());
|
|
329
583
|
}
|
|
330
584
|
/**
|
|
331
585
|
* Exchange a refresh token for an updated access token.
|
|
586
|
+
*
|
|
587
|
+
* Supports multiple client authentication methods as specified in OAuth 2.1:
|
|
588
|
+
* - Automatically selects the best authentication method based on server support
|
|
589
|
+
* - Preserves the original refresh token if a new one is not returned
|
|
590
|
+
*
|
|
591
|
+
* @param authorizationServerUrl - The authorization server's base URL
|
|
592
|
+
* @param options - Configuration object containing client info, refresh token, etc.
|
|
593
|
+
* @returns Promise resolving to OAuth tokens (preserves original refresh_token if not replaced)
|
|
594
|
+
* @throws {Error} When token refresh fails or authentication is invalid
|
|
332
595
|
*/
|
|
333
|
-
async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, }) {
|
|
596
|
+
async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn, }) {
|
|
597
|
+
var _a;
|
|
334
598
|
const grantType = "refresh_token";
|
|
335
599
|
let tokenUrl;
|
|
336
600
|
if (metadata) {
|
|
@@ -344,33 +608,39 @@ async function refreshAuthorization(authorizationServerUrl, { metadata, clientIn
|
|
|
344
608
|
tokenUrl = new URL("/token", authorizationServerUrl);
|
|
345
609
|
}
|
|
346
610
|
// Exchange refresh token
|
|
611
|
+
const headers = new Headers({
|
|
612
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
613
|
+
});
|
|
347
614
|
const params = new URLSearchParams({
|
|
348
615
|
grant_type: grantType,
|
|
349
|
-
client_id: clientInformation.client_id,
|
|
350
616
|
refresh_token: refreshToken,
|
|
351
617
|
});
|
|
352
|
-
if (
|
|
353
|
-
|
|
618
|
+
if (addClientAuthentication) {
|
|
619
|
+
addClientAuthentication(headers, params, authorizationServerUrl, metadata);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
// Determine and apply client authentication method
|
|
623
|
+
const supportedMethods = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.token_endpoint_auth_methods_supported) !== null && _a !== void 0 ? _a : [];
|
|
624
|
+
const authMethod = selectClientAuthMethod(clientInformation, supportedMethods);
|
|
625
|
+
applyClientAuthentication(authMethod, clientInformation, headers, params);
|
|
354
626
|
}
|
|
355
627
|
if (resource) {
|
|
356
628
|
params.set("resource", resource.href);
|
|
357
629
|
}
|
|
358
|
-
const response = await fetch(tokenUrl, {
|
|
630
|
+
const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(tokenUrl, {
|
|
359
631
|
method: "POST",
|
|
360
|
-
headers
|
|
361
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
362
|
-
},
|
|
632
|
+
headers,
|
|
363
633
|
body: params,
|
|
364
634
|
});
|
|
365
635
|
if (!response.ok) {
|
|
366
|
-
throw
|
|
636
|
+
throw await parseErrorResponse(response);
|
|
367
637
|
}
|
|
368
638
|
return OAuthTokensSchema.parse({ refresh_token: refreshToken, ...(await response.json()) });
|
|
369
639
|
}
|
|
370
640
|
/**
|
|
371
641
|
* Performs OAuth 2.0 Dynamic Client Registration according to RFC 7591.
|
|
372
642
|
*/
|
|
373
|
-
async function registerClient(authorizationServerUrl, { metadata, clientMetadata, }) {
|
|
643
|
+
async function registerClient(authorizationServerUrl, { metadata, clientMetadata, fetchFn, }) {
|
|
374
644
|
let registrationUrl;
|
|
375
645
|
if (metadata) {
|
|
376
646
|
if (!metadata.registration_endpoint) {
|
|
@@ -381,7 +651,7 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
|
|
|
381
651
|
else {
|
|
382
652
|
registrationUrl = new URL("/register", authorizationServerUrl);
|
|
383
653
|
}
|
|
384
|
-
const response = await fetch(registrationUrl, {
|
|
654
|
+
const response = await (fetchFn !== null && fetchFn !== void 0 ? fetchFn : fetch)(registrationUrl, {
|
|
385
655
|
method: "POST",
|
|
386
656
|
headers: {
|
|
387
657
|
"Content-Type": "application/json",
|
|
@@ -389,10 +659,10 @@ async function registerClient(authorizationServerUrl, { metadata, clientMetadata
|
|
|
389
659
|
body: JSON.stringify(clientMetadata),
|
|
390
660
|
});
|
|
391
661
|
if (!response.ok) {
|
|
392
|
-
throw
|
|
662
|
+
throw await parseErrorResponse(response);
|
|
393
663
|
}
|
|
394
664
|
return OAuthClientInformationFullSchema.parse(await response.json());
|
|
395
665
|
}
|
|
396
666
|
|
|
397
|
-
export { UnauthorizedError, auth,
|
|
667
|
+
export { UnauthorizedError, auth, buildDiscoveryUrls, discoverAuthorizationServerMetadata, discoverOAuthProtectedResourceMetadata, exchangeAuthorization, extractResourceMetadataUrl, parseErrorResponse, refreshAuthorization, registerClient, selectResourceURL, startAuthorization };
|
|
398
668
|
//# sourceMappingURL=auth.js.map
|